Первая версия (CLI-only) #1

Merged
weirdcat merged 26 commits from develop into main 2025-11-27 13:09:27 +03:00
12 changed files with 0 additions and 991 deletions
Showing only changes of commit 1562998d01 - Show all commits

View File

@@ -1,46 +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 <https://www.gnu.org/licenses/>.
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()
}

View File

@@ -1,34 +0,0 @@
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
)

View File

@@ -1,74 +0,0 @@
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=

View File

@@ -1,75 +0,0 @@
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,
}
}

View File

@@ -1,224 +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 <https://www.gnu.org/licenses/>.
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
}

View File

@@ -1,24 +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 <https://www.gnu.org/licenses/>.
package constants
const (
AppName = "auto-attendance"
)

View File

@@ -1,88 +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 <https://www.gnu.org/licenses/>.
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,
}
}

View File

@@ -1,66 +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 <https://www.gnu.org/licenses/>.
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}
}

View File

@@ -1,27 +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 <https://www.gnu.org/licenses/>.
package screencapturer
type ScreenCapturer interface {
Init() error
Start() error
Stop() error
Get() (filepath string, err error)
}

View File

@@ -1,241 +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 <https://www.gnu.org/licenses/>.
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
}

View File

@@ -1,89 +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 <https://www.gnu.org/licenses/>.
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}
}

View File

@@ -1,3 +0,0 @@
package vision
type VisionData []string