Merge pull request 'Первая версия (CLI-only)' (#1) from develop into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
125
.gitignore
vendored
125
.gitignore
vendored
@@ -1,5 +1,56 @@
|
|||||||
# Created by https://www.toptal.com/developers/gitignore/api/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
|
# 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 ###
|
### Git ###
|
||||||
# Created by git for backups. To disable backups in 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
|
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||||
.idea/**/azureSettings.xml
|
.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 ###
|
### Vim ###
|
||||||
# Swap
|
# Swap
|
||||||
[._]*.s[a-v][a-z]
|
[._]*.s[a-v][a-z]
|
||||||
@@ -167,7 +259,6 @@ Sessionx.vim
|
|||||||
|
|
||||||
# Temporary
|
# Temporary
|
||||||
.netrwhist
|
.netrwhist
|
||||||
*~
|
|
||||||
# Auto-generated tag files
|
# Auto-generated tag files
|
||||||
tags
|
tags
|
||||||
# Persistent undo
|
# Persistent undo
|
||||||
@@ -192,6 +283,32 @@ tags
|
|||||||
.history
|
.history
|
||||||
.ionide
|
.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/
|
bin/
|
||||||
|
|||||||
18
Makefile
18
Makefile
@@ -1,10 +1,10 @@
|
|||||||
APP_NAME := autoattendance
|
APP_NAME := autoattendance
|
||||||
SRC_DIR := src
|
SRC_DIR := ./
|
||||||
BUILD_DIR := bin
|
BUILD_DIR := bin
|
||||||
CMD_PATH := ./cmd
|
CMD_PATH := ./cmd
|
||||||
|
|
||||||
GOCMD := go
|
GOCMD := go
|
||||||
GOBUILD := $(GOCMD) build
|
GOBUILD := $(GOCMD) build -v
|
||||||
GOTEST := $(GOCMD) test
|
GOTEST := $(GOCMD) test
|
||||||
GOCLEAN := $(GOCMD) clean
|
GOCLEAN := $(GOCMD) clean
|
||||||
GOTIDY := $(GOCMD) mod tidy
|
GOTIDY := $(GOCMD) mod tidy
|
||||||
@@ -28,11 +28,11 @@ $(shell $(MKDIR) $(BUILD_DIR))
|
|||||||
all: tidy test build
|
all: tidy test build
|
||||||
|
|
||||||
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)"
|
@echo "Build complete: $(BINARY_NAME)"
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
@./$(BINARY_NAME)
|
@$(BINARY_NAME)
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@cd $(SRC_DIR) && $(GOTEST) ./...
|
@cd $(SRC_DIR) && $(GOTEST) ./...
|
||||||
@@ -42,6 +42,7 @@ tidy:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
@$(RM) $(BINARY_NAME)
|
@$(RM) $(BINARY_NAME)
|
||||||
|
@go clean -modcache
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@@ -50,11 +51,12 @@ help:
|
|||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
@$(MKDIR) $(BUILD_DIR)
|
@$(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:
|
build-windows:
|
||||||
@$(MKDIR) $(BUILD_DIR)
|
@$(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)
|
||||||
|
|
||||||
build-all: build-linux build-windows
|
build-darwin:
|
||||||
@echo "Cross-compilation complete"
|
@$(MKDIR) $(BUILD_DIR)
|
||||||
|
@cd $(SRC_DIR) && GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BUILD_DIR)/$(APP_NAME)-darwin $(CMD_PATH)
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -54,10 +54,12 @@
|
|||||||
### 🧩 Зависимости
|
### 🧩 Зависимости
|
||||||
|
|
||||||
- git
|
- git
|
||||||
|
- make
|
||||||
- curl
|
- curl
|
||||||
- go 1.25.4 (ранние версии не тестировались)
|
- opencv 4.12.0
|
||||||
|
- go 1.25.4
|
||||||
|
|
||||||
### Linux & Windows
|
### Linux & MacOS & Windows
|
||||||
|
|
||||||
Склонируйте мой репозиторий:
|
Склонируйте мой репозиторий:
|
||||||
```bash
|
```bash
|
||||||
@@ -74,18 +76,12 @@ cd auto-attendance
|
|||||||
# для текущей платформы
|
# для текущей платформы
|
||||||
make
|
make
|
||||||
|
|
||||||
# для Linux
|
# make build-linux
|
||||||
make build-linux
|
# make build-macos
|
||||||
|
# make build-windows
|
||||||
# для Windows
|
|
||||||
make build-windows
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Далее для запуска можете использовать команду `make run`.
|
Далее для запуска можете использовать команду `make run`.
|
||||||
|
|
||||||
### Mac
|
|
||||||
|
|
||||||
Я не шарю за этот Ваш мак. Если есть кто-то, кто хочет помочь, - я готов принять pull-request ([пишите в телегу](https://t.me/thebreadcat)).
|
|
||||||
|
|
||||||
# Поддержать разработчиков
|
# Поддержать разработчиков
|
||||||

|

|
||||||
|
|||||||
@@ -20,9 +20,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
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/config"
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator"
|
"git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator"
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/logger"
|
"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"
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,9 +36,14 @@ func main() {
|
|||||||
config.NewConfig,
|
config.NewConfig,
|
||||||
logger.NewLogger,
|
logger.NewLogger,
|
||||||
linkvalidator.NewLinkValidator,
|
linkvalidator.NewLinkValidator,
|
||||||
|
browserlauncher.NewBrowserLauncher,
|
||||||
|
screencapturer.NewWholeScreenCapturer,
|
||||||
|
vision.NewVision,
|
||||||
|
app.NewApp,
|
||||||
),
|
),
|
||||||
fx.Invoke(func(log *logger.Logger) {
|
fx.Invoke(func(a app.App) {
|
||||||
log.Info("Starting application...");
|
a.Init()
|
||||||
|
a.Start()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2,11 +2,21 @@ module git.weirdcat.su/weirdcat/auto-attendance
|
|||||||
|
|
||||||
go 1.25.4
|
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 (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
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/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/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
@@ -15,10 +25,10 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
go.uber.org/dig v1.19.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/multierr v1.10.0 // indirect
|
||||||
go.uber.org/zap v1.26.0 // indirect
|
go.uber.org/zap v1.26.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
)
|
)
|
||||||
74
go.sum
Normal file
74
go.sum
Normal file
@@ -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=
|
||||||
314
internal/app/app.go
Normal file
314
internal/app/app.go
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2025 Nikolai Papin
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
//
|
//
|
||||||
// This file is part of the Auto Attendance app that looks for
|
// 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.
|
// browser.
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -17,4 +17,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package browserlauncher
|
package app
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
Status QrminatorStatus
|
||||||
|
Uptime time.Duration
|
||||||
|
FoundAmount int
|
||||||
|
Link string
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2025 Nikolai Papin
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
//
|
//
|
||||||
// This file is part of the Auto Attendance app that looks for
|
// 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.
|
// browser.
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -17,4 +17,15 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package browserlauncher
|
package app
|
||||||
|
|
||||||
|
type QrminatorStatus int16
|
||||||
|
|
||||||
|
const (
|
||||||
|
Offline QrminatorStatus = iota
|
||||||
|
Waiting
|
||||||
|
Screenshotting
|
||||||
|
Analyzing
|
||||||
|
Validating
|
||||||
|
Completed
|
||||||
|
)
|
||||||
75
internal/browserlauncher/browserlauncher.go
Normal file
75
internal/browserlauncher/browserlauncher.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
App AppConfig `mapstructure:"app"`
|
App AppConfig `mapstructure:"app"`
|
||||||
|
Screenshot ScreenshotConfig `mapstructure:"screenshot"`
|
||||||
Communication CommunicationConfig `mapstructure:"communication"`
|
Communication CommunicationConfig `mapstructure:"communication"`
|
||||||
Telemetry TelemetryConfig `mapstructure:"telemetry"`
|
Telemetry TelemetryConfig `mapstructure:"telemetry"`
|
||||||
Logging LoggingConfig `mapstructure:"logging"`
|
Logging LoggingConfig `mapstructure:"logging"`
|
||||||
@@ -39,11 +40,20 @@ type AppConfig struct {
|
|||||||
SettingsReviewed bool `mapstructure:"settings_reviewed"`
|
SettingsReviewed bool `mapstructure:"settings_reviewed"`
|
||||||
EnableAlarm bool `mapstructure:"enable_alarm"`
|
EnableAlarm bool `mapstructure:"enable_alarm"`
|
||||||
EnableLinkOpening bool `mapstructure:"enable_link_opening"`
|
EnableLinkOpening bool `mapstructure:"enable_link_opening"`
|
||||||
|
LinksToOpenCount int `mapstructure:"links_to_open_count"`
|
||||||
UseAttendanceJounralApi bool `mapstructure:"use_attendance_journal_api"`
|
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"`
|
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 {
|
type CommunicationConfig struct {
|
||||||
QrUrl string `mapstructure:"self_approve_url"`
|
QrUrl string `mapstructure:"self_approve_url"`
|
||||||
QrQueryToken string `mapstructure:"qr_query_token"`
|
QrQueryToken string `mapstructure:"qr_query_token"`
|
||||||
@@ -60,18 +70,35 @@ type LoggingConfig struct {
|
|||||||
Output string `mapstructure:"output"`
|
Output string `mapstructure:"output"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTempDirectoryPath() string {
|
||||||
|
return os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
func getDefaultConfig() Config {
|
func getDefaultConfig() Config {
|
||||||
return Config{
|
return Config{
|
||||||
App: AppConfig{
|
App: AppConfig{
|
||||||
SettingsReviewed: false,
|
SettingsReviewed: false,
|
||||||
EnableAlarm: false,
|
EnableAlarm: false,
|
||||||
EnableLinkOpening: true,
|
EnableLinkOpening: true,
|
||||||
|
LinksToOpenCount: 5,
|
||||||
UseAttendanceJounralApi: false,
|
UseAttendanceJounralApi: false,
|
||||||
Browser: "firefox",
|
UseCustomBrowserCommand: false,
|
||||||
|
BrowserOpenCommand: "firefox %s",
|
||||||
EnableCheckingUpdates: true,
|
EnableCheckingUpdates: true,
|
||||||
},
|
},
|
||||||
|
Screenshot: ScreenshotConfig{
|
||||||
|
ScreenIndex: 0,
|
||||||
|
Interval: 2,
|
||||||
|
Directory: getTempDirectoryPath(),
|
||||||
|
BufferCount: 5,
|
||||||
|
},
|
||||||
|
Communication: CommunicationConfig{
|
||||||
|
QrUrl: "",
|
||||||
|
QrQueryToken: "",
|
||||||
|
ApiSelfApproveMethod: "",
|
||||||
|
},
|
||||||
Logging: LoggingConfig{
|
Logging: LoggingConfig{
|
||||||
Level: "info",
|
Level: "debug",
|
||||||
Output: "stdout",
|
Output: "stdout",
|
||||||
},
|
},
|
||||||
Telemetry: TelemetryConfig{
|
Telemetry: TelemetryConfig{
|
||||||
@@ -113,10 +140,22 @@ func initializeViper(appName string) (*viper.Viper, string, error) {
|
|||||||
v.SetDefault("app.settings_reviewed", defaults.App.SettingsReviewed)
|
v.SetDefault("app.settings_reviewed", defaults.App.SettingsReviewed)
|
||||||
v.SetDefault("app.enable_alarm", defaults.App.EnableAlarm)
|
v.SetDefault("app.enable_alarm", defaults.App.EnableAlarm)
|
||||||
v.SetDefault("app.enable_link_opening", defaults.App.EnableLinkOpening)
|
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_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("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)
|
||||||
|
|
||||||
|
// 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.level", defaults.Logging.Level)
|
||||||
v.SetDefault("logging.output", defaults.Logging.Output)
|
v.SetDefault("logging.output", defaults.Logging.Output)
|
||||||
|
|
||||||
@@ -127,35 +166,37 @@ func initializeViper(appName string) (*viper.Viper, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Save() error {
|
func (c *Config) Save() error {
|
||||||
v, _, err := initializeViper(constants.AppName)
|
v, _, err := initializeViper(constants.AppName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize viper: %w", err)
|
return fmt.Errorf("failed to initialize viper: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
v.Set("app.settings_reviewed", c.App.SettingsReviewed)
|
v.Set("app.settings_reviewed", c.App.SettingsReviewed)
|
||||||
v.Set("app.enable_alarm", c.App.EnableAlarm)
|
v.Set("app.enable_alarm", c.App.EnableAlarm)
|
||||||
v.Set("app.enable_link_opening", c.App.EnableLinkOpening)
|
v.Set("app.enable_link_opening", c.App.EnableLinkOpening)
|
||||||
v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi)
|
v.Set("app.links_to_open_count", c.App.LinksToOpenCount)
|
||||||
v.Set("app.browser", c.App.Browser)
|
v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi)
|
||||||
v.Set("app.enable_checking_updates", c.App.EnableCheckingUpdates)
|
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("logging.level", c.Logging.Level)
|
v.Set("screenshot.screen_index", c.Screenshot.ScreenIndex)
|
||||||
v.Set("logging.output", c.Logging.Output)
|
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)
|
// Always set communication parameters, even if empty
|
||||||
v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports)
|
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)
|
||||||
|
|
||||||
if c.Communication.QrUrl != "" {
|
v.Set("logging.level", c.Logging.Level)
|
||||||
v.Set("communication.self_approve_url", c.Communication.QrUrl)
|
v.Set("logging.output", c.Logging.Output)
|
||||||
}
|
|
||||||
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()
|
v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection)
|
||||||
|
v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports)
|
||||||
|
|
||||||
|
return v.WriteConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() (*Config, error) {
|
func NewConfig() (*Config, error) {
|
||||||
@@ -20,5 +20,6 @@
|
|||||||
package constants
|
package constants
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AppName = "auto-attendance"
|
AppName = "auto-attendance"
|
||||||
|
AppClassGtk = "su.weirdcat.autoattendance"
|
||||||
)
|
)
|
||||||
@@ -22,6 +22,7 @@ package linkvalidator
|
|||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/config"
|
"git.weirdcat.su/weirdcat/auto-attendance/internal/config"
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/logger"
|
"git.weirdcat.su/weirdcat/auto-attendance/internal/logger"
|
||||||
@@ -29,11 +30,15 @@ import (
|
|||||||
|
|
||||||
type LinkValidator interface {
|
type LinkValidator interface {
|
||||||
ValidateLink(string) (token string, ok bool)
|
ValidateLink(string) (token string, ok bool)
|
||||||
|
ResetSeenLinks()
|
||||||
|
GetSeenCount() int
|
||||||
}
|
}
|
||||||
|
|
||||||
type linkValidatorImpl struct {
|
type linkValidatorImpl struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
log *logger.Logger
|
log *logger.Logger
|
||||||
|
seenMu sync.RWMutex
|
||||||
|
seenLinks map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateLink implements LinkValidator.
|
// ValidateLink implements LinkValidator.
|
||||||
@@ -52,10 +57,10 @@ func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool)
|
|||||||
if v.config.Communication.QrUrl != "" {
|
if v.config.Communication.QrUrl != "" {
|
||||||
expectedURL, err := url.Parse(v.config.Communication.QrUrl)
|
expectedURL, err := url.Parse(v.config.Communication.QrUrl)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if parsedURL.Scheme != expectedURL.Scheme ||
|
if parsedURL.Scheme != expectedURL.Scheme ||
|
||||||
parsedURL.Host != expectedURL.Host ||
|
parsedURL.Host != expectedURL.Host ||
|
||||||
parsedURL.Path != expectedURL.Path {
|
parsedURL.Path != expectedURL.Path {
|
||||||
v.log.Debug("URL doesn't match configured QR URL pattern",
|
v.log.Debug("URL doesn't match configured QR URL pattern",
|
||||||
"url", rawURL, "expected", v.config.Communication.QrUrl)
|
"url", rawURL, "expected", v.config.Communication.QrUrl)
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -64,7 +69,7 @@ func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool)
|
|||||||
|
|
||||||
if v.config.Communication.ApiSelfApproveMethod != "" {
|
if v.config.Communication.ApiSelfApproveMethod != "" {
|
||||||
if !strings.Contains(parsedURL.Path, 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)
|
"url", parsedURL.Path, "expected", v.config.Communication.ApiSelfApproveMethod)
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -76,13 +81,46 @@ func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool)
|
|||||||
return "", false
|
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
|
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 {
|
func NewLinkValidator(config *config.Config, log *logger.Logger) LinkValidator {
|
||||||
return &linkValidatorImpl{
|
return &linkValidatorImpl{
|
||||||
config: config,
|
config: config,
|
||||||
log: log,
|
log: log,
|
||||||
|
seenLinks: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2025 Nikolai Papin
|
// Copyright (c) 2025 Nikolai Papin
|
||||||
//
|
//
|
||||||
// This file is part of the Auto Attendance app that looks for
|
// 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.
|
// browser.
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -17,4 +17,9 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package screenshotter
|
package screencapturer
|
||||||
|
|
||||||
|
type ScreenCapturer interface {
|
||||||
|
Init() error
|
||||||
|
Get() (filepath string, err error)
|
||||||
|
}
|
||||||
186
internal/screencapturer/wholescreencapturer.go
Normal file
186
internal/screencapturer/wholescreencapturer.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
// 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
|
||||||
|
bufferCount int
|
||||||
|
tempDirectory string
|
||||||
|
initialized bool
|
||||||
|
|
||||||
|
files []string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements ScreenCapturer.
|
||||||
|
func (w *wholeScreenCapturer) Get() (filepath string, err error) {
|
||||||
|
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
|
||||||
|
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.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.initialized = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
lc.Append(fx.StopHook(func(ctx context.Context) error {
|
||||||
|
if !capturer.initialized {
|
||||||
|
log.Debug("wholescreencapturer not initialized, nothing to do")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("cleaning up wholescreencapturer")
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("failed to remove temp directory")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
return capturer
|
||||||
|
}
|
||||||
89
internal/vision/vision.go
Normal file
89
internal/vision/vision.go
Normal file
@@ -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 <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)
|
||||||
|
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}
|
||||||
|
}
|
||||||
3
internal/vision/visiondata.go
Normal file
3
internal/vision/visiondata.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package vision
|
||||||
|
|
||||||
|
type VisionData []string
|
||||||
35
src/go.sum
35
src/go.sum
@@ -1,35 +0,0 @@
|
|||||||
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/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/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=
|
|
||||||
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/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/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=
|
|
||||||
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=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package vision
|
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package platform
|
|
||||||
Reference in New Issue
Block a user