From 419dc56f5009d5454448ec6899486cdbe48b8c56 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 11:50:12 +0300 Subject: [PATCH 01/25] feat: screenshot config --- src/internal/config/config.go | 76 ++++++++++++++++-------- src/internal/screencapturer/interface.go | 26 ++++++++ 2 files changed, 78 insertions(+), 24 deletions(-) create mode 100644 src/internal/screencapturer/interface.go diff --git a/src/internal/config/config.go b/src/internal/config/config.go index 8a3638f..afd03a4 100644 --- a/src/internal/config/config.go +++ b/src/internal/config/config.go @@ -30,6 +30,7 @@ import ( type Config struct { App AppConfig `mapstructure:"app"` + Screenshot ScreenshotConfig `mapstructure:"screenshot"` Communication CommunicationConfig `mapstructure:"communication"` Telemetry TelemetryConfig `mapstructure:"telemetry"` Logging LoggingConfig `mapstructure:"logging"` @@ -44,6 +45,13 @@ type AppConfig struct { EnableCheckingUpdates bool `mapstructure:"enable_checking_updates"` } +type ScreenshotConfig struct { + ScreenIndex int `mapstructure:"screen_index"` + Interval int `mapstructure:"interval"` + Directory string `mapstructure:"directory"` + BufferCount int `mapstructure:"buffer_count"` +} + type CommunicationConfig struct { QrUrl string `mapstructure:"self_approve_url"` QrQueryToken string `mapstructure:"qr_query_token"` @@ -60,6 +68,10 @@ type LoggingConfig struct { Output string `mapstructure:"output"` } +func getTempDirectoryPath() string { + return os.TempDir() +} + func getDefaultConfig() Config { return Config{ App: AppConfig{ @@ -70,6 +82,12 @@ func getDefaultConfig() Config { Browser: "firefox", EnableCheckingUpdates: true, }, + Screenshot: ScreenshotConfig{ + ScreenIndex: 0, + Interval: 5, + Directory: getTempDirectoryPath(), + BufferCount: 5, + }, Logging: LoggingConfig{ Level: "info", Output: "stdout", @@ -117,6 +135,11 @@ func initializeViper(appName string) (*viper.Viper, string, error) { v.SetDefault("app.browser", defaults.App.Browser) v.SetDefault("app.enable_checking_updates", defaults.App.EnableCheckingUpdates) + v.SetDefault("screenshot.screen_index", defaults.Screenshot.ScreenIndex) + v.SetDefault("screenshot.interval", defaults.Screenshot.Interval) + v.SetDefault("screenshot.directory", defaults.Screenshot.Directory) + v.SetDefault("screenshot.buffer_count", defaults.Screenshot.BufferCount) + v.SetDefault("logging.level", defaults.Logging.Level) v.SetDefault("logging.output", defaults.Logging.Output) @@ -127,35 +150,40 @@ func initializeViper(appName string) (*viper.Viper, string, error) { } func (c *Config) Save() error { - v, _, err := initializeViper(constants.AppName) - if err != nil { - return fmt.Errorf("failed to initialize viper: %w", err) - } + v, _, err := initializeViper(constants.AppName) + if err != nil { + return fmt.Errorf("failed to initialize viper: %w", err) + } - v.Set("app.settings_reviewed", c.App.SettingsReviewed) - v.Set("app.enable_alarm", c.App.EnableAlarm) - v.Set("app.enable_link_opening", c.App.EnableLinkOpening) - v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi) - v.Set("app.browser", c.App.Browser) - v.Set("app.enable_checking_updates", c.App.EnableCheckingUpdates) + v.Set("app.settings_reviewed", c.App.SettingsReviewed) + v.Set("app.enable_alarm", c.App.EnableAlarm) + v.Set("app.enable_link_opening", c.App.EnableLinkOpening) + v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi) + v.Set("app.browser", c.App.Browser) + v.Set("app.enable_checking_updates", c.App.EnableCheckingUpdates) - v.Set("logging.level", c.Logging.Level) - v.Set("logging.output", c.Logging.Output) + v.Set("screenshot.screen_index", c.Screenshot.ScreenIndex) + v.Set("screenshot.interval", c.Screenshot.Interval) + v.Set("screenshot.directory", c.Screenshot.Directory) + v.Set("screenshot.buffer_count", c.Screenshot.BufferCount) - v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection) - v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports) + v.Set("logging.level", c.Logging.Level) + v.Set("logging.output", c.Logging.Output) - if c.Communication.QrUrl != "" { - v.Set("communication.self_approve_url", c.Communication.QrUrl) - } - if c.Communication.QrQueryToken != "" { - v.Set("communication.qr_query_token", c.Communication.QrQueryToken) - } - if c.Communication.ApiSelfApproveMethod != "" { - v.Set("communication.api_self_approve_method", c.Communication.ApiSelfApproveMethod) - } + v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection) + v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports) - return v.WriteConfig() + if c.Communication.QrUrl != "" { + v.Set("communication.self_approve_url", c.Communication.QrUrl) + } + if c.Communication.QrQueryToken != "" { + v.Set("communication.qr_query_token", c.Communication.QrQueryToken) + } + if c.Communication.ApiSelfApproveMethod != "" { + v.Set("communication.api_self_approve_method", c.Communication.ApiSelfApproveMethod) + } + + return v.WriteConfig() } func NewConfig() (*Config, error) { diff --git a/src/internal/screencapturer/interface.go b/src/internal/screencapturer/interface.go new file mode 100644 index 0000000..1db851d --- /dev/null +++ b/src/internal/screencapturer/interface.go @@ -0,0 +1,26 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package screencapturer + +type ScreenCapturer interface { + Start() error + Stop() error + Screenshot() (filepath string) +} -- 2.49.1 From 4a39c1e157821831f25f7bc1152f1beb792c10b3 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 14:31:00 +0300 Subject: [PATCH 02/25] feat: mediocre WholeScreenCapturer implementation --- src/cmd/main.go | 6 +- src/go.mod | 5 + src/go.sum | 11 + src/internal/screencapturer/interface.go | 3 +- .../screencapturer/wholescreencapturer.go | 218 ++++++++++++++++++ src/internal/screenshotter/screenshotter.go | 20 -- 6 files changed, 240 insertions(+), 23 deletions(-) create mode 100644 src/internal/screencapturer/wholescreencapturer.go delete mode 100644 src/internal/screenshotter/screenshotter.go diff --git a/src/cmd/main.go b/src/cmd/main.go index 1bf2699..d8dff87 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -23,6 +23,7 @@ import ( "git.weirdcat.su/weirdcat/auto-attendance/internal/config" "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" "go.uber.org/fx" ) @@ -32,9 +33,10 @@ func main() { config.NewConfig, logger.NewLogger, linkvalidator.NewLinkValidator, + screencapturer.NewWholeScreenCapturer, ), - fx.Invoke(func(log *logger.Logger) { - log.Info("Starting application..."); + fx.Invoke(func(log *logger.Logger, capturer screencapturer.ScreenCapturer) { + log.Debug("starting application...") }), ) diff --git a/src/go.mod b/src/go.mod index ec0d5e2..724c113 100644 --- a/src/go.mod +++ b/src/go.mod @@ -6,7 +6,12 @@ require github.com/spf13/viper v1.21.0 require ( github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gen2brain/shm v0.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/jezek/xgb v1.1.1 // indirect + github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 // indirect + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect diff --git a/src/go.sum b/src/go.sum index 724bd2d..e419078 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,7 +1,17 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY= +github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= +github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= @@ -28,6 +38,7 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= diff --git a/src/internal/screencapturer/interface.go b/src/internal/screencapturer/interface.go index 1db851d..ef2b976 100644 --- a/src/internal/screencapturer/interface.go +++ b/src/internal/screencapturer/interface.go @@ -20,7 +20,8 @@ package screencapturer type ScreenCapturer interface { + Init() error Start() error Stop() error - Screenshot() (filepath string) + Get() (filepath string, err error) } diff --git a/src/internal/screencapturer/wholescreencapturer.go b/src/internal/screencapturer/wholescreencapturer.go new file mode 100644 index 0000000..4a21c66 --- /dev/null +++ b/src/internal/screencapturer/wholescreencapturer.go @@ -0,0 +1,218 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package screencapturer + +import ( + "context" + "errors" + "fmt" + "image" + "image/png" + "os" + "path/filepath" + "sync" + "time" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/constants" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "github.com/kbinani/screenshot" + "go.uber.org/fx" +) + +var ( + ErrNotInitialized = errors.New("wholescreencapturer was not initialized") +) + +type wholeScreenCapturer struct { + config *config.Config + log *logger.Logger + + displayIndex int + displayBounds image.Rectangle + interval int + bufferCount int + tempDirectory string + initialized bool + running bool + + files []string + latest string + done chan struct{} + sync.WaitGroup +} + +// Get implements ScreenCapturer. +func (w *wholeScreenCapturer) Get() (filepath string, err error) { + if !w.initialized { + return "", ErrNotInitialized + } + if w.latest == "" { + return "", errors.New("no screenshot captured yet") + } + return w.latest, nil +} + +// Init implements ScreenCapturer. +func (w *wholeScreenCapturer) Init() (err error) { + + displayCount := screenshot.NumActiveDisplays() + w.displayIndex = w.config.Screenshot.ScreenIndex % displayCount + w.log.Debug("detected displays", "count", displayCount, "selected", w.displayIndex) + + w.displayBounds = screenshot.GetDisplayBounds(w.displayIndex) + w.log.Debug( + "display bounds set", + "dx", w.displayBounds.Dx(), + "dy", w.displayBounds.Dy(), + ) + + w.interval = w.config.Screenshot.Interval + w.log.Debug("screenshot interval set", "interval", w.interval) + + w.bufferCount = w.config.Screenshot.BufferCount + w.log.Debug("screenshot buffer count set", "count", w.bufferCount) + + if w.tempDirectory, err = w.createTempDirectory(); err != nil { + w.log.Error("failed to create temporary directory", "error", err) + return err + } + w.log.Debug("temporary directory created", "path", w.tempDirectory) + + w.files = make([]string, 0, w.bufferCount) + w.latest = "" + w.initialized = true + return nil +} + +// Start implements ScreenCapturer. +func (w *wholeScreenCapturer) Start() error { + if !w.initialized { + return ErrNotInitialized + } + if w.running { + w.log.Debug("wholescreencapturer is already running, ignoring start") + return nil + } + w.running = true + w.Go(func() { + w.captureLoop() + }) + w.log.Debug("wholescreencapturer started") + return nil +} + +// Stop implements ScreenCapturer. +func (w *wholeScreenCapturer) Stop() error { + if !w.initialized { + return ErrNotInitialized + } + if !w.running { + w.log.Debug("wholescreencapturer is not running, ignoring stop") + return nil + } + w.log.Debug("stopping wholescreencapturer") + close(w.done) + w.Wait() + w.running = false + w.log.Debug("wholescreencapturer stopped") + return nil +} + +func (w *wholeScreenCapturer) captureLoop() { + ticker := time.NewTicker(time.Duration(w.interval) * time.Second) + defer ticker.Stop() + + for { + select { + case <-w.done: + w.log.Debug("capture loop stopped") + return + case <-ticker.C: + fp, err := w.captureAndSave() + if err != nil { + w.log.Error("screenshot capture failed", "error", err) + continue + } + w.addToBuffer(fp) + } + } +} + +func (w *wholeScreenCapturer) captureAndSave() (string, error) { + img, err := screenshot.CaptureRect(w.displayBounds) + if err != nil { + w.log.Error("failed to capture screenshot", "error", err) + return "", err + } + + now := time.Now().UnixMilli() + filename := fmt.Sprintf("%d.png", now) + filePath := filepath.Join(w.tempDirectory, filename) + + file, err := os.Create(filePath) + if err != nil { + w.log.Error("failed to create screenshot file", "path", filePath, "error", err) + return "", err + } + + defer file.Close() + err = png.Encode(file, img) + if err != nil { + w.log.Error("failed to encode image into file", "path", filePath, "error", err) + return "", err + } + + w.log.Debug("Screenshot saved", "path", filePath) + return filePath, nil +} + +func (w *wholeScreenCapturer) addToBuffer(fp string) { + w.files = append(w.files, fp) + w.latest = fp + + if len(w.files) > w.bufferCount { + old := w.files[0] + if err := os.Remove(old); err != nil { + w.log.Warn("failed to remove old screenshot", "path", old, "error", err) + } else { + w.log.Debug("removed old screenshot", "path", old) + } + w.files = w.files[1:] + } +} + +func (w *wholeScreenCapturer) createTempDirectory() (path string, err error) { + return os.MkdirTemp(w.config.Screenshot.Directory, constants.AppName+"-*") +} + +func NewWholeScreenCapturer(lc fx.Lifecycle, config *config.Config, log *logger.Logger) ScreenCapturer { + capturer := &wholeScreenCapturer{ + config: config, + log: log, + done: make(chan struct{}), + } + lc.Append(fx.StopHook(func(ctx context.Context) error { + log.Debug("Stopping capturer (fx hook)") + return capturer.Stop() + })) + return capturer +} + diff --git a/src/internal/screenshotter/screenshotter.go b/src/internal/screenshotter/screenshotter.go deleted file mode 100644 index 87cf36e..0000000 --- a/src/internal/screenshotter/screenshotter.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2025 Nikolai Papin -// -// This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your -// browser. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -// the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package screenshotter -- 2.49.1 From d4fae7098ca12cd444f41cae932094ed307e6837 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 14:57:06 +0300 Subject: [PATCH 03/25] feat: mutex, graceful shutdown --- .../screencapturer/wholescreencapturer.go | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/internal/screencapturer/wholescreencapturer.go b/src/internal/screencapturer/wholescreencapturer.go index 4a21c66..bf15551 100644 --- a/src/internal/screencapturer/wholescreencapturer.go +++ b/src/internal/screencapturer/wholescreencapturer.go @@ -57,10 +57,13 @@ type wholeScreenCapturer struct { latest string done chan struct{} sync.WaitGroup + mu sync.RWMutex } // Get implements ScreenCapturer. func (w *wholeScreenCapturer) Get() (filepath string, err error) { + w.mu.RLock() + defer w.mu.Unlock() if !w.initialized { return "", ErrNotInitialized } @@ -185,6 +188,8 @@ func (w *wholeScreenCapturer) captureAndSave() (string, error) { } func (w *wholeScreenCapturer) addToBuffer(fp string) { + w.mu.Lock() + defer w.mu.Unlock() w.files = append(w.files, fp) w.latest = fp @@ -210,8 +215,25 @@ func NewWholeScreenCapturer(lc fx.Lifecycle, config *config.Config, log *logger. done: make(chan struct{}), } lc.Append(fx.StopHook(func(ctx context.Context) error { - log.Debug("Stopping capturer (fx hook)") - return capturer.Stop() + if (!capturer.initialized) { + log.Debug("wholescreencapturer not initialized, nothing to do") + return nil + } + + log.Debug("stopping wholescreencapturer") + + err := capturer.Stop() + if err != nil { + log.Error("failed to stop wholescreencapturer gracefully") + return err + } + + err = os.RemoveAll(capturer.tempDirectory) + if err != nil { + log.Error("failed to remove temp directory") + return err + } + return nil })) return capturer } -- 2.49.1 From 4876ba5954421f68e41cab472c25a9b28a7cdd99 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 15:49:20 +0300 Subject: [PATCH 04/25] feat: vision for qr-scanning and decoding --- src/cmd/main.go | 2 + src/go.mod | 3 ++ src/go.sum | 6 +++ src/internal/vision/vision.go | 70 ++++++++++++++++++++++++++++++- src/internal/vision/visiondata.go | 3 ++ 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/internal/vision/visiondata.go diff --git a/src/cmd/main.go b/src/cmd/main.go index d8dff87..75773bb 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -24,6 +24,7 @@ import ( "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" + "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" "go.uber.org/fx" ) @@ -34,6 +35,7 @@ func main() { logger.NewLogger, linkvalidator.NewLinkValidator, screencapturer.NewWholeScreenCapturer, + vision.NewVision, ), fx.Invoke(func(log *logger.Logger, capturer screencapturer.ScreenCapturer) { log.Debug("starting application...") diff --git a/src/go.mod b/src/go.mod index 724c113..a11df27 100644 --- a/src/go.mod +++ b/src/go.mod @@ -12,6 +12,7 @@ require ( github.com/jezek/xgb v1.1.1 // indirect github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 // indirect github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect + github.com/makiuchi-d/gozxing v0.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect @@ -24,6 +25,8 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + gocv.io/x/gocv v0.42.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.28.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/src/go.sum b/src/go.sum index e419078..c83bb0a 100644 --- a/src/go.sum +++ b/src/go.sum @@ -12,6 +12,8 @@ github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWr github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ= github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= +github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= @@ -38,9 +40,13 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gocv.io/x/gocv v0.42.0 h1:AAsrFJH2aIsQHukkCovWqj0MCGZleQpVyf5gNVRXjQI= +gocv.io/x/gocv v0.42.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/src/internal/vision/vision.go b/src/internal/vision/vision.go index e4ed0da..867ca33 100644 --- a/src/internal/vision/vision.go +++ b/src/internal/vision/vision.go @@ -1,7 +1,7 @@ // Copyright (c) 2025 Nikolai Papin // // This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your +// self-attend QR-codes during lectures and opens their URLs in your // browser. // // This program is free software: you can redistribute it and/or modify @@ -18,3 +18,71 @@ // along with this program. If not, see . package vision + +import ( + "errors" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode" + "gocv.io/x/gocv" +) + +type Vision interface { + AnalyzeImage(filePath string) (data VisionData, err error) +} + +type visionImpl struct { + log *logger.Logger +} + +var ( + ErrImageEmpty = errors.New("Image from file was empty") +) + +// AnalyzeImage implements Vision. +func (v *visionImpl) AnalyzeImage(filePath string) (data VisionData, err error) { + v.log.Debug("analyzing image for qr codes", "filePath", filePath) + + img := gocv.IMRead(filePath, gocv.IMReadColor) + if img.Empty() { + v.log.Error("could not read image file", "filePath", filePath) + return VisionData{}, ErrImageEmpty + } + defer img.Close() + + // Convert to grayscale for QR code detection + gray := gocv.NewMat() + defer gray.Close() + gocv.CvtColor(img, &gray, gocv.ColorBGRToGray) + + // Convert gocv.Mat to image.Image for gozxing + imgGray, err := gray.ToImage() + if err != nil { + v.log.Error("failed to convert image", "error", err) + return VisionData{}, err + } + + // Create a binary bitmap from the image + bmp, err := gozxing.NewBinaryBitmapFromImage(imgGray) + if err != nil { + v.log.Error("failed to create binary bitmap", "error", err) + return VisionData{}, err + } + + reader := qrcode.NewQRCodeReader() + result, err := reader.Decode(bmp, nil) + if err != nil { + v.log.Debug("no qr code found in image", "filePath", filePath, "error", err) + return VisionData{}, nil + } + + v.log.Info("QR code decoded successfully", "content", result.GetText()) + + data = VisionData{result.GetText()} + return data, nil +} + +func NewVision(log *logger.Logger) Vision { + return &visionImpl{log: log} +} diff --git a/src/internal/vision/visiondata.go b/src/internal/vision/visiondata.go new file mode 100644 index 0000000..40f2700 --- /dev/null +++ b/src/internal/vision/visiondata.go @@ -0,0 +1,3 @@ +package vision + +type VisionData []string -- 2.49.1 From a857f12cb5fdd0f900151a999355c8fafa3e7fa1 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 15:52:38 +0300 Subject: [PATCH 05/25] chore: linted --- src/internal/browserlauncher/chrome.go | 2 +- src/internal/browserlauncher/firefox.go | 2 +- src/internal/config/config.go | 10 ++++---- src/internal/linkvalidator/linkvalidator.go | 10 ++++---- src/internal/screencapturer/interface.go | 2 +- .../screencapturer/wholescreencapturer.go | 25 ++++++++++--------- src/internal/vision/vision.go | 3 ++- src/utils/platform.go | 2 +- 8 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/internal/browserlauncher/chrome.go b/src/internal/browserlauncher/chrome.go index fdadbe0..2639813 100644 --- a/src/internal/browserlauncher/chrome.go +++ b/src/internal/browserlauncher/chrome.go @@ -1,7 +1,7 @@ // Copyright (c) 2025 Nikolai Papin // // This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your +// self-attend QR-codes during lectures and opens their URLs in your // browser. // // This program is free software: you can redistribute it and/or modify diff --git a/src/internal/browserlauncher/firefox.go b/src/internal/browserlauncher/firefox.go index fdadbe0..2639813 100644 --- a/src/internal/browserlauncher/firefox.go +++ b/src/internal/browserlauncher/firefox.go @@ -1,7 +1,7 @@ // Copyright (c) 2025 Nikolai Papin // // This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your +// self-attend QR-codes during lectures and opens their URLs in your // browser. // // This program is free software: you can redistribute it and/or modify diff --git a/src/internal/config/config.go b/src/internal/config/config.go index afd03a4..baf4bd3 100644 --- a/src/internal/config/config.go +++ b/src/internal/config/config.go @@ -46,10 +46,10 @@ type AppConfig struct { } type ScreenshotConfig struct { - ScreenIndex int `mapstructure:"screen_index"` - Interval int `mapstructure:"interval"` + ScreenIndex int `mapstructure:"screen_index"` + Interval int `mapstructure:"interval"` Directory string `mapstructure:"directory"` - BufferCount int `mapstructure:"buffer_count"` + BufferCount int `mapstructure:"buffer_count"` } type CommunicationConfig struct { @@ -84,8 +84,8 @@ func getDefaultConfig() Config { }, Screenshot: ScreenshotConfig{ ScreenIndex: 0, - Interval: 5, - Directory: getTempDirectoryPath(), + Interval: 5, + Directory: getTempDirectoryPath(), BufferCount: 5, }, Logging: LoggingConfig{ diff --git a/src/internal/linkvalidator/linkvalidator.go b/src/internal/linkvalidator/linkvalidator.go index b4e8173..c78e105 100644 --- a/src/internal/linkvalidator/linkvalidator.go +++ b/src/internal/linkvalidator/linkvalidator.go @@ -52,10 +52,10 @@ func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool) if v.config.Communication.QrUrl != "" { expectedURL, err := url.Parse(v.config.Communication.QrUrl) if err == nil { - if parsedURL.Scheme != expectedURL.Scheme || - parsedURL.Host != expectedURL.Host || - parsedURL.Path != expectedURL.Path { - v.log.Debug("URL doesn't match configured QR URL pattern", + if parsedURL.Scheme != expectedURL.Scheme || + parsedURL.Host != expectedURL.Host || + parsedURL.Path != expectedURL.Path { + v.log.Debug("URL doesn't match configured QR URL pattern", "url", rawURL, "expected", v.config.Communication.QrUrl) return "", false } @@ -64,7 +64,7 @@ func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool) if v.config.Communication.ApiSelfApproveMethod != "" { if !strings.Contains(parsedURL.Path, v.config.Communication.ApiSelfApproveMethod) { - v.log.Debug("URL doesn't contain expected API method", + v.log.Debug("URL doesn't contain expected API method", "url", parsedURL.Path, "expected", v.config.Communication.ApiSelfApproveMethod) return "", false } diff --git a/src/internal/screencapturer/interface.go b/src/internal/screencapturer/interface.go index ef2b976..6045dcd 100644 --- a/src/internal/screencapturer/interface.go +++ b/src/internal/screencapturer/interface.go @@ -1,7 +1,7 @@ // Copyright (c) 2025 Nikolai Papin // // This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your +// self-attend QR-codes during lectures and opens their URLs in your // browser. // // This program is free software: you can redistribute it and/or modify diff --git a/src/internal/screencapturer/wholescreencapturer.go b/src/internal/screencapturer/wholescreencapturer.go index bf15551..12c43bc 100644 --- a/src/internal/screencapturer/wholescreencapturer.go +++ b/src/internal/screencapturer/wholescreencapturer.go @@ -43,15 +43,15 @@ var ( type wholeScreenCapturer struct { config *config.Config - log *logger.Logger + log *logger.Logger - displayIndex int - displayBounds image.Rectangle - interval int - bufferCount int + displayIndex int + displayBounds image.Rectangle + interval int + bufferCount int tempDirectory string - initialized bool - running bool + initialized bool + running bool files []string latest string @@ -189,7 +189,7 @@ func (w *wholeScreenCapturer) captureAndSave() (string, error) { func (w *wholeScreenCapturer) addToBuffer(fp string) { w.mu.Lock() - defer w.mu.Unlock() + defer w.mu.Unlock() w.files = append(w.files, fp) w.latest = fp @@ -208,20 +208,22 @@ func (w *wholeScreenCapturer) createTempDirectory() (path string, err error) { return os.MkdirTemp(w.config.Screenshot.Directory, constants.AppName+"-*") } -func NewWholeScreenCapturer(lc fx.Lifecycle, config *config.Config, log *logger.Logger) ScreenCapturer { +func NewWholeScreenCapturer( + lc fx.Lifecycle, config *config.Config, log *logger.Logger, +) ScreenCapturer { capturer := &wholeScreenCapturer{ config: config, log: log, done: make(chan struct{}), } lc.Append(fx.StopHook(func(ctx context.Context) error { - if (!capturer.initialized) { + if !capturer.initialized { log.Debug("wholescreencapturer not initialized, nothing to do") return nil } log.Debug("stopping wholescreencapturer") - + err := capturer.Stop() if err != nil { log.Error("failed to stop wholescreencapturer gracefully") @@ -237,4 +239,3 @@ func NewWholeScreenCapturer(lc fx.Lifecycle, config *config.Config, log *logger. })) return capturer } - diff --git a/src/internal/vision/vision.go b/src/internal/vision/vision.go index 867ca33..45881eb 100644 --- a/src/internal/vision/vision.go +++ b/src/internal/vision/vision.go @@ -42,6 +42,7 @@ var ( // AnalyzeImage implements Vision. func (v *visionImpl) AnalyzeImage(filePath string) (data VisionData, err error) { + // TODO: scanning for multiple QR-codes at once v.log.Debug("analyzing image for qr codes", "filePath", filePath) img := gocv.IMRead(filePath, gocv.IMReadColor) @@ -78,7 +79,7 @@ func (v *visionImpl) AnalyzeImage(filePath string) (data VisionData, err error) } v.log.Info("QR code decoded successfully", "content", result.GetText()) - + data = VisionData{result.GetText()} return data, nil } diff --git a/src/utils/platform.go b/src/utils/platform.go index 7f27a60..f898cdc 100644 --- a/src/utils/platform.go +++ b/src/utils/platform.go @@ -1,7 +1,7 @@ // Copyright (c) 2025 Nikolai Papin // // This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your +// self-attend QR-codes during lectures and opens their URLs in your // browser. // // This program is free software: you can redistribute it and/or modify -- 2.49.1 From 39fcd1f18cba35b31d9d57de121bd0507373b98e Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 16:24:06 +0300 Subject: [PATCH 06/25] refactor: removed cross-compilation cuz opencv don't wanna --- Makefile | 3 --- src/go.mod | 12 +++++++----- src/go.sum | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index ee2c019..7720f00 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,3 @@ build-linux: build-windows: @$(MKDIR) $(BUILD_DIR) @cd $(SRC_DIR) && GOOS=windows GOARCH=amd64 $(GOBUILD) -o ../$(BUILD_DIR)/$(APP_NAME).exe $(CMD_PATH) - -build-all: build-linux build-windows - @echo "Cross-compilation complete" diff --git a/src/go.mod b/src/go.mod index a11df27..58b8632 100644 --- a/src/go.mod +++ b/src/go.mod @@ -2,7 +2,13 @@ module git.weirdcat.su/weirdcat/auto-attendance go 1.25.4 -require github.com/spf13/viper v1.21.0 +require ( + github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 + github.com/makiuchi-d/gozxing v0.1.1 + github.com/spf13/viper v1.21.0 + go.uber.org/fx v1.24.0 + gocv.io/x/gocv v0.42.0 +) require ( github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -10,9 +16,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/jezek/xgb v1.1.1 // indirect - github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 // indirect github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect - github.com/makiuchi-d/gozxing v0.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect @@ -21,11 +25,9 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/dig v1.19.0 // indirect - go.uber.org/fx v1.24.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - gocv.io/x/gocv v0.42.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/src/go.sum b/src/go.sum index c83bb0a..5d866fb 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,3 +1,7 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY= @@ -6,16 +10,26 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -28,12 +42,16 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= @@ -50,3 +68,7 @@ golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -- 2.49.1 From dcf9f695152b94a650b0c028b5fb04a368b58d96 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 17:55:50 +0300 Subject: [PATCH 07/25] fix: mutex in wholescreencapturer --- .../screencapturer/wholescreencapturer.go | 14 ++++++------- src/utils/platform.go | 20 ------------------- 2 files changed, 7 insertions(+), 27 deletions(-) delete mode 100644 src/utils/platform.go diff --git a/src/internal/screencapturer/wholescreencapturer.go b/src/internal/screencapturer/wholescreencapturer.go index 12c43bc..8b00a09 100644 --- a/src/internal/screencapturer/wholescreencapturer.go +++ b/src/internal/screencapturer/wholescreencapturer.go @@ -64,13 +64,13 @@ type wholeScreenCapturer struct { func (w *wholeScreenCapturer) Get() (filepath string, err error) { w.mu.RLock() defer w.mu.Unlock() - if !w.initialized { - return "", ErrNotInitialized - } - if w.latest == "" { - return "", errors.New("no screenshot captured yet") - } - return w.latest, nil + if !w.initialized { + return "", ErrNotInitialized + } + if w.latest == "" { + return "", errors.New("no screenshot captured yet") + } + return w.latest, nil } // Init implements ScreenCapturer. diff --git a/src/utils/platform.go b/src/utils/platform.go deleted file mode 100644 index f898cdc..0000000 --- a/src/utils/platform.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2025 Nikolai Papin -// -// This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your -// browser. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -// the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package platform -- 2.49.1 From d079c66dc26f5ae3f142a30a33c156547a8ea9f8 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 18:07:25 +0300 Subject: [PATCH 08/25] feat: browserlauncher --- .../browserlauncher/browserlauncher.go | 75 +++++++++++++++++++ src/internal/browserlauncher/chrome.go | 20 ----- src/internal/browserlauncher/firefox.go | 20 ----- src/internal/config/config.go | 12 ++- 4 files changed, 83 insertions(+), 44 deletions(-) create mode 100644 src/internal/browserlauncher/browserlauncher.go delete mode 100644 src/internal/browserlauncher/chrome.go delete mode 100644 src/internal/browserlauncher/firefox.go diff --git a/src/internal/browserlauncher/browserlauncher.go b/src/internal/browserlauncher/browserlauncher.go new file mode 100644 index 0000000..bc14a7d --- /dev/null +++ b/src/internal/browserlauncher/browserlauncher.go @@ -0,0 +1,75 @@ +package browserlauncher + +import ( + "fmt" + "os/exec" + "runtime" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" +) + +type BrowserLauncher interface { + OpenDefault(url string) error + OpenCustom(url string) error + OpenAuto(url string) error +} + +type browserLauncherImpl struct { + config *config.Config + log *logger.Logger + useCustomCommand bool + customCommand string +} + +// OpenAuto implements BrowserLauncher. +func (b *browserLauncherImpl) OpenAuto(url string) error { + if (b.useCustomCommand) { + return b.OpenCustom(url) + } else { + return b.OpenDefault(url) + } +} + +// OpenCustom implements BrowserLauncher. +func (b *browserLauncherImpl) OpenCustom(url string) error { + command := fmt.Sprintf(b.customCommand, url) + + b.log.Debug("opening link with custom command", "command", command, "url", url) + err := exec.Command(command).Start() + + if err != nil { + b.log.Error("failed to open link with custom command", "command", command, "url", url, "error", err) + } + + return err +} + +// OpenDefault implements BrowserLauncher. +func (b *browserLauncherImpl) OpenDefault(url string) (err error) { + b.log.Debug("opening link with default browser", "url", url) + switch runtime.GOOS { + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + default: + err = exec.Command("xdg-open", url).Start() + } + + if err != nil { + b.log.Error("failed to open link with default browser", "url", url, "error", err) + } + + return err +} + +func NewBrowserLauncher(config *config.Config, log *logger.Logger) BrowserLauncher { + + useCustomCommand := config.App.UseCustomBrowserCommand + customCommand := config.App.BrowserOpenCommand + return &browserLauncherImpl{ + config: config, + log: log, + useCustomCommand: useCustomCommand, + customCommand: customCommand, + } +} diff --git a/src/internal/browserlauncher/chrome.go b/src/internal/browserlauncher/chrome.go deleted file mode 100644 index 2639813..0000000 --- a/src/internal/browserlauncher/chrome.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2025 Nikolai Papin -// -// This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your -// browser. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -// the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package browserlauncher diff --git a/src/internal/browserlauncher/firefox.go b/src/internal/browserlauncher/firefox.go deleted file mode 100644 index 2639813..0000000 --- a/src/internal/browserlauncher/firefox.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2025 Nikolai Papin -// -// This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your -// browser. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -// the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package browserlauncher diff --git a/src/internal/config/config.go b/src/internal/config/config.go index baf4bd3..2cae791 100644 --- a/src/internal/config/config.go +++ b/src/internal/config/config.go @@ -41,7 +41,8 @@ type AppConfig struct { EnableAlarm bool `mapstructure:"enable_alarm"` EnableLinkOpening bool `mapstructure:"enable_link_opening"` UseAttendanceJounralApi bool `mapstructure:"use_attendance_journal_api"` - Browser string `mapstructure:"browser"` + UseCustomBrowserCommand bool `mapstructure:"use_custom_browser_command"` + BrowserOpenCommand string `mapstructure:"browser_open_command"` EnableCheckingUpdates bool `mapstructure:"enable_checking_updates"` } @@ -79,7 +80,8 @@ func getDefaultConfig() Config { EnableAlarm: false, EnableLinkOpening: true, UseAttendanceJounralApi: false, - Browser: "firefox", + UseCustomBrowserCommand: false, + BrowserOpenCommand: "firefox %s", EnableCheckingUpdates: true, }, Screenshot: ScreenshotConfig{ @@ -132,7 +134,8 @@ func initializeViper(appName string) (*viper.Viper, string, error) { v.SetDefault("app.enable_alarm", defaults.App.EnableAlarm) v.SetDefault("app.enable_link_opening", defaults.App.EnableLinkOpening) v.SetDefault("app.use_attendance_journal_api", defaults.App.UseAttendanceJounralApi) - v.SetDefault("app.browser", defaults.App.Browser) + v.SetDefault("app.use_custom_browser_command", defaults.App.UseCustomBrowserCommand) + v.SetDefault("app.browser_open_command", defaults.App.BrowserOpenCommand) v.SetDefault("app.enable_checking_updates", defaults.App.EnableCheckingUpdates) v.SetDefault("screenshot.screen_index", defaults.Screenshot.ScreenIndex) @@ -159,7 +162,8 @@ func (c *Config) Save() error { v.Set("app.enable_alarm", c.App.EnableAlarm) v.Set("app.enable_link_opening", c.App.EnableLinkOpening) v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi) - v.Set("app.browser", c.App.Browser) + v.Set("app.use_custom_browser_command", c.App.UseCustomBrowserCommand) + v.Set("app.browser_open_command", c.App.BrowserOpenCommand) v.Set("app.enable_checking_updates", c.App.EnableCheckingUpdates) v.Set("screenshot.screen_index", c.Screenshot.ScreenIndex) -- 2.49.1 From d367dd09090fa6cca54d353c4e1b8fd416001ab2 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 18:15:18 +0300 Subject: [PATCH 09/25] chore: docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b6690ad..d08dc61 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ - git - curl +- opencv >=4.12.0 - go 1.25.4 (ранние версии не тестировались) ### Linux & Windows -- 2.49.1 From e5fe816325870b0d525198b8b08551edbe1291bf Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 18:25:06 +0300 Subject: [PATCH 10/25] refactor: project structure, makefile --- Makefile | 10 +- cmd/main.go | 46 ++++ go.mod | 34 +++ go.sum | 74 ++++++ internal/browserlauncher/browserlauncher.go | 75 ++++++ internal/config/config.go | 224 ++++++++++++++++ internal/constants/app.go | 24 ++ internal/linkvalidator/linkvalidator.go | 88 +++++++ internal/logger/logger.go | 66 +++++ internal/screencapturer/interface.go | 27 ++ .../screencapturer/wholescreencapturer.go | 241 ++++++++++++++++++ internal/vision/vision.go | 89 +++++++ internal/vision/visiondata.go | 3 + 13 files changed, 996 insertions(+), 5 deletions(-) create mode 100644 cmd/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/browserlauncher/browserlauncher.go create mode 100644 internal/config/config.go create mode 100644 internal/constants/app.go create mode 100644 internal/linkvalidator/linkvalidator.go create mode 100644 internal/logger/logger.go create mode 100644 internal/screencapturer/interface.go create mode 100644 internal/screencapturer/wholescreencapturer.go create mode 100644 internal/vision/vision.go create mode 100644 internal/vision/visiondata.go diff --git a/Makefile b/Makefile index 7720f00..2d54f1f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ APP_NAME := autoattendance -SRC_DIR := src +SRC_DIR := ./ BUILD_DIR := bin CMD_PATH := ./cmd @@ -28,11 +28,11 @@ $(shell $(MKDIR) $(BUILD_DIR)) all: tidy test build build: - @cd $(SRC_DIR) && $(GOBUILD) -o ../$(BINARY_NAME) $(CMD_PATH) + @cd $(SRC_DIR) && $(GOBUILD) -o $(BINARY_NAME) $(CMD_PATH) @echo "Build complete: $(BINARY_NAME)" run: build - @./$(BINARY_NAME) + @$(BINARY_NAME) test: @cd $(SRC_DIR) && $(GOTEST) ./... @@ -50,8 +50,8 @@ help: build-linux: @$(MKDIR) $(BUILD_DIR) - @cd $(SRC_DIR) && GOOS=linux GOARCH=amd64 $(GOBUILD) -o ../$(BUILD_DIR)/$(APP_NAME)-linux $(CMD_PATH) + @cd $(SRC_DIR) && GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME)-linux $(CMD_PATH) build-windows: @$(MKDIR) $(BUILD_DIR) - @cd $(SRC_DIR) && GOOS=windows GOARCH=amd64 $(GOBUILD) -o ../$(BUILD_DIR)/$(APP_NAME).exe $(CMD_PATH) + @cd $(SRC_DIR) && GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME).exe $(CMD_PATH) diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..75773bb --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,46 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package main + +import ( + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" + "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" + "go.uber.org/fx" +) + +func main() { + app := fx.New( + fx.Provide( + config.NewConfig, + logger.NewLogger, + linkvalidator.NewLinkValidator, + screencapturer.NewWholeScreenCapturer, + vision.NewVision, + ), + fx.Invoke(func(log *logger.Logger, capturer screencapturer.ScreenCapturer) { + log.Debug("starting application...") + }), + ) + + app.Run() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..58b8632 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module git.weirdcat.su/weirdcat/auto-attendance + +go 1.25.4 + +require ( + github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 + github.com/makiuchi-d/gozxing v0.1.1 + github.com/spf13/viper v1.21.0 + go.uber.org/fx v1.24.0 + gocv.io/x/gocv v0.42.0 +) + +require ( + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gen2brain/shm v0.1.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/jezek/xgb v1.1.1 // indirect + github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5d866fb --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY= +github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= +github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= +github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gocv.io/x/gocv v0.42.0 h1:AAsrFJH2aIsQHukkCovWqj0MCGZleQpVyf5gNVRXjQI= +gocv.io/x/gocv v0.42.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/browserlauncher/browserlauncher.go b/internal/browserlauncher/browserlauncher.go new file mode 100644 index 0000000..bc14a7d --- /dev/null +++ b/internal/browserlauncher/browserlauncher.go @@ -0,0 +1,75 @@ +package browserlauncher + +import ( + "fmt" + "os/exec" + "runtime" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" +) + +type BrowserLauncher interface { + OpenDefault(url string) error + OpenCustom(url string) error + OpenAuto(url string) error +} + +type browserLauncherImpl struct { + config *config.Config + log *logger.Logger + useCustomCommand bool + customCommand string +} + +// OpenAuto implements BrowserLauncher. +func (b *browserLauncherImpl) OpenAuto(url string) error { + if (b.useCustomCommand) { + return b.OpenCustom(url) + } else { + return b.OpenDefault(url) + } +} + +// OpenCustom implements BrowserLauncher. +func (b *browserLauncherImpl) OpenCustom(url string) error { + command := fmt.Sprintf(b.customCommand, url) + + b.log.Debug("opening link with custom command", "command", command, "url", url) + err := exec.Command(command).Start() + + if err != nil { + b.log.Error("failed to open link with custom command", "command", command, "url", url, "error", err) + } + + return err +} + +// OpenDefault implements BrowserLauncher. +func (b *browserLauncherImpl) OpenDefault(url string) (err error) { + b.log.Debug("opening link with default browser", "url", url) + switch runtime.GOOS { + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + default: + err = exec.Command("xdg-open", url).Start() + } + + if err != nil { + b.log.Error("failed to open link with default browser", "url", url, "error", err) + } + + return err +} + +func NewBrowserLauncher(config *config.Config, log *logger.Logger) BrowserLauncher { + + useCustomCommand := config.App.UseCustomBrowserCommand + customCommand := config.App.BrowserOpenCommand + return &browserLauncherImpl{ + config: config, + log: log, + useCustomCommand: useCustomCommand, + customCommand: customCommand, + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2cae791 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,224 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package config + +import ( + "fmt" + "os" + "path/filepath" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/constants" + "github.com/spf13/viper" +) + +type Config struct { + App AppConfig `mapstructure:"app"` + Screenshot ScreenshotConfig `mapstructure:"screenshot"` + Communication CommunicationConfig `mapstructure:"communication"` + Telemetry TelemetryConfig `mapstructure:"telemetry"` + Logging LoggingConfig `mapstructure:"logging"` +} + +type AppConfig struct { + SettingsReviewed bool `mapstructure:"settings_reviewed"` + EnableAlarm bool `mapstructure:"enable_alarm"` + EnableLinkOpening bool `mapstructure:"enable_link_opening"` + UseAttendanceJounralApi bool `mapstructure:"use_attendance_journal_api"` + UseCustomBrowserCommand bool `mapstructure:"use_custom_browser_command"` + BrowserOpenCommand string `mapstructure:"browser_open_command"` + EnableCheckingUpdates bool `mapstructure:"enable_checking_updates"` +} + +type ScreenshotConfig struct { + ScreenIndex int `mapstructure:"screen_index"` + Interval int `mapstructure:"interval"` + Directory string `mapstructure:"directory"` + BufferCount int `mapstructure:"buffer_count"` +} + +type CommunicationConfig struct { + QrUrl string `mapstructure:"self_approve_url"` + QrQueryToken string `mapstructure:"qr_query_token"` + ApiSelfApproveMethod string `mapstructure:"api_self_approve_method"` +} + +type TelemetryConfig struct { + EnableStatisticsCollection bool `mapstructure:"enable_anonymous_statistics_collection"` + EnableAnonymousErrorReports bool `mapstructure:"enable_anonymous_error_reports"` +} + +type LoggingConfig struct { + Level string `mapstructure:"level"` + Output string `mapstructure:"output"` +} + +func getTempDirectoryPath() string { + return os.TempDir() +} + +func getDefaultConfig() Config { + return Config{ + App: AppConfig{ + SettingsReviewed: false, + EnableAlarm: false, + EnableLinkOpening: true, + UseAttendanceJounralApi: false, + UseCustomBrowserCommand: false, + BrowserOpenCommand: "firefox %s", + EnableCheckingUpdates: true, + }, + Screenshot: ScreenshotConfig{ + ScreenIndex: 0, + Interval: 5, + Directory: getTempDirectoryPath(), + BufferCount: 5, + }, + Logging: LoggingConfig{ + Level: "info", + Output: "stdout", + }, + Telemetry: TelemetryConfig{ + EnableStatisticsCollection: true, + EnableAnonymousErrorReports: true, + }, + } +} + +func getConfigDir(appName string) (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", fmt.Errorf("failed to get user config directory: %w", err) + } + + appConfigDir := filepath.Join(configDir, appName) + + if err := os.MkdirAll(appConfigDir, 0755); err != nil { + return "", fmt.Errorf("failed to create config directory: %w", err) + } + + return appConfigDir, nil +} + +func initializeViper(appName string) (*viper.Viper, string, error) { + configDir, err := getConfigDir(appName) + if err != nil { + return nil, "", err + } + + configFile := filepath.Join(configDir, appName+".toml") + + v := viper.New() + v.SetConfigFile(configFile) + v.SetConfigType("toml") + + defaults := getDefaultConfig() + + v.SetDefault("app.settings_reviewed", defaults.App.SettingsReviewed) + v.SetDefault("app.enable_alarm", defaults.App.EnableAlarm) + v.SetDefault("app.enable_link_opening", defaults.App.EnableLinkOpening) + v.SetDefault("app.use_attendance_journal_api", defaults.App.UseAttendanceJounralApi) + v.SetDefault("app.use_custom_browser_command", defaults.App.UseCustomBrowserCommand) + v.SetDefault("app.browser_open_command", defaults.App.BrowserOpenCommand) + v.SetDefault("app.enable_checking_updates", defaults.App.EnableCheckingUpdates) + + v.SetDefault("screenshot.screen_index", defaults.Screenshot.ScreenIndex) + v.SetDefault("screenshot.interval", defaults.Screenshot.Interval) + v.SetDefault("screenshot.directory", defaults.Screenshot.Directory) + v.SetDefault("screenshot.buffer_count", defaults.Screenshot.BufferCount) + + v.SetDefault("logging.level", defaults.Logging.Level) + v.SetDefault("logging.output", defaults.Logging.Output) + + v.SetDefault("telemetry.enable_statistics_collection", defaults.Telemetry.EnableStatisticsCollection) + v.SetDefault("telemetry.enable_anonymous_error_reports", defaults.Telemetry.EnableAnonymousErrorReports) + + return v, configFile, nil +} + +func (c *Config) Save() error { + v, _, err := initializeViper(constants.AppName) + if err != nil { + return fmt.Errorf("failed to initialize viper: %w", err) + } + + v.Set("app.settings_reviewed", c.App.SettingsReviewed) + v.Set("app.enable_alarm", c.App.EnableAlarm) + v.Set("app.enable_link_opening", c.App.EnableLinkOpening) + v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi) + v.Set("app.use_custom_browser_command", c.App.UseCustomBrowserCommand) + v.Set("app.browser_open_command", c.App.BrowserOpenCommand) + v.Set("app.enable_checking_updates", c.App.EnableCheckingUpdates) + + v.Set("screenshot.screen_index", c.Screenshot.ScreenIndex) + v.Set("screenshot.interval", c.Screenshot.Interval) + v.Set("screenshot.directory", c.Screenshot.Directory) + v.Set("screenshot.buffer_count", c.Screenshot.BufferCount) + + v.Set("logging.level", c.Logging.Level) + v.Set("logging.output", c.Logging.Output) + + v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection) + v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports) + + if c.Communication.QrUrl != "" { + v.Set("communication.self_approve_url", c.Communication.QrUrl) + } + if c.Communication.QrQueryToken != "" { + v.Set("communication.qr_query_token", c.Communication.QrQueryToken) + } + if c.Communication.ApiSelfApproveMethod != "" { + v.Set("communication.api_self_approve_method", c.Communication.ApiSelfApproveMethod) + } + + return v.WriteConfig() +} + +func NewConfig() (*Config, error) { + v, configFile, err := initializeViper(constants.AppName) + if err != nil { + return nil, fmt.Errorf("failed to initialize viper: %w", err) + } + + if _, err := os.Stat(configFile); os.IsNotExist(err) { + file, err := os.Create(configFile) + if err != nil { + return nil, fmt.Errorf("failed to create config file: %w", err) + } + file.Close() + + if err := v.WriteConfig(); err != nil { + return nil, fmt.Errorf("failed to write default config: %w", err) + } + fmt.Printf("Created new config file at: %s\n", configFile) + } else if err != nil { + return nil, fmt.Errorf("failed to check config file: %w", err) + } + + if err := v.ReadInConfig(); err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + var config Config + if err := v.Unmarshal(&config); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return &config, nil +} diff --git a/internal/constants/app.go b/internal/constants/app.go new file mode 100644 index 0000000..4e6eb7e --- /dev/null +++ b/internal/constants/app.go @@ -0,0 +1,24 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package constants + +const ( + AppName = "auto-attendance" +) diff --git a/internal/linkvalidator/linkvalidator.go b/internal/linkvalidator/linkvalidator.go new file mode 100644 index 0000000..c78e105 --- /dev/null +++ b/internal/linkvalidator/linkvalidator.go @@ -0,0 +1,88 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package linkvalidator + +import ( + "net/url" + "strings" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" +) + +type LinkValidator interface { + ValidateLink(string) (token string, ok bool) +} + +type linkValidatorImpl struct { + config *config.Config + log *logger.Logger +} + +// ValidateLink implements LinkValidator. +func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool) { + if rawURL == "" { + v.log.Debug("Empty URL provided for validation") + return "", false + } + + parsedURL, err := url.Parse(rawURL) + if err != nil { + v.log.Debug("Failed to parse URL", "url", rawURL, "error", err) + return "", false + } + + if v.config.Communication.QrUrl != "" { + expectedURL, err := url.Parse(v.config.Communication.QrUrl) + if err == nil { + if parsedURL.Scheme != expectedURL.Scheme || + parsedURL.Host != expectedURL.Host || + parsedURL.Path != expectedURL.Path { + v.log.Debug("URL doesn't match configured QR URL pattern", + "url", rawURL, "expected", v.config.Communication.QrUrl) + return "", false + } + } + } + + if v.config.Communication.ApiSelfApproveMethod != "" { + if !strings.Contains(parsedURL.Path, v.config.Communication.ApiSelfApproveMethod) { + v.log.Debug("URL doesn't contain expected API method", + "url", parsedURL.Path, "expected", v.config.Communication.ApiSelfApproveMethod) + return "", false + } + } + + token = parsedURL.Query().Get("token") + if token == "" { + v.log.Debug("URL missing token parameter", "url", rawURL) + return "", false + } + + v.log.Debug("URL validation successful", "url", rawURL) + return token, true +} + +func NewLinkValidator(config *config.Config, log *logger.Logger) LinkValidator { + return &linkValidatorImpl{ + config: config, + log: log, + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..3f2b7ea --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,66 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package logger + +import ( + "log/slog" + "os" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" +) + +type Logger struct { + *slog.Logger + *config.Config +} + +func NewLogger(config *config.Config) *Logger { + var level slog.Level + switch config.Logging.Level { + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + level = slog.LevelInfo + } + + var output *os.File + switch config.Logging.Output { + case "stderr": + output = os.Stderr + case "stdout": + output = os.Stdout + default: + output = os.Stdout + } + + handler := slog.NewTextHandler(output, &slog.HandlerOptions{ + Level: level, + }) + + logger := slog.New(handler) + + return &Logger{logger, config} +} diff --git a/internal/screencapturer/interface.go b/internal/screencapturer/interface.go new file mode 100644 index 0000000..6045dcd --- /dev/null +++ b/internal/screencapturer/interface.go @@ -0,0 +1,27 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package screencapturer + +type ScreenCapturer interface { + Init() error + Start() error + Stop() error + Get() (filepath string, err error) +} diff --git a/internal/screencapturer/wholescreencapturer.go b/internal/screencapturer/wholescreencapturer.go new file mode 100644 index 0000000..8b00a09 --- /dev/null +++ b/internal/screencapturer/wholescreencapturer.go @@ -0,0 +1,241 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package screencapturer + +import ( + "context" + "errors" + "fmt" + "image" + "image/png" + "os" + "path/filepath" + "sync" + "time" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/constants" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "github.com/kbinani/screenshot" + "go.uber.org/fx" +) + +var ( + ErrNotInitialized = errors.New("wholescreencapturer was not initialized") +) + +type wholeScreenCapturer struct { + config *config.Config + log *logger.Logger + + displayIndex int + displayBounds image.Rectangle + interval int + bufferCount int + tempDirectory string + initialized bool + running bool + + files []string + latest string + done chan struct{} + sync.WaitGroup + mu sync.RWMutex +} + +// Get implements ScreenCapturer. +func (w *wholeScreenCapturer) Get() (filepath string, err error) { + w.mu.RLock() + defer w.mu.Unlock() + if !w.initialized { + return "", ErrNotInitialized + } + if w.latest == "" { + return "", errors.New("no screenshot captured yet") + } + return w.latest, nil +} + +// Init implements ScreenCapturer. +func (w *wholeScreenCapturer) Init() (err error) { + + displayCount := screenshot.NumActiveDisplays() + w.displayIndex = w.config.Screenshot.ScreenIndex % displayCount + w.log.Debug("detected displays", "count", displayCount, "selected", w.displayIndex) + + w.displayBounds = screenshot.GetDisplayBounds(w.displayIndex) + w.log.Debug( + "display bounds set", + "dx", w.displayBounds.Dx(), + "dy", w.displayBounds.Dy(), + ) + + w.interval = w.config.Screenshot.Interval + w.log.Debug("screenshot interval set", "interval", w.interval) + + w.bufferCount = w.config.Screenshot.BufferCount + w.log.Debug("screenshot buffer count set", "count", w.bufferCount) + + if w.tempDirectory, err = w.createTempDirectory(); err != nil { + w.log.Error("failed to create temporary directory", "error", err) + return err + } + w.log.Debug("temporary directory created", "path", w.tempDirectory) + + w.files = make([]string, 0, w.bufferCount) + w.latest = "" + w.initialized = true + return nil +} + +// Start implements ScreenCapturer. +func (w *wholeScreenCapturer) Start() error { + if !w.initialized { + return ErrNotInitialized + } + if w.running { + w.log.Debug("wholescreencapturer is already running, ignoring start") + return nil + } + w.running = true + w.Go(func() { + w.captureLoop() + }) + w.log.Debug("wholescreencapturer started") + return nil +} + +// Stop implements ScreenCapturer. +func (w *wholeScreenCapturer) Stop() error { + if !w.initialized { + return ErrNotInitialized + } + if !w.running { + w.log.Debug("wholescreencapturer is not running, ignoring stop") + return nil + } + w.log.Debug("stopping wholescreencapturer") + close(w.done) + w.Wait() + w.running = false + w.log.Debug("wholescreencapturer stopped") + return nil +} + +func (w *wholeScreenCapturer) captureLoop() { + ticker := time.NewTicker(time.Duration(w.interval) * time.Second) + defer ticker.Stop() + + for { + select { + case <-w.done: + w.log.Debug("capture loop stopped") + return + case <-ticker.C: + fp, err := w.captureAndSave() + if err != nil { + w.log.Error("screenshot capture failed", "error", err) + continue + } + w.addToBuffer(fp) + } + } +} + +func (w *wholeScreenCapturer) captureAndSave() (string, error) { + img, err := screenshot.CaptureRect(w.displayBounds) + if err != nil { + w.log.Error("failed to capture screenshot", "error", err) + return "", err + } + + now := time.Now().UnixMilli() + filename := fmt.Sprintf("%d.png", now) + filePath := filepath.Join(w.tempDirectory, filename) + + file, err := os.Create(filePath) + if err != nil { + w.log.Error("failed to create screenshot file", "path", filePath, "error", err) + return "", err + } + + defer file.Close() + err = png.Encode(file, img) + if err != nil { + w.log.Error("failed to encode image into file", "path", filePath, "error", err) + return "", err + } + + w.log.Debug("Screenshot saved", "path", filePath) + return filePath, nil +} + +func (w *wholeScreenCapturer) addToBuffer(fp string) { + w.mu.Lock() + defer w.mu.Unlock() + w.files = append(w.files, fp) + w.latest = fp + + if len(w.files) > w.bufferCount { + old := w.files[0] + if err := os.Remove(old); err != nil { + w.log.Warn("failed to remove old screenshot", "path", old, "error", err) + } else { + w.log.Debug("removed old screenshot", "path", old) + } + w.files = w.files[1:] + } +} + +func (w *wholeScreenCapturer) createTempDirectory() (path string, err error) { + return os.MkdirTemp(w.config.Screenshot.Directory, constants.AppName+"-*") +} + +func NewWholeScreenCapturer( + lc fx.Lifecycle, config *config.Config, log *logger.Logger, +) ScreenCapturer { + capturer := &wholeScreenCapturer{ + config: config, + log: log, + done: make(chan struct{}), + } + lc.Append(fx.StopHook(func(ctx context.Context) error { + if !capturer.initialized { + log.Debug("wholescreencapturer not initialized, nothing to do") + return nil + } + + log.Debug("stopping wholescreencapturer") + + err := capturer.Stop() + if err != nil { + log.Error("failed to stop wholescreencapturer gracefully") + return err + } + + err = os.RemoveAll(capturer.tempDirectory) + if err != nil { + log.Error("failed to remove temp directory") + return err + } + return nil + })) + return capturer +} diff --git a/internal/vision/vision.go b/internal/vision/vision.go new file mode 100644 index 0000000..45881eb --- /dev/null +++ b/internal/vision/vision.go @@ -0,0 +1,89 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package vision + +import ( + "errors" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode" + "gocv.io/x/gocv" +) + +type Vision interface { + AnalyzeImage(filePath string) (data VisionData, err error) +} + +type visionImpl struct { + log *logger.Logger +} + +var ( + ErrImageEmpty = errors.New("Image from file was empty") +) + +// AnalyzeImage implements Vision. +func (v *visionImpl) AnalyzeImage(filePath string) (data VisionData, err error) { + // TODO: scanning for multiple QR-codes at once + v.log.Debug("analyzing image for qr codes", "filePath", filePath) + + img := gocv.IMRead(filePath, gocv.IMReadColor) + if img.Empty() { + v.log.Error("could not read image file", "filePath", filePath) + return VisionData{}, ErrImageEmpty + } + defer img.Close() + + // Convert to grayscale for QR code detection + gray := gocv.NewMat() + defer gray.Close() + gocv.CvtColor(img, &gray, gocv.ColorBGRToGray) + + // Convert gocv.Mat to image.Image for gozxing + imgGray, err := gray.ToImage() + if err != nil { + v.log.Error("failed to convert image", "error", err) + return VisionData{}, err + } + + // Create a binary bitmap from the image + bmp, err := gozxing.NewBinaryBitmapFromImage(imgGray) + if err != nil { + v.log.Error("failed to create binary bitmap", "error", err) + return VisionData{}, err + } + + reader := qrcode.NewQRCodeReader() + result, err := reader.Decode(bmp, nil) + if err != nil { + v.log.Debug("no qr code found in image", "filePath", filePath, "error", err) + return VisionData{}, nil + } + + v.log.Info("QR code decoded successfully", "content", result.GetText()) + + data = VisionData{result.GetText()} + return data, nil +} + +func NewVision(log *logger.Logger) Vision { + return &visionImpl{log: log} +} diff --git a/internal/vision/visiondata.go b/internal/vision/visiondata.go new file mode 100644 index 0000000..40f2700 --- /dev/null +++ b/internal/vision/visiondata.go @@ -0,0 +1,3 @@ +package vision + +type VisionData []string -- 2.49.1 From 80b07c627e200cd64f8deb4e50e75327e0af2008 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 18:25:06 +0300 Subject: [PATCH 11/25] refactor: project structure, makefile --- Makefile | 10 +++++----- {src/cmd => cmd}/main.go | 0 src/go.mod => go.mod | 0 src/go.sum => go.sum | 0 .../browserlauncher/browserlauncher.go | 0 {src/internal => internal}/config/config.go | 0 {src/internal => internal}/constants/app.go | 0 .../linkvalidator/linkvalidator.go | 0 {src/internal => internal}/logger/logger.go | 0 {src/internal => internal}/screencapturer/interface.go | 0 .../screencapturer/wholescreencapturer.go | 0 {src/internal => internal}/vision/vision.go | 0 {src/internal => internal}/vision/visiondata.go | 0 13 files changed, 5 insertions(+), 5 deletions(-) rename {src/cmd => cmd}/main.go (100%) rename src/go.mod => go.mod (100%) rename src/go.sum => go.sum (100%) rename {src/internal => internal}/browserlauncher/browserlauncher.go (100%) rename {src/internal => internal}/config/config.go (100%) rename {src/internal => internal}/constants/app.go (100%) rename {src/internal => internal}/linkvalidator/linkvalidator.go (100%) rename {src/internal => internal}/logger/logger.go (100%) rename {src/internal => internal}/screencapturer/interface.go (100%) rename {src/internal => internal}/screencapturer/wholescreencapturer.go (100%) rename {src/internal => internal}/vision/vision.go (100%) rename {src/internal => internal}/vision/visiondata.go (100%) diff --git a/Makefile b/Makefile index 7720f00..2d54f1f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ APP_NAME := autoattendance -SRC_DIR := src +SRC_DIR := ./ BUILD_DIR := bin CMD_PATH := ./cmd @@ -28,11 +28,11 @@ $(shell $(MKDIR) $(BUILD_DIR)) all: tidy test build build: - @cd $(SRC_DIR) && $(GOBUILD) -o ../$(BINARY_NAME) $(CMD_PATH) + @cd $(SRC_DIR) && $(GOBUILD) -o $(BINARY_NAME) $(CMD_PATH) @echo "Build complete: $(BINARY_NAME)" run: build - @./$(BINARY_NAME) + @$(BINARY_NAME) test: @cd $(SRC_DIR) && $(GOTEST) ./... @@ -50,8 +50,8 @@ help: build-linux: @$(MKDIR) $(BUILD_DIR) - @cd $(SRC_DIR) && GOOS=linux GOARCH=amd64 $(GOBUILD) -o ../$(BUILD_DIR)/$(APP_NAME)-linux $(CMD_PATH) + @cd $(SRC_DIR) && GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME)-linux $(CMD_PATH) build-windows: @$(MKDIR) $(BUILD_DIR) - @cd $(SRC_DIR) && GOOS=windows GOARCH=amd64 $(GOBUILD) -o ../$(BUILD_DIR)/$(APP_NAME).exe $(CMD_PATH) + @cd $(SRC_DIR) && GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME).exe $(CMD_PATH) diff --git a/src/cmd/main.go b/cmd/main.go similarity index 100% rename from src/cmd/main.go rename to cmd/main.go diff --git a/src/go.mod b/go.mod similarity index 100% rename from src/go.mod rename to go.mod diff --git a/src/go.sum b/go.sum similarity index 100% rename from src/go.sum rename to go.sum diff --git a/src/internal/browserlauncher/browserlauncher.go b/internal/browserlauncher/browserlauncher.go similarity index 100% rename from src/internal/browserlauncher/browserlauncher.go rename to internal/browserlauncher/browserlauncher.go diff --git a/src/internal/config/config.go b/internal/config/config.go similarity index 100% rename from src/internal/config/config.go rename to internal/config/config.go diff --git a/src/internal/constants/app.go b/internal/constants/app.go similarity index 100% rename from src/internal/constants/app.go rename to internal/constants/app.go diff --git a/src/internal/linkvalidator/linkvalidator.go b/internal/linkvalidator/linkvalidator.go similarity index 100% rename from src/internal/linkvalidator/linkvalidator.go rename to internal/linkvalidator/linkvalidator.go diff --git a/src/internal/logger/logger.go b/internal/logger/logger.go similarity index 100% rename from src/internal/logger/logger.go rename to internal/logger/logger.go diff --git a/src/internal/screencapturer/interface.go b/internal/screencapturer/interface.go similarity index 100% rename from src/internal/screencapturer/interface.go rename to internal/screencapturer/interface.go diff --git a/src/internal/screencapturer/wholescreencapturer.go b/internal/screencapturer/wholescreencapturer.go similarity index 100% rename from src/internal/screencapturer/wholescreencapturer.go rename to internal/screencapturer/wholescreencapturer.go diff --git a/src/internal/vision/vision.go b/internal/vision/vision.go similarity index 100% rename from src/internal/vision/vision.go rename to internal/vision/vision.go diff --git a/src/internal/vision/visiondata.go b/internal/vision/visiondata.go similarity index 100% rename from src/internal/vision/visiondata.go rename to internal/vision/visiondata.go -- 2.49.1 From 638210a26c790e6eba7fa04c1eb8fbec51fa3f86 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 18:40:55 +0300 Subject: [PATCH 12/25] feat: yabloOS --- Makefile | 4 ++++ README.md | 9 ++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 2d54f1f..40bc3fb 100644 --- a/Makefile +++ b/Makefile @@ -55,3 +55,7 @@ build-linux: build-windows: @$(MKDIR) $(BUILD_DIR) @cd $(SRC_DIR) && GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME).exe $(CMD_PATH) + +build-darwin: + @$(MKDIR) $(BUILD_DIR) + @cd $(SRC_DIR) && GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME)-darwin $(CMD_PATH) diff --git a/README.md b/README.md index d08dc61..9792a10 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ - opencv >=4.12.0 - go 1.25.4 (ранние версии не тестировались) -### Linux & Windows +### Linux & Windows & MacOS Склонируйте мой репозиторий: ```bash @@ -80,13 +80,12 @@ make build-linux # для Windows make build-windows + +# для MacOS +make build-darwin ``` Далее для запуска можете использовать команду `make run`. -### Mac - -Я не шарю за этот Ваш мак. Если есть кто-то, кто хочет помочь, - я готов принять pull-request ([пишите в телегу](https://t.me/thebreadcat)). - # Поддержать разработчиков ![Николай (Telegram)](https://t.me/thebreadcat) -- 2.49.1 From f520a0f8012d18bd27797e2ecc6784c5d963777b Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 18:49:33 +0300 Subject: [PATCH 13/25] refactor: gitignore --- .gitignore | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 7ecceae..1858921 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,56 @@ -# Created by https://www.toptal.com/developers/gitignore/api/go,git,vim,goland,visualstudiocode -# Edit at https://www.toptal.com/developers/gitignore?templates=go,git,vim,goland,visualstudiocode +# Created by https://www.toptal.com/developers/gitignore/api/go,git,vim,goland,visualstudiocode,windows,macos,emacs,opencv +# Edit at https://www.toptal.com/developers/gitignore?templates=go,git,vim,goland,visualstudiocode,windows,macos,emacs,opencv + +### Emacs ### +# -*- mode: gitignore; -*- +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Org-mode +.org-id-locations +*_archive + +# flymake-mode +*_flymake.* + +# eshell files +/eshell/history +/eshell/lastdir + +# elpa packages +/elpa/ + +# reftex files +*.rel + +# AUCTeX auto folder +/auto/ + +# cask packages +.cask/ +dist/ + +# Flycheck +flycheck_*.el + +# server auth directory +/server/ + +# projectiles files +.projectile + +# directory configuration +.dir-locals.el + +# network security +/network-security.data + ### Git ### # Created by git for backups. To disable backups in Git: @@ -152,6 +203,47 @@ fabric.properties # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij .idea/**/azureSettings.xml +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### OpenCV ### +#OpenCV for Mac and Linux +#build and release folders +*/CMakeFiles +*/CMakeCache.txt +*/Makefile +*/cmake_install.cmake + ### Vim ### # Swap [._]*.s[a-v][a-z] @@ -167,7 +259,6 @@ Sessionx.vim # Temporary .netrwhist -*~ # Auto-generated tag files tags # Persistent undo @@ -192,6 +283,32 @@ tags .history .ionide -# End of https://www.toptal.com/developers/gitignore/api/go,git,vim,goland,visualstudiocode +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/go,git,vim,goland,visualstudiocode,windows,macos,emacs,opencv bin/ -- 2.49.1 From 26556c53e8e70bb5d01a7e104241b93e8e1d97b9 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Tue, 25 Nov 2025 22:17:43 +0300 Subject: [PATCH 14/25] added gotk to project --- go.mod | 1 + go.sum | 2 ++ internal/ui/handlers.go | 7 +++++++ internal/ui/window.go | 1 + 4 files changed, 11 insertions(+) create mode 100644 internal/ui/handlers.go create mode 100644 internal/ui/window.go diff --git a/go.mod b/go.mod index 58b8632..f130c11 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/gen2brain/shm v0.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gotk3/gotk3 v0.6.4 github.com/jezek/xgb v1.1.1 // indirect github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 5d866fb..5179238 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gotk3/gotk3 v0.6.4 h1:5ur/PRr86PwCG8eSj98D1eXvhrNNK6GILS2zq779dCg= +github.com/gotk3/gotk3 v0.6.4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go new file mode 100644 index 0000000..91836b7 --- /dev/null +++ b/internal/ui/handlers.go @@ -0,0 +1,7 @@ +package ui + +import "github.com/gotk3/gotk3/gtk" + +type Handlers struct { + gtk.AboutDialog +} diff --git a/internal/ui/window.go b/internal/ui/window.go new file mode 100644 index 0000000..5b1faa2 --- /dev/null +++ b/internal/ui/window.go @@ -0,0 +1 @@ +package ui -- 2.49.1 From 3e77a26b0a42aadcbec47f45f0917e6cf41ebdd3 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Wed, 26 Nov 2025 16:28:37 +0300 Subject: [PATCH 15/25] feat: gtk --- Makefile | 3 +- cmd/main.go | 3 +- go.mod | 2 +- go.sum | 2 + internal/constants/app.go | 1 + internal/ui/builder.go | 36 +++ internal/ui/handlers.go | 7 - internal/ui/mainwindow.go | 74 ++++++ internal/ui/resources/qrcode.png | Bin 0 -> 6428 bytes internal/ui/resources/resources.go | 30 +++ internal/ui/resources/window.glade | 367 +++++++++++++++++++++++++++++ internal/ui/window.go | 1 - 12 files changed, 514 insertions(+), 12 deletions(-) create mode 100644 internal/ui/builder.go delete mode 100644 internal/ui/handlers.go create mode 100644 internal/ui/mainwindow.go create mode 100644 internal/ui/resources/qrcode.png create mode 100644 internal/ui/resources/resources.go create mode 100644 internal/ui/resources/window.glade delete mode 100644 internal/ui/window.go diff --git a/Makefile b/Makefile index 40bc3fb..a791eda 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ BUILD_DIR := bin CMD_PATH := ./cmd GOCMD := go -GOBUILD := $(GOCMD) build +GOBUILD := $(GOCMD) build -v GOTEST := $(GOCMD) test GOCLEAN := $(GOCMD) clean GOTIDY := $(GOCMD) mod tidy @@ -42,6 +42,7 @@ tidy: clean: @$(RM) $(BINARY_NAME) + @go clean -modcache @echo "Clean complete" help: diff --git a/cmd/main.go b/cmd/main.go index 75773bb..9452dd5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,8 +37,7 @@ func main() { screencapturer.NewWholeScreenCapturer, vision.NewVision, ), - fx.Invoke(func(log *logger.Logger, capturer screencapturer.ScreenCapturer) { - log.Debug("starting application...") + fx.Invoke(func() { }), ) diff --git a/go.mod b/go.mod index f130c11..abfe5ff 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/gen2brain/shm v0.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gotk3/gotk3 v0.6.4 + github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35 github.com/jezek/xgb v1.1.1 // indirect github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 5179238..a03fcb7 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gotk3/gotk3 v0.6.4 h1:5ur/PRr86PwCG8eSj98D1eXvhrNNK6GILS2zq779dCg= github.com/gotk3/gotk3 v0.6.4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35 h1:BelWQzAzJfSMA1qbuzoV9Tp57+NCvYouEA5cWVpYcSk= +github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= diff --git a/internal/constants/app.go b/internal/constants/app.go index 4e6eb7e..b281f50 100644 --- a/internal/constants/app.go +++ b/internal/constants/app.go @@ -21,4 +21,5 @@ package constants const ( AppName = "auto-attendance" + AppClassGtk = "su.weirdcat.autoattendance" ) diff --git a/internal/ui/builder.go b/internal/ui/builder.go new file mode 100644 index 0000000..6ae52af --- /dev/null +++ b/internal/ui/builder.go @@ -0,0 +1,36 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "github.com/gotk3/gotk3/gtk" +) + +func NewBuilder() (*gtk.Builder, error) { + + gtk.Init(nil) + + builder, err := gtk.BuilderNew() + if err != nil { + return nil, err + } + + return builder, nil +} diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go deleted file mode 100644 index 91836b7..0000000 --- a/internal/ui/handlers.go +++ /dev/null @@ -1,7 +0,0 @@ -package ui - -import "github.com/gotk3/gotk3/gtk" - -type Handlers struct { - gtk.AboutDialog -} diff --git a/internal/ui/mainwindow.go b/internal/ui/mainwindow.go new file mode 100644 index 0000000..a8684b0 --- /dev/null +++ b/internal/ui/mainwindow.go @@ -0,0 +1,74 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "git.weirdcat.su/weirdcat/auto-attendance/internal/ui/resources" + "github.com/gotk3/gotk3/gtk" +) + +type MainWindow struct { + builder *gtk.Builder + window *gtk.Window +} + +func (m *MainWindow) Start() { + m.window.ShowAll() + go func() { gtk.Main() }() +} + +func NewMainWindow(builder *gtk.Builder) (*MainWindow, error) { + err := builder.AddFromString(string(resources.GladeMainWindow)) + if err != nil { + return nil, err + } + + windowObj, err := builder.GetObject("window_main") + if err != nil { + return nil, err + } + window := windowObj.(*gtk.Window) + window.Connect("destroy", func () { + gtk.MainQuit() + }) + + notebookObj, err := builder.GetObject("notebook_main") + if err != nil { + return nil, err + } + notebook := notebookObj.(*gtk.Notebook) + + mainPageObj, err := builder.GetObject("page_main") + if err != nil { + return nil, err + } + mainPage := mainPageObj.(*gtk.Grid) + + settingsPageObj, err := builder.GetObject("page_settings") + if err != nil { + return nil, err + } + settingsPage := settingsPageObj.(*gtk.Box) + + notebook.SetTabLabelText(mainPage, "Overview") + notebook.SetTabLabelText(settingsPage, "Settings") + + return &MainWindow{builder: builder, window: window}, nil +} diff --git a/internal/ui/resources/qrcode.png b/internal/ui/resources/qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..baeb4f1a96be05ed3c0ffc35a212a8e79aa36d41 GIT binary patch literal 6428 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. + +package resources + +import _ "embed" + +var ( + //go:embed window.glade + GladeMainWindow []byte + + //go:embed qrcode.png + QrCodePng []byte +) diff --git a/internal/ui/resources/window.glade b/internal/ui/resources/window.glade new file mode 100644 index 0000000..e50eaf3 --- /dev/null +++ b/internal/ui/resources/window.glade @@ -0,0 +1,367 @@ + + + + + + 100 + 5000 + 500 + 100 + 500 + + + [ERROR] 2025-11-26 14:24:15 - Failed to fetch data from API: Timeout occurred after 30 seconds. +[ERROR] 2025-11-26 14:24:30 - Exception: NullReferenceException in ModuleX: Object reference not set to an instance of an object. + + + + window_main + False + 400 + 600 + zoom-best-fit + + + True + True + + + + App + True + False + True + True + + + True + True + bottom-left + in + + + True + True + 10 + 10 + 10 + 10 + False + word-char + console_output + terminal + True + + + + + 0 + 3 + + + + + + True + False + True + True + + + True + False + vertical + top + + + + + + True + False + vertical + + + + + + False + True + 1 + + + + + + + + 0 + 0 + + + + + 0 + 0 + 3 + + + + + True + False + + + + + Settings + True + False + 20 + 20 + 20 + 20 + vertical + 12 + + + True + False + 0 + none + + + True + False + 12 + 12 + 12 + 12 + vertical + 6 + + + Auto-scan for QR codes + True + True + False + True + True + + + False + True + 0 + + + + + Beep on successful scan + True + True + False + True + True + + + False + True + 1 + + + + + + + True + False + Scanning Options + + + + + + + + False + True + 0 + + + + + True + False + 0 + none + + + True + False + 12 + 12 + 12 + 12 + vertical + 6 + + + True + False + 6 + + + True + False + Scan interval (ms): + 0 + + + False + True + 0 + + + + + True + True + adjustment1 + 1 + True + True + 500 + + + False + True + 1 + + + + + False + True + 0 + + + + + True + False + 6 + + + True + False + Output format: + 0 + + + False + True + 0 + + + + + True + False + 0 + + Text + JSON + XML + URL + + + + False + True + 1 + + + + + False + True + 1 + + + + + + + True + False + Advanced Settings + + + + + + + + False + True + 1 + + + + + Save Settings + True + True + True + end + 12 + + + False + True + 2 + + + + + 1 + True + + + + + + + True + False + Qrminator + Offline + 1 + True + + + True + True + Toggle QR-searching + + + Toggle QR-handling + When enabled, program will search QR-codes on the screen and react to them according to user settings + + + + + + + True + False + True + + + 1 + + + + + + diff --git a/internal/ui/window.go b/internal/ui/window.go deleted file mode 100644 index 5b1faa2..0000000 --- a/internal/ui/window.go +++ /dev/null @@ -1 +0,0 @@ -package ui -- 2.49.1 From a7af6f383ae2fc358b69c6e95ad9f075e0266749 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Wed, 26 Nov 2025 20:39:26 +0300 Subject: [PATCH 16/25] refactor: removed redundant loop mechanism in wholescreencapturer --- internal/screencapturer/interface.go | 2 - .../screencapturer/wholescreencapturer.go | 107 +++++------------- 2 files changed, 26 insertions(+), 83 deletions(-) diff --git a/internal/screencapturer/interface.go b/internal/screencapturer/interface.go index 6045dcd..a0fb940 100644 --- a/internal/screencapturer/interface.go +++ b/internal/screencapturer/interface.go @@ -21,7 +21,5 @@ package screencapturer type ScreenCapturer interface { Init() error - Start() error - Stop() error Get() (filepath string, err error) } diff --git a/internal/screencapturer/wholescreencapturer.go b/internal/screencapturer/wholescreencapturer.go index 8b00a09..2e10a06 100644 --- a/internal/screencapturer/wholescreencapturer.go +++ b/internal/screencapturer/wholescreencapturer.go @@ -47,34 +47,36 @@ type wholeScreenCapturer struct { displayIndex int displayBounds image.Rectangle - interval int bufferCount int tempDirectory string initialized bool - running bool files []string - latest string - done chan struct{} - sync.WaitGroup - mu sync.RWMutex + mu sync.RWMutex } // Get implements ScreenCapturer. func (w *wholeScreenCapturer) Get() (filepath string, err error) { - w.mu.RLock() - defer w.mu.Unlock() - if !w.initialized { - return "", ErrNotInitialized - } - if w.latest == "" { - return "", errors.New("no screenshot captured yet") - } - return w.latest, nil + if !w.initialized { + return "", ErrNotInitialized + } + + fp, err := w.captureAndSave() + if err != nil { + w.log.Error("screenshot capture failed", "error", err) + return "", err + } + + w.addToBuffer(fp) + return fp, nil } // Init implements ScreenCapturer. func (w *wholeScreenCapturer) Init() (err error) { + if w.initialized { + w.log.Debug("wholescreencapturer already initialized, skipping initialization") + return nil + } displayCount := screenshot.NumActiveDisplays() w.displayIndex = w.config.Screenshot.ScreenIndex % displayCount @@ -87,9 +89,6 @@ func (w *wholeScreenCapturer) Init() (err error) { "dy", w.displayBounds.Dy(), ) - w.interval = w.config.Screenshot.Interval - w.log.Debug("screenshot interval set", "interval", w.interval) - w.bufferCount = w.config.Screenshot.BufferCount w.log.Debug("screenshot buffer count set", "count", w.bufferCount) @@ -100,65 +99,10 @@ func (w *wholeScreenCapturer) Init() (err error) { w.log.Debug("temporary directory created", "path", w.tempDirectory) w.files = make([]string, 0, w.bufferCount) - w.latest = "" w.initialized = true return nil } -// Start implements ScreenCapturer. -func (w *wholeScreenCapturer) Start() error { - if !w.initialized { - return ErrNotInitialized - } - if w.running { - w.log.Debug("wholescreencapturer is already running, ignoring start") - return nil - } - w.running = true - w.Go(func() { - w.captureLoop() - }) - w.log.Debug("wholescreencapturer started") - return nil -} - -// Stop implements ScreenCapturer. -func (w *wholeScreenCapturer) Stop() error { - if !w.initialized { - return ErrNotInitialized - } - if !w.running { - w.log.Debug("wholescreencapturer is not running, ignoring stop") - return nil - } - w.log.Debug("stopping wholescreencapturer") - close(w.done) - w.Wait() - w.running = false - w.log.Debug("wholescreencapturer stopped") - return nil -} - -func (w *wholeScreenCapturer) captureLoop() { - ticker := time.NewTicker(time.Duration(w.interval) * time.Second) - defer ticker.Stop() - - for { - select { - case <-w.done: - w.log.Debug("capture loop stopped") - return - case <-ticker.C: - fp, err := w.captureAndSave() - if err != nil { - w.log.Error("screenshot capture failed", "error", err) - continue - } - w.addToBuffer(fp) - } - } -} - func (w *wholeScreenCapturer) captureAndSave() (string, error) { img, err := screenshot.CaptureRect(w.displayBounds) if err != nil { @@ -191,7 +135,6 @@ func (w *wholeScreenCapturer) addToBuffer(fp string) { w.mu.Lock() defer w.mu.Unlock() w.files = append(w.files, fp) - w.latest = fp if len(w.files) > w.bufferCount { old := w.files[0] @@ -214,7 +157,6 @@ func NewWholeScreenCapturer( capturer := &wholeScreenCapturer{ config: config, log: log, - done: make(chan struct{}), } lc.Append(fx.StopHook(func(ctx context.Context) error { if !capturer.initialized { @@ -222,15 +164,18 @@ func NewWholeScreenCapturer( return nil } - log.Debug("stopping wholescreencapturer") + log.Debug("cleaning up wholescreencapturer") - err := capturer.Stop() - if err != nil { - log.Error("failed to stop wholescreencapturer gracefully") - return err + // Clean up all screenshot files + capturer.mu.Lock() + defer capturer.mu.Unlock() + for _, file := range capturer.files { + if err := os.Remove(file); err != nil { + log.Warn("failed to remove screenshot file during cleanup", "path", file, "error", err) + } } - err = os.RemoveAll(capturer.tempDirectory) + err := os.RemoveAll(capturer.tempDirectory) if err != nil { log.Error("failed to remove temp directory") return err -- 2.49.1 From 1e2b717ce80b5d5e441e4cd2b4c9825f0e4100e5 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Wed, 26 Nov 2025 21:12:30 +0300 Subject: [PATCH 17/25] chore: initialized qrminator --- cmd/main.go | 7 ++- internal/qrminator/qrminator.go | 96 +++++++++++++++++++++++++++++++++ internal/qrminator/stats.go | 29 ++++++++++ internal/qrminator/status.go | 31 +++++++++++ 4 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 internal/qrminator/qrminator.go create mode 100644 internal/qrminator/stats.go create mode 100644 internal/qrminator/status.go diff --git a/cmd/main.go b/cmd/main.go index 9452dd5..0ba6756 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -20,9 +20,11 @@ package main import ( + "git.weirdcat.su/weirdcat/auto-attendance/internal/browserlauncher" "git.weirdcat.su/weirdcat/auto-attendance/internal/config" "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "git.weirdcat.su/weirdcat/auto-attendance/internal/qrminator" "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" "go.uber.org/fx" @@ -34,10 +36,13 @@ func main() { config.NewConfig, logger.NewLogger, linkvalidator.NewLinkValidator, + browserlauncher.NewBrowserLauncher, screencapturer.NewWholeScreenCapturer, vision.NewVision, + qrminator.NewQrminator, ), - fx.Invoke(func() { + fx.Invoke(func(qrm qrminator.Qrminator) { + }), ) diff --git a/internal/qrminator/qrminator.go b/internal/qrminator/qrminator.go new file mode 100644 index 0000000..000e21c --- /dev/null +++ b/internal/qrminator/qrminator.go @@ -0,0 +1,96 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package qrminator + +import ( + "sync" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/browserlauncher" + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" + "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" +) + +type Qrminator interface { + Init() error + Start() error + Stop() error + Toggle() error + ConsoleOutput() (string, error) + UpdateConfig(string) error +} + +type qrminatorImpl struct { + config *config.Config + log *logger.Logger + capturer screencapturer.ScreenCapturer + vision vision.Vision + validator linkvalidator.LinkValidator + launcher browserlauncher.BrowserLauncher + + active bool + stats Stats + mu sync.Mutex +} + +// ConsoleOutput implements Qrminator. +func (q *qrminatorImpl) ConsoleOutput() (string, error) { + panic("unimplemented") +} + +// Init implements Qrminator. +func (q *qrminatorImpl) Init() error { + panic("unimplemented") +} + +// Start implements Qrminator. +func (q *qrminatorImpl) Start() error { + panic("unimplemented") +} + +// Stop implements Qrminator. +func (q *qrminatorImpl) Stop() error { + panic("unimplemented") +} + +// Toggle implements Qrminator. +func (q *qrminatorImpl) Toggle() error { + panic("unimplemented") +} + +// UpdateConfig implements Qrminator. +func (q *qrminatorImpl) UpdateConfig(string) error { + panic("unimplemented") +} + +func NewQrminator(cfg *config.Config, log *logger.Logger, capt screencapturer.ScreenCapturer, vis vision.Vision, val linkvalidator.LinkValidator, launch browserlauncher.BrowserLauncher) Qrminator { + return &qrminatorImpl{ + config: cfg, + log: log, + capturer: capt, + vision: vis, + validator: val, + launcher: launch, + active: false, + stats: Stats{}, + } +} diff --git a/internal/qrminator/stats.go b/internal/qrminator/stats.go new file mode 100644 index 0000000..57534a5 --- /dev/null +++ b/internal/qrminator/stats.go @@ -0,0 +1,29 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package qrminator + +import "time" + +type Stats struct { + Status QrminatorStatus + Uptime time.Duration + FoundAmount int + Link string +} diff --git a/internal/qrminator/status.go b/internal/qrminator/status.go new file mode 100644 index 0000000..0f3992c --- /dev/null +++ b/internal/qrminator/status.go @@ -0,0 +1,31 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package qrminator + +type QrminatorStatus int16 + +const ( + Offline QrminatorStatus = iota + Waiting + Screenshotting + Analyzing + Validating + Completed +) -- 2.49.1 From 0fed9a2681bfbf60268a347dfd4560dd7aa5a6bd Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Wed, 26 Nov 2025 21:13:10 +0300 Subject: [PATCH 18/25] chore: lint --- internal/browserlauncher/browserlauncher.go | 8 ++++---- internal/constants/app.go | 2 +- internal/qrminator/qrminator.go | 9 ++++++++- internal/screencapturer/wholescreencapturer.go | 4 ++-- internal/ui/builder.go | 16 +++++++--------- internal/ui/mainwindow.go | 6 +++--- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/internal/browserlauncher/browserlauncher.go b/internal/browserlauncher/browserlauncher.go index bc14a7d..641060f 100644 --- a/internal/browserlauncher/browserlauncher.go +++ b/internal/browserlauncher/browserlauncher.go @@ -24,7 +24,7 @@ type browserLauncherImpl struct { // OpenAuto implements BrowserLauncher. func (b *browserLauncherImpl) OpenAuto(url string) error { - if (b.useCustomCommand) { + if b.useCustomCommand { return b.OpenCustom(url) } else { return b.OpenDefault(url) @@ -67,9 +67,9 @@ func NewBrowserLauncher(config *config.Config, log *logger.Logger) BrowserLaunch useCustomCommand := config.App.UseCustomBrowserCommand customCommand := config.App.BrowserOpenCommand return &browserLauncherImpl{ - config: config, - log: log, + config: config, + log: log, useCustomCommand: useCustomCommand, - customCommand: customCommand, + customCommand: customCommand, } } diff --git a/internal/constants/app.go b/internal/constants/app.go index b281f50..d41119d 100644 --- a/internal/constants/app.go +++ b/internal/constants/app.go @@ -20,6 +20,6 @@ package constants const ( - AppName = "auto-attendance" + AppName = "auto-attendance" AppClassGtk = "su.weirdcat.autoattendance" ) diff --git a/internal/qrminator/qrminator.go b/internal/qrminator/qrminator.go index 000e21c..da2cbff 100644 --- a/internal/qrminator/qrminator.go +++ b/internal/qrminator/qrminator.go @@ -82,7 +82,14 @@ func (q *qrminatorImpl) UpdateConfig(string) error { panic("unimplemented") } -func NewQrminator(cfg *config.Config, log *logger.Logger, capt screencapturer.ScreenCapturer, vis vision.Vision, val linkvalidator.LinkValidator, launch browserlauncher.BrowserLauncher) Qrminator { +func NewQrminator( + cfg *config.Config, + log *logger.Logger, + capt screencapturer.ScreenCapturer, + vis vision.Vision, + val linkvalidator.LinkValidator, + launch browserlauncher.BrowserLauncher, +) Qrminator { return &qrminatorImpl{ config: cfg, log: log, diff --git a/internal/screencapturer/wholescreencapturer.go b/internal/screencapturer/wholescreencapturer.go index 2e10a06..dbb2398 100644 --- a/internal/screencapturer/wholescreencapturer.go +++ b/internal/screencapturer/wholescreencapturer.go @@ -51,8 +51,8 @@ type wholeScreenCapturer struct { tempDirectory string initialized bool - files []string - mu sync.RWMutex + files []string + mu sync.RWMutex } // Get implements ScreenCapturer. diff --git a/internal/ui/builder.go b/internal/ui/builder.go index 6ae52af..47a1dbe 100644 --- a/internal/ui/builder.go +++ b/internal/ui/builder.go @@ -19,18 +19,16 @@ package ui -import ( - "github.com/gotk3/gotk3/gtk" -) +import "github.com/gotk3/gotk3/gtk" func NewBuilder() (*gtk.Builder, error) { gtk.Init(nil) - builder, err := gtk.BuilderNew() - if err != nil { - return nil, err - } - - return builder, nil + builder, err := gtk.BuilderNew() + if err != nil { + return nil, err + } + + return builder, nil } diff --git a/internal/ui/mainwindow.go b/internal/ui/mainwindow.go index a8684b0..c10a021 100644 --- a/internal/ui/mainwindow.go +++ b/internal/ui/mainwindow.go @@ -26,12 +26,12 @@ import ( type MainWindow struct { builder *gtk.Builder - window *gtk.Window + window *gtk.Window } func (m *MainWindow) Start() { m.window.ShowAll() - go func() { gtk.Main() }() + go func() { gtk.Main() }() } func NewMainWindow(builder *gtk.Builder) (*MainWindow, error) { @@ -45,7 +45,7 @@ func NewMainWindow(builder *gtk.Builder) (*MainWindow, error) { return nil, err } window := windowObj.(*gtk.Window) - window.Connect("destroy", func () { + window.Connect("destroy", func() { gtk.MainQuit() }) -- 2.49.1 From f08d72830412f6bc0a7f2b17e65c3b96d9297f00 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 27 Nov 2025 11:21:43 +0300 Subject: [PATCH 19/25] chore: gtk removed --- go.mod | 1 - go.sum | 4 - internal/ui/builder.go | 34 --- internal/ui/mainwindow.go | 74 ------ internal/ui/resources/qrcode.png | Bin 6428 -> 0 bytes internal/ui/resources/resources.go | 30 --- internal/ui/resources/window.glade | 367 ----------------------------- 7 files changed, 510 deletions(-) delete mode 100644 internal/ui/builder.go delete mode 100644 internal/ui/mainwindow.go delete mode 100644 internal/ui/resources/qrcode.png delete mode 100644 internal/ui/resources/resources.go delete mode 100644 internal/ui/resources/window.glade diff --git a/go.mod b/go.mod index abfe5ff..58b8632 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/gen2brain/shm v0.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35 github.com/jezek/xgb v1.1.1 // indirect github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index a03fcb7..5d866fb 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,6 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/gotk3/gotk3 v0.6.4 h1:5ur/PRr86PwCG8eSj98D1eXvhrNNK6GILS2zq779dCg= -github.com/gotk3/gotk3 v0.6.4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= -github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35 h1:BelWQzAzJfSMA1qbuzoV9Tp57+NCvYouEA5cWVpYcSk= -github.com/gotk3/gotk3 v0.6.5-0.20251124190141-e7a9e823ca35/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4= diff --git a/internal/ui/builder.go b/internal/ui/builder.go deleted file mode 100644 index 47a1dbe..0000000 --- a/internal/ui/builder.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2025 Nikolai Papin -// -// This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your -// browser. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -// the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package ui - -import "github.com/gotk3/gotk3/gtk" - -func NewBuilder() (*gtk.Builder, error) { - - gtk.Init(nil) - - builder, err := gtk.BuilderNew() - if err != nil { - return nil, err - } - - return builder, nil -} diff --git a/internal/ui/mainwindow.go b/internal/ui/mainwindow.go deleted file mode 100644 index c10a021..0000000 --- a/internal/ui/mainwindow.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2025 Nikolai Papin -// -// This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your -// browser. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -// the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package ui - -import ( - "git.weirdcat.su/weirdcat/auto-attendance/internal/ui/resources" - "github.com/gotk3/gotk3/gtk" -) - -type MainWindow struct { - builder *gtk.Builder - window *gtk.Window -} - -func (m *MainWindow) Start() { - m.window.ShowAll() - go func() { gtk.Main() }() -} - -func NewMainWindow(builder *gtk.Builder) (*MainWindow, error) { - err := builder.AddFromString(string(resources.GladeMainWindow)) - if err != nil { - return nil, err - } - - windowObj, err := builder.GetObject("window_main") - if err != nil { - return nil, err - } - window := windowObj.(*gtk.Window) - window.Connect("destroy", func() { - gtk.MainQuit() - }) - - notebookObj, err := builder.GetObject("notebook_main") - if err != nil { - return nil, err - } - notebook := notebookObj.(*gtk.Notebook) - - mainPageObj, err := builder.GetObject("page_main") - if err != nil { - return nil, err - } - mainPage := mainPageObj.(*gtk.Grid) - - settingsPageObj, err := builder.GetObject("page_settings") - if err != nil { - return nil, err - } - settingsPage := settingsPageObj.(*gtk.Box) - - notebook.SetTabLabelText(mainPage, "Overview") - notebook.SetTabLabelText(settingsPage, "Settings") - - return &MainWindow{builder: builder, window: window}, nil -} diff --git a/internal/ui/resources/qrcode.png b/internal/ui/resources/qrcode.png deleted file mode 100644 index baeb4f1a96be05ed3c0ffc35a212a8e79aa36d41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6428 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. - -package resources - -import _ "embed" - -var ( - //go:embed window.glade - GladeMainWindow []byte - - //go:embed qrcode.png - QrCodePng []byte -) diff --git a/internal/ui/resources/window.glade b/internal/ui/resources/window.glade deleted file mode 100644 index e50eaf3..0000000 --- a/internal/ui/resources/window.glade +++ /dev/null @@ -1,367 +0,0 @@ - - - - - - 100 - 5000 - 500 - 100 - 500 - - - [ERROR] 2025-11-26 14:24:15 - Failed to fetch data from API: Timeout occurred after 30 seconds. -[ERROR] 2025-11-26 14:24:30 - Exception: NullReferenceException in ModuleX: Object reference not set to an instance of an object. - - - - window_main - False - 400 - 600 - zoom-best-fit - - - True - True - - - - App - True - False - True - True - - - True - True - bottom-left - in - - - True - True - 10 - 10 - 10 - 10 - False - word-char - console_output - terminal - True - - - - - 0 - 3 - - - - - - True - False - True - True - - - True - False - vertical - top - - - - - - True - False - vertical - - - - - - False - True - 1 - - - - - - - - 0 - 0 - - - - - 0 - 0 - 3 - - - - - True - False - - - - - Settings - True - False - 20 - 20 - 20 - 20 - vertical - 12 - - - True - False - 0 - none - - - True - False - 12 - 12 - 12 - 12 - vertical - 6 - - - Auto-scan for QR codes - True - True - False - True - True - - - False - True - 0 - - - - - Beep on successful scan - True - True - False - True - True - - - False - True - 1 - - - - - - - True - False - Scanning Options - - - - - - - - False - True - 0 - - - - - True - False - 0 - none - - - True - False - 12 - 12 - 12 - 12 - vertical - 6 - - - True - False - 6 - - - True - False - Scan interval (ms): - 0 - - - False - True - 0 - - - - - True - True - adjustment1 - 1 - True - True - 500 - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - 6 - - - True - False - Output format: - 0 - - - False - True - 0 - - - - - True - False - 0 - - Text - JSON - XML - URL - - - - False - True - 1 - - - - - False - True - 1 - - - - - - - True - False - Advanced Settings - - - - - - - - False - True - 1 - - - - - Save Settings - True - True - True - end - 12 - - - False - True - 2 - - - - - 1 - True - - - - - - - True - False - Qrminator - Offline - 1 - True - - - True - True - Toggle QR-searching - - - Toggle QR-handling - When enabled, program will search QR-codes on the screen and react to them according to user settings - - - - - - - True - False - True - - - 1 - - - - - - -- 2.49.1 From f6ef335a53cad565e35c6cf87635530e0bbb1df2 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 27 Nov 2025 12:05:33 +0300 Subject: [PATCH 20/25] refactor: renamed qrminator --- cmd/main.go | 6 ++-- internal/{qrminator => app}/qrminator.go | 37 ++++++++++++------------ internal/{qrminator => app}/stats.go | 2 +- internal/{qrminator => app}/status.go | 2 +- 4 files changed, 24 insertions(+), 23 deletions(-) rename internal/{qrminator => app}/qrminator.go (77%) rename internal/{qrminator => app}/stats.go (98%) rename internal/{qrminator => app}/status.go (98%) diff --git a/cmd/main.go b/cmd/main.go index 0ba6756..c59ed8c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,7 +24,7 @@ import ( "git.weirdcat.su/weirdcat/auto-attendance/internal/config" "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" - "git.weirdcat.su/weirdcat/auto-attendance/internal/qrminator" + "git.weirdcat.su/weirdcat/auto-attendance/internal/app" "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" "go.uber.org/fx" @@ -39,9 +39,9 @@ func main() { browserlauncher.NewBrowserLauncher, screencapturer.NewWholeScreenCapturer, vision.NewVision, - qrminator.NewQrminator, + app.NewApp, ), - fx.Invoke(func(qrm qrminator.Qrminator) { + fx.Invoke(func(qrm app.App) { }), ) diff --git a/internal/qrminator/qrminator.go b/internal/app/qrminator.go similarity index 77% rename from internal/qrminator/qrminator.go rename to internal/app/qrminator.go index da2cbff..679bf17 100644 --- a/internal/qrminator/qrminator.go +++ b/internal/app/qrminator.go @@ -17,7 +17,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package qrminator +package app import ( "sync" @@ -30,7 +30,7 @@ import ( "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" ) -type Qrminator interface { +type App interface { Init() error Start() error Stop() error @@ -39,7 +39,7 @@ type Qrminator interface { UpdateConfig(string) error } -type qrminatorImpl struct { +type appImpl struct { config *config.Config log *logger.Logger capturer screencapturer.ScreenCapturer @@ -52,45 +52,46 @@ type qrminatorImpl struct { mu sync.Mutex } -// ConsoleOutput implements Qrminator. -func (q *qrminatorImpl) ConsoleOutput() (string, error) { +// ConsoleOutput implements App. +func (a *appImpl) ConsoleOutput() (string, error) { + // TODO: Outputing stdout/logs content panic("unimplemented") } -// Init implements Qrminator. -func (q *qrminatorImpl) Init() error { +// Init implements App. +func (a *appImpl) Init() error { panic("unimplemented") } -// Start implements Qrminator. -func (q *qrminatorImpl) Start() error { +// Start implements App. +func (a *appImpl) Start() error { panic("unimplemented") } -// Stop implements Qrminator. -func (q *qrminatorImpl) Stop() error { +// Stop implements App. +func (a *appImpl) Stop() error { panic("unimplemented") } -// Toggle implements Qrminator. -func (q *qrminatorImpl) Toggle() error { +// Toggle implements App. +func (a *appImpl) Toggle() error { panic("unimplemented") } -// UpdateConfig implements Qrminator. -func (q *qrminatorImpl) UpdateConfig(string) error { +// UpdateConfig implements App. +func (a *appImpl) UpdateConfig(string) error { panic("unimplemented") } -func NewQrminator( +func NewApp( cfg *config.Config, log *logger.Logger, capt screencapturer.ScreenCapturer, vis vision.Vision, val linkvalidator.LinkValidator, launch browserlauncher.BrowserLauncher, -) Qrminator { - return &qrminatorImpl{ +) App { + return &appImpl{ config: cfg, log: log, capturer: capt, diff --git a/internal/qrminator/stats.go b/internal/app/stats.go similarity index 98% rename from internal/qrminator/stats.go rename to internal/app/stats.go index 57534a5..baa62ba 100644 --- a/internal/qrminator/stats.go +++ b/internal/app/stats.go @@ -17,7 +17,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package qrminator +package app import "time" diff --git a/internal/qrminator/status.go b/internal/app/status.go similarity index 98% rename from internal/qrminator/status.go rename to internal/app/status.go index 0f3992c..6def329 100644 --- a/internal/qrminator/status.go +++ b/internal/app/status.go @@ -17,7 +17,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package qrminator +package app type QrminatorStatus int16 -- 2.49.1 From 71c96ab5ac62e4cdab41d6d7bfe4efa6250acdef Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 27 Nov 2025 13:02:31 +0300 Subject: [PATCH 21/25] chore: added missing config defaults --- internal/config/config.go | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index 2cae791..a771cf0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,6 +40,7 @@ type AppConfig struct { SettingsReviewed bool `mapstructure:"settings_reviewed"` EnableAlarm bool `mapstructure:"enable_alarm"` EnableLinkOpening bool `mapstructure:"enable_link_opening"` + LinksToOpenCount int `mapstructure:"links_to_open_count"` UseAttendanceJounralApi bool `mapstructure:"use_attendance_journal_api"` UseCustomBrowserCommand bool `mapstructure:"use_custom_browser_command"` BrowserOpenCommand string `mapstructure:"browser_open_command"` @@ -79,6 +80,7 @@ func getDefaultConfig() Config { SettingsReviewed: false, EnableAlarm: false, EnableLinkOpening: true, + LinksToOpenCount: 5, UseAttendanceJounralApi: false, UseCustomBrowserCommand: false, BrowserOpenCommand: "firefox %s", @@ -86,12 +88,17 @@ func getDefaultConfig() Config { }, Screenshot: ScreenshotConfig{ ScreenIndex: 0, - Interval: 5, + Interval: 2, Directory: getTempDirectoryPath(), BufferCount: 5, }, + Communication: CommunicationConfig{ + QrUrl: "", + QrQueryToken: "", + ApiSelfApproveMethod: "", + }, Logging: LoggingConfig{ - Level: "info", + Level: "debug", Output: "stdout", }, Telemetry: TelemetryConfig{ @@ -133,6 +140,7 @@ func initializeViper(appName string) (*viper.Viper, string, error) { v.SetDefault("app.settings_reviewed", defaults.App.SettingsReviewed) v.SetDefault("app.enable_alarm", defaults.App.EnableAlarm) v.SetDefault("app.enable_link_opening", defaults.App.EnableLinkOpening) + v.SetDefault("app.links_to_open_count", defaults.App.LinksToOpenCount) v.SetDefault("app.use_attendance_journal_api", defaults.App.UseAttendanceJounralApi) v.SetDefault("app.use_custom_browser_command", defaults.App.UseCustomBrowserCommand) v.SetDefault("app.browser_open_command", defaults.App.BrowserOpenCommand) @@ -143,6 +151,11 @@ func initializeViper(appName string) (*viper.Viper, string, error) { v.SetDefault("screenshot.directory", defaults.Screenshot.Directory) v.SetDefault("screenshot.buffer_count", defaults.Screenshot.BufferCount) + // Set empty string defaults for communication parameters + v.SetDefault("communication.self_approve_url", defaults.Communication.QrUrl) + v.SetDefault("communication.qr_query_token", defaults.Communication.QrQueryToken) + v.SetDefault("communication.api_self_approve_method", defaults.Communication.ApiSelfApproveMethod) + v.SetDefault("logging.level", defaults.Logging.Level) v.SetDefault("logging.output", defaults.Logging.Output) @@ -161,6 +174,7 @@ func (c *Config) Save() error { v.Set("app.settings_reviewed", c.App.SettingsReviewed) v.Set("app.enable_alarm", c.App.EnableAlarm) v.Set("app.enable_link_opening", c.App.EnableLinkOpening) + v.Set("app.links_to_open_count", c.App.LinksToOpenCount) v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi) v.Set("app.use_custom_browser_command", c.App.UseCustomBrowserCommand) v.Set("app.browser_open_command", c.App.BrowserOpenCommand) @@ -171,22 +185,17 @@ func (c *Config) Save() error { v.Set("screenshot.directory", c.Screenshot.Directory) v.Set("screenshot.buffer_count", c.Screenshot.BufferCount) + // Always set communication parameters, even if empty + v.Set("communication.self_approve_url", c.Communication.QrUrl) + v.Set("communication.qr_query_token", c.Communication.QrQueryToken) + v.Set("communication.api_self_approve_method", c.Communication.ApiSelfApproveMethod) + v.Set("logging.level", c.Logging.Level) v.Set("logging.output", c.Logging.Output) v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection) v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports) - if c.Communication.QrUrl != "" { - v.Set("communication.self_approve_url", c.Communication.QrUrl) - } - if c.Communication.QrQueryToken != "" { - v.Set("communication.qr_query_token", c.Communication.QrQueryToken) - } - if c.Communication.ApiSelfApproveMethod != "" { - v.Set("communication.api_self_approve_method", c.Communication.ApiSelfApproveMethod) - } - return v.WriteConfig() } -- 2.49.1 From cc73978637d35f535661cd8b2f0652b2df943213 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 27 Nov 2025 13:03:19 +0300 Subject: [PATCH 22/25] chore: removed error output when no qrcode is found --- internal/vision/vision.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/vision/vision.go b/internal/vision/vision.go index 45881eb..f1f2ba1 100644 --- a/internal/vision/vision.go +++ b/internal/vision/vision.go @@ -74,7 +74,7 @@ func (v *visionImpl) AnalyzeImage(filePath string) (data VisionData, err error) reader := qrcode.NewQRCodeReader() result, err := reader.Decode(bmp, nil) if err != nil { - v.log.Debug("no qr code found in image", "filePath", filePath, "error", err) + v.log.Debug("no qr code found in image", "filePath", filePath) return VisionData{}, nil } -- 2.49.1 From cab126c8dec394914e482efafb4725e39bbf9999 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 27 Nov 2025 13:03:58 +0300 Subject: [PATCH 23/25] feat: ignore seen links --- internal/linkvalidator/linkvalidator.go | 44 +++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/internal/linkvalidator/linkvalidator.go b/internal/linkvalidator/linkvalidator.go index c78e105..70db3ac 100644 --- a/internal/linkvalidator/linkvalidator.go +++ b/internal/linkvalidator/linkvalidator.go @@ -22,6 +22,7 @@ package linkvalidator import ( "net/url" "strings" + "sync" "git.weirdcat.su/weirdcat/auto-attendance/internal/config" "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" @@ -29,11 +30,15 @@ import ( type LinkValidator interface { ValidateLink(string) (token string, ok bool) + ResetSeenLinks() + GetSeenCount() int } type linkValidatorImpl struct { - config *config.Config - log *logger.Logger + config *config.Config + log *logger.Logger + seenMu sync.RWMutex + seenLinks map[string]bool } // ValidateLink implements LinkValidator. @@ -76,13 +81,46 @@ func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool) return "", false } - v.log.Debug("URL validation successful", "url", rawURL) + // Check if we've already seen this token + v.seenMu.RLock() + alreadySeen := v.seenLinks[token] + v.seenMu.RUnlock() + + if alreadySeen { + v.log.Debug("URL token already processed, skipping", "token", token, "url", rawURL) + return token, false + } + + // Mark this token as seen + v.seenMu.Lock() + v.seenLinks[token] = true + v.seenMu.Unlock() + + v.log.Debug("URL validation successful", "url", rawURL, "token", token) return token, true } +// ResetSeenLinks clears the cache of seen links +func (v *linkValidatorImpl) ResetSeenLinks() { + v.seenMu.Lock() + defer v.seenMu.Unlock() + + oldCount := len(v.seenLinks) + v.seenLinks = make(map[string]bool) + v.log.Debug("Reset seen links cache", "previous_count", oldCount) +} + +// GetSeenCount returns the number of unique links seen +func (v *linkValidatorImpl) GetSeenCount() int { + v.seenMu.RLock() + defer v.seenMu.RUnlock() + return len(v.seenLinks) +} + func NewLinkValidator(config *config.Config, log *logger.Logger) LinkValidator { return &linkValidatorImpl{ config: config, log: log, + seenLinks: make(map[string]bool), } } -- 2.49.1 From 67ed1ced9c6e2ebd9ff4b7dc6ab4967aebcc7038 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 27 Nov 2025 13:04:50 +0300 Subject: [PATCH 24/25] feat: qrminator main app loop --- cmd/main.go | 7 +- internal/app/app.go | 314 ++++++++++++++++++++++++++++++++++++++ internal/app/qrminator.go | 104 ------------- 3 files changed, 318 insertions(+), 107 deletions(-) create mode 100644 internal/app/app.go delete mode 100644 internal/app/qrminator.go diff --git a/cmd/main.go b/cmd/main.go index c59ed8c..bd8cbdc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -20,11 +20,11 @@ package main import ( + "git.weirdcat.su/weirdcat/auto-attendance/internal/app" "git.weirdcat.su/weirdcat/auto-attendance/internal/browserlauncher" "git.weirdcat.su/weirdcat/auto-attendance/internal/config" "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" - "git.weirdcat.su/weirdcat/auto-attendance/internal/app" "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" "go.uber.org/fx" @@ -41,8 +41,9 @@ func main() { vision.NewVision, app.NewApp, ), - fx.Invoke(func(qrm app.App) { - + fx.Invoke(func(a app.App) { + a.Init() + a.Start() }), ) diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..c447d9a --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,314 @@ +// Copyright (c) 2025 Nikolai Papin +// +// This file is part of the Auto Attendance app that looks for +// self-attend QR-codes during lectures and opens their URLs in your +// browser. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See +// the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package app + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "git.weirdcat.su/weirdcat/auto-attendance/internal/browserlauncher" + "git.weirdcat.su/weirdcat/auto-attendance/internal/config" + "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" + "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" + "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" + "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" + "go.uber.org/fx" +) + +type App interface { + Init() error + Start() error + Stop() error + Toggle() error + ConsoleOutput() (string, error) + UpdateConfig(string) error +} + +type appImpl struct { + config *config.Config + log *logger.Logger + capturer screencapturer.ScreenCapturer + vision vision.Vision + validator linkvalidator.LinkValidator + launcher browserlauncher.BrowserLauncher + + active atomic.Bool + stats Stats + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + startTime time.Time + foundCount int + consoleBuf []string + consoleMu sync.RWMutex +} + +// ConsoleOutput implements App. +func (a *appImpl) ConsoleOutput() (string, error) { + a.consoleMu.RLock() + defer a.consoleMu.RUnlock() + + if len(a.consoleBuf) == 0 { + return "", nil + } + + output := "" + for _, line := range a.consoleBuf { + output += line + "\n" + } + return output, nil +} + +// Init implements App. +func (a *appImpl) Init() error { + // Initialize screen capturer + if err := a.capturer.Init(); err != nil { + a.log.Error("Failed to initialize screen capturer", "error", err) + return err + } + + a.ctx, a.cancel = context.WithCancel(context.Background()) + a.stats.Status = Offline + a.consoleBuf = make([]string, 0, 100) + + a.addConsoleOutput("Application initialized successfully") + return nil +} + +// Start implements App. +func (a *appImpl) Start() error { + a.mu.Lock() + defer a.mu.Unlock() + + if a.active.Load() { + a.log.Warn("Application is already running") + return fmt.Errorf("application is already running") + } + + a.log.Info("Starting application") + a.active.Store(true) + a.startTime = time.Now() + a.stats.Status = Waiting + a.stats.FoundAmount = 0 + a.foundCount = 0 + + a.wg.Add(1) + go a.pollingLoop() + + a.addConsoleOutput("Application started - QR code monitoring active") + a.log.Info("Application started successfully") + return nil +} + +// Stop implements App. +func (a *appImpl) Stop() error { + a.mu.Lock() + defer a.mu.Unlock() + + if !a.active.Load() { + a.log.Warn("Application is not running") + return fmt.Errorf("application is not running") + } + + a.log.Info("Stopping application") + a.active.Store(false) + + if a.cancel != nil { + a.cancel() + } + + a.wg.Wait() + a.stats.Status = Offline + a.stats.Uptime = time.Since(a.startTime) + + a.addConsoleOutput(fmt.Sprintf("Application stopped - Found %d QR codes", a.foundCount)) + a.log.Info("Application stopped successfully", "found_count", a.foundCount, "uptime", a.stats.Uptime) + return nil +} + +// Toggle implements App. +func (a *appImpl) Toggle() error { + if a.active.Load() { + return a.Stop() + } else { + return a.Start() + } +} + +// UpdateConfig implements App. +func (a *appImpl) UpdateConfig(configStr string) error { + panic("not implemented") +} + +func (a *appImpl) pollingLoop() { + defer a.wg.Done() + + interval := time.Duration(a.config.Screenshot.Interval) * time.Second + ticker := time.NewTicker(interval) + defer ticker.Stop() + + a.log.Info("Polling loop started", "interval", interval) + + for { + select { + case <-a.ctx.Done(): + a.log.Debug("Polling loop stopped via context") + return + case <-ticker.C: + if !a.active.Load() { + continue + } + + a.processScreenshot() + + // Check if we've reached the maximum number of links to open + if a.foundCount >= a.config.App.LinksToOpenCount { + a.log.Info("Reached maximum links to open, stopping", "max_links", a.config.App.LinksToOpenCount) + a.Stop() + return + } + } + } +} + +func (a *appImpl) processScreenshot() { + a.stats.Status = Screenshotting + + // Capture screenshot + filePath, err := a.capturer.Get() + if err != nil { + a.log.Error("Failed to capture screenshot", "error", err) + a.addConsoleOutput("Error: Failed to capture screenshot") + a.stats.Status = Waiting + return + } + + a.log.Debug("Screenshot captured", "path", filePath) + + // Analyze image for QR codes + a.stats.Status = Analyzing + visionData, err := a.vision.AnalyzeImage(filePath) + if err != nil { + a.log.Error("Failed to analyze image", "error", err) + a.addConsoleOutput("Error: Failed to analyze screenshot for QR codes") + a.stats.Status = Waiting + return + } + + if len(visionData) == 0 { + a.log.Debug("No QR codes found in screenshot") + a.stats.Status = Waiting + return + } + + a.log.Info("QR codes found in screenshot", "count", len(visionData)) + + // Process each found QR code + for _, data := range visionData { + if !a.active.Load() { + break + } + + a.stats.Status = Validating + a.stats.Link = data + + token, valid := a.validator.ValidateLink(data) + if !valid { + a.log.Debug("QR code content is not a valid link", "data", data) + continue + } + + a.log.Info("Valid QR code found", "token", token) + a.addConsoleOutput(fmt.Sprintf("Valid QR code found: %s", data)) + + // Open the link in browser if enabled + if a.config.App.EnableLinkOpening { + a.stats.Status = Completed + if err := a.launcher.OpenAuto(data); err != nil { + a.log.Error("Failed to open link in browser", "error", err) + a.addConsoleOutput("Error: Failed to open link in browser") + } else { + a.log.Info("Link opened in browser successfully") + a.addConsoleOutput("Link opened in browser successfully") + a.foundCount++ + a.stats.FoundAmount = a.foundCount + } + } else { + a.log.Info("Link opening is disabled in config") + a.addConsoleOutput("Link opening is disabled - would have opened: " + data) + } + + break + } + + a.stats.Status = Waiting + a.stats.Uptime = time.Since(a.startTime) +} + +func (a *appImpl) addConsoleOutput(message string) { + a.consoleMu.Lock() + defer a.consoleMu.Unlock() + + timestamp := time.Now().Format("15:04:05") + formattedMessage := fmt.Sprintf("[%s] %s", timestamp, message) + + if len(a.consoleBuf) >= 100 { + a.consoleBuf = a.consoleBuf[1:] + } + a.consoleBuf = append(a.consoleBuf, formattedMessage) +} + +func NewApp( + lc fx.Lifecycle, + cfg *config.Config, + log *logger.Logger, + capt screencapturer.ScreenCapturer, + vis vision.Vision, + val linkvalidator.LinkValidator, + launch browserlauncher.BrowserLauncher, +) App { + app := &appImpl{ + config: cfg, + log: log, + capturer: capt, + vision: vis, + validator: val, + launcher: launch, + stats: Stats{ + Status: Offline, + Uptime: 0, + FoundAmount: 0, + Link: "", + }, + } + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + log.Debug("stopping qrminator") + return app.Stop() + }, + }) + + return app +} diff --git a/internal/app/qrminator.go b/internal/app/qrminator.go deleted file mode 100644 index 679bf17..0000000 --- a/internal/app/qrminator.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2025 Nikolai Papin -// -// This file is part of the Auto Attendance app that looks for -// self-attend QR-codes during lectures and opens their URLs in your -// browser. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See -// the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package app - -import ( - "sync" - - "git.weirdcat.su/weirdcat/auto-attendance/internal/browserlauncher" - "git.weirdcat.su/weirdcat/auto-attendance/internal/config" - "git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator" - "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" - "git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer" - "git.weirdcat.su/weirdcat/auto-attendance/internal/vision" -) - -type App interface { - Init() error - Start() error - Stop() error - Toggle() error - ConsoleOutput() (string, error) - UpdateConfig(string) error -} - -type appImpl struct { - config *config.Config - log *logger.Logger - capturer screencapturer.ScreenCapturer - vision vision.Vision - validator linkvalidator.LinkValidator - launcher browserlauncher.BrowserLauncher - - active bool - stats Stats - mu sync.Mutex -} - -// ConsoleOutput implements App. -func (a *appImpl) ConsoleOutput() (string, error) { - // TODO: Outputing stdout/logs content - panic("unimplemented") -} - -// Init implements App. -func (a *appImpl) Init() error { - panic("unimplemented") -} - -// Start implements App. -func (a *appImpl) Start() error { - panic("unimplemented") -} - -// Stop implements App. -func (a *appImpl) Stop() error { - panic("unimplemented") -} - -// Toggle implements App. -func (a *appImpl) Toggle() error { - panic("unimplemented") -} - -// UpdateConfig implements App. -func (a *appImpl) UpdateConfig(string) error { - panic("unimplemented") -} - -func NewApp( - cfg *config.Config, - log *logger.Logger, - capt screencapturer.ScreenCapturer, - vis vision.Vision, - val linkvalidator.LinkValidator, - launch browserlauncher.BrowserLauncher, -) App { - return &appImpl{ - config: cfg, - log: log, - capturer: capt, - vision: vis, - validator: val, - launcher: launch, - active: false, - stats: Stats{}, - } -} -- 2.49.1 From c2a01899511d42fcedccace6d7c4938ded0bf5d6 Mon Sep 17 00:00:00 2001 From: Nikolai Papin Date: Thu, 27 Nov 2025 13:08:39 +0300 Subject: [PATCH 25/25] refactor: docs --- README.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9792a10..9666a5b 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,12 @@ ### 🧩 Зависимости - git +- make - curl -- opencv >=4.12.0 -- go 1.25.4 (ранние версии не тестировались) +- opencv 4.12.0 +- go 1.25.4 -### Linux & Windows & MacOS +### Linux & MacOS & Windows Склонируйте мой репозиторий: ```bash @@ -75,14 +76,9 @@ cd auto-attendance # для текущей платформы make -# для Linux -make build-linux - -# для Windows -make build-windows - -# для MacOS -make build-darwin +# make build-linux +# make build-macos +# make build-windows ``` Далее для запуска можете использовать команду `make run`. -- 2.49.1