Compare commits
15 Commits
f08d728304
...
0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5456ca5120 | |||
| 20b6841043 | |||
| f1d7cf656c | |||
| a76be07298 | |||
| 8e55175ff1 | |||
| ef5671d8ed | |||
| 4dbf40c4a0 | |||
| 050fae6fd2 | |||
| df689a3eb7 | |||
| c2a0189951 | |||
| 67ed1ced9c | |||
| cab126c8de | |||
| cc73978637 | |||
| 71c96ab5ac | |||
| f6ef335a53 |
2
Makefile
2
Makefile
@@ -1,7 +1,7 @@
|
|||||||
APP_NAME := autoattendance
|
APP_NAME := autoattendance
|
||||||
SRC_DIR := ./
|
SRC_DIR := ./
|
||||||
BUILD_DIR := bin
|
BUILD_DIR := bin
|
||||||
CMD_PATH := ./cmd
|
CMD_PATH := ./cmd/
|
||||||
|
|
||||||
GOCMD := go
|
GOCMD := go
|
||||||
GOBUILD := $(GOCMD) build -v
|
GOBUILD := $(GOCMD) build -v
|
||||||
|
|||||||
49
README.md
49
README.md
@@ -19,13 +19,15 @@
|
|||||||
- [x] Обнаружение QR-кодов
|
- [x] Обнаружение QR-кодов
|
||||||
- [x] Вывод ссылки в консоль
|
- [x] Вывод ссылки в консоль
|
||||||
- [x] Автоматический переход по ссылке
|
- [x] Автоматический переход по ссылке
|
||||||
- [x] Звуковой сигнал
|
|
||||||
|
|
||||||
### ⏰ Запланированные
|
### ⏰ Запланированные
|
||||||
|
- [ ] GUI
|
||||||
|
- [ ] Звуковой сигнал
|
||||||
- [ ] Push-уведомления с ntfy
|
- [ ] Push-уведомления с ntfy
|
||||||
|
|
||||||
### 🤔 На рассмотрении
|
### 🤔 На рассмотрении
|
||||||
- [ ] Работа при свёрнутом окне
|
- [ ] Скриншот только области окна браузера
|
||||||
|
- [ ] Работа при свёрнутом окне браузера
|
||||||
- [ ] Отметка через API журнала
|
- [ ] Отметка через API журнала
|
||||||
|
|
||||||
## 📐 Реализация
|
## 📐 Реализация
|
||||||
@@ -38,8 +40,9 @@
|
|||||||
|
|
||||||
Вам потребуется поддерживаемая операционная система из списка ниже. Если галочки нет, то поддержка появится в будущем.
|
Вам потребуется поддерживаемая операционная система из списка ниже. Если галочки нет, то поддержка появится в будущем.
|
||||||
- [x] Linux (Wayland)
|
- [x] Linux (Wayland)
|
||||||
- [ ] Linux (XORG)
|
- [x] Linux (XORG)
|
||||||
- [x] Windows 10/11
|
- [ ] Windows 10/11 (не тестировалось, должно билдиться)
|
||||||
|
- [ ] MacOS (не тестировалось, должно билдиться)
|
||||||
|
|
||||||
### ⬇️ Установка
|
### ⬇️ Установка
|
||||||
|
|
||||||
@@ -47,18 +50,37 @@
|
|||||||
|
|
||||||
### ⚙️ Настройка
|
### ⚙️ Настройка
|
||||||
|
|
||||||
При первом запуске будет необходимо выполнить настройку приложения. Для этого нужно выбрать соответствующий пункт меню и следовать инструкциям на экране. Если Вы получили готовый файл настройки из другого источника, можно передать программе его URL.
|
При первом запуске будет необходимо выполнить настройку приложения. В Linux файл конфигурации располагается здесь:
|
||||||
|
`~/.config/auto-attendance/auto-attendance.toml`. Необходимо получить актуальный файл конфигурации, который будет работать с конкретным журналом посещений
|
||||||
|
|
||||||
## 🔨 Сборка
|
## 🔨 Сборка
|
||||||
|
|
||||||
### 🧩 Зависимости
|
### 🧩 Зависимости
|
||||||
|
|
||||||
- git
|
- git
|
||||||
|
- make
|
||||||
- curl
|
- curl
|
||||||
- opencv >=4.12.0
|
- opencv 4.12.0
|
||||||
- go 1.25.4 (ранние версии не тестировались)
|
- go 1.25.4
|
||||||
|
|
||||||
### Linux & Windows & MacOS
|
### Go
|
||||||
|
|
||||||
|
Используйте следующую команду в терминале:
|
||||||
|
```bash
|
||||||
|
go install git.weirdcat.su/weirdcat/auto-attendance/cmd/qrminator-cli@main
|
||||||
|
```
|
||||||
|
|
||||||
|
Далее можно запускать командой:
|
||||||
|
```bash
|
||||||
|
qrminator-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Убедитесь, что вы добавили `~/.go/bin` в PATH. Например:
|
||||||
|
```bash
|
||||||
|
export PATH=$PATH:~/.go/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Make
|
||||||
|
|
||||||
Склонируйте мой репозиторий:
|
Склонируйте мой репозиторий:
|
||||||
```bash
|
```bash
|
||||||
@@ -75,14 +97,9 @@ cd auto-attendance
|
|||||||
# для текущей платформы
|
# для текущей платформы
|
||||||
make
|
make
|
||||||
|
|
||||||
# для Linux
|
# make build-linux
|
||||||
make build-linux
|
# make build-macos
|
||||||
|
# make build-windows
|
||||||
# для Windows
|
|
||||||
make build-windows
|
|
||||||
|
|
||||||
# для MacOS
|
|
||||||
make build-darwin
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Далее для запуска можете использовать команду `make run`.
|
Далее для запуска можете использовать команду `make run`.
|
||||||
|
|||||||
@@ -20,11 +20,11 @@
|
|||||||
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/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/qrminator"
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer"
|
"git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer"
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/vision"
|
"git.weirdcat.su/weirdcat/auto-attendance/internal/vision"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
@@ -39,10 +39,11 @@ func main() {
|
|||||||
browserlauncher.NewBrowserLauncher,
|
browserlauncher.NewBrowserLauncher,
|
||||||
screencapturer.NewWholeScreenCapturer,
|
screencapturer.NewWholeScreenCapturer,
|
||||||
vision.NewVision,
|
vision.NewVision,
|
||||||
qrminator.NewQrminator,
|
app.NewApp,
|
||||||
),
|
),
|
||||||
fx.Invoke(func(qrm qrminator.Qrminator) {
|
fx.Invoke(func(a app.App) {
|
||||||
|
a.Init()
|
||||||
|
a.Start()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
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 obtain screenshot from capturer", "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
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
// 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 qrminator
|
package app
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
// 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 qrminator
|
package app
|
||||||
|
|
||||||
type QrminatorStatus int16
|
type QrminatorStatus int16
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ 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"`
|
||||||
UseCustomBrowserCommand bool `mapstructure:"use_custom_browser_command"`
|
UseCustomBrowserCommand bool `mapstructure:"use_custom_browser_command"`
|
||||||
BrowserOpenCommand string `mapstructure:"browser_open_command"`
|
BrowserOpenCommand string `mapstructure:"browser_open_command"`
|
||||||
@@ -79,6 +80,7 @@ func getDefaultConfig() Config {
|
|||||||
SettingsReviewed: false,
|
SettingsReviewed: false,
|
||||||
EnableAlarm: false,
|
EnableAlarm: false,
|
||||||
EnableLinkOpening: true,
|
EnableLinkOpening: true,
|
||||||
|
LinksToOpenCount: 5,
|
||||||
UseAttendanceJounralApi: false,
|
UseAttendanceJounralApi: false,
|
||||||
UseCustomBrowserCommand: false,
|
UseCustomBrowserCommand: false,
|
||||||
BrowserOpenCommand: "firefox %s",
|
BrowserOpenCommand: "firefox %s",
|
||||||
@@ -86,12 +88,17 @@ func getDefaultConfig() Config {
|
|||||||
},
|
},
|
||||||
Screenshot: ScreenshotConfig{
|
Screenshot: ScreenshotConfig{
|
||||||
ScreenIndex: 0,
|
ScreenIndex: 0,
|
||||||
Interval: 5,
|
Interval: 2,
|
||||||
Directory: getTempDirectoryPath(),
|
Directory: getTempDirectoryPath(),
|
||||||
BufferCount: 5,
|
BufferCount: 5,
|
||||||
},
|
},
|
||||||
|
Communication: CommunicationConfig{
|
||||||
|
QrUrl: "",
|
||||||
|
QrQueryToken: "",
|
||||||
|
ApiSelfApproveMethod: "",
|
||||||
|
},
|
||||||
Logging: LoggingConfig{
|
Logging: LoggingConfig{
|
||||||
Level: "info",
|
Level: "debug",
|
||||||
Output: "stdout",
|
Output: "stdout",
|
||||||
},
|
},
|
||||||
Telemetry: TelemetryConfig{
|
Telemetry: TelemetryConfig{
|
||||||
@@ -133,6 +140,7 @@ func initializeViper(appName string) (*viper.Viper, string, error) {
|
|||||||
v.SetDefault("app.settings_reviewed", defaults.App.SettingsReviewed)
|
v.SetDefault("app.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.use_custom_browser_command", defaults.App.UseCustomBrowserCommand)
|
v.SetDefault("app.use_custom_browser_command", defaults.App.UseCustomBrowserCommand)
|
||||||
v.SetDefault("app.browser_open_command", defaults.App.BrowserOpenCommand)
|
v.SetDefault("app.browser_open_command", defaults.App.BrowserOpenCommand)
|
||||||
@@ -143,6 +151,11 @@ func initializeViper(appName string) (*viper.Viper, string, error) {
|
|||||||
v.SetDefault("screenshot.directory", defaults.Screenshot.Directory)
|
v.SetDefault("screenshot.directory", defaults.Screenshot.Directory)
|
||||||
v.SetDefault("screenshot.buffer_count", defaults.Screenshot.BufferCount)
|
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)
|
||||||
|
|
||||||
@@ -161,6 +174,7 @@ func (c *Config) Save() error {
|
|||||||
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.links_to_open_count", c.App.LinksToOpenCount)
|
||||||
v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi)
|
v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi)
|
||||||
v.Set("app.use_custom_browser_command", c.App.UseCustomBrowserCommand)
|
v.Set("app.use_custom_browser_command", c.App.UseCustomBrowserCommand)
|
||||||
v.Set("app.browser_open_command", c.App.BrowserOpenCommand)
|
v.Set("app.browser_open_command", c.App.BrowserOpenCommand)
|
||||||
@@ -171,22 +185,17 @@ func (c *Config) Save() error {
|
|||||||
v.Set("screenshot.directory", c.Screenshot.Directory)
|
v.Set("screenshot.directory", c.Screenshot.Directory)
|
||||||
v.Set("screenshot.buffer_count", c.Screenshot.BufferCount)
|
v.Set("screenshot.buffer_count", c.Screenshot.BufferCount)
|
||||||
|
|
||||||
|
// Always set communication parameters, even if empty
|
||||||
|
v.Set("communication.self_approve_url", c.Communication.QrUrl)
|
||||||
|
v.Set("communication.qr_query_token", c.Communication.QrQueryToken)
|
||||||
|
v.Set("communication.api_self_approve_method", c.Communication.ApiSelfApproveMethod)
|
||||||
|
|
||||||
v.Set("logging.level", c.Logging.Level)
|
v.Set("logging.level", c.Logging.Level)
|
||||||
v.Set("logging.output", c.Logging.Output)
|
v.Set("logging.output", c.Logging.Output)
|
||||||
|
|
||||||
v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection)
|
v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection)
|
||||||
v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports)
|
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()
|
return v.WriteConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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,103 +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 qrminator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/browserlauncher"
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/config"
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/linkvalidator"
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/logger"
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/screencapturer"
|
|
||||||
"git.weirdcat.su/weirdcat/auto-attendance/internal/vision"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Qrminator interface {
|
|
||||||
Init() error
|
|
||||||
Start() error
|
|
||||||
Stop() error
|
|
||||||
Toggle() error
|
|
||||||
ConsoleOutput() (string, error)
|
|
||||||
UpdateConfig(string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type qrminatorImpl struct {
|
|
||||||
config *config.Config
|
|
||||||
log *logger.Logger
|
|
||||||
capturer screencapturer.ScreenCapturer
|
|
||||||
vision vision.Vision
|
|
||||||
validator linkvalidator.LinkValidator
|
|
||||||
launcher browserlauncher.BrowserLauncher
|
|
||||||
|
|
||||||
active bool
|
|
||||||
stats Stats
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsoleOutput implements Qrminator.
|
|
||||||
func (q *qrminatorImpl) ConsoleOutput() (string, error) {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init implements Qrminator.
|
|
||||||
func (q *qrminatorImpl) Init() error {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start implements Qrminator.
|
|
||||||
func (q *qrminatorImpl) Start() error {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop implements Qrminator.
|
|
||||||
func (q *qrminatorImpl) Stop() error {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle implements Qrminator.
|
|
||||||
func (q *qrminatorImpl) Toggle() error {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateConfig implements Qrminator.
|
|
||||||
func (q *qrminatorImpl) UpdateConfig(string) error {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewQrminator(
|
|
||||||
cfg *config.Config,
|
|
||||||
log *logger.Logger,
|
|
||||||
capt screencapturer.ScreenCapturer,
|
|
||||||
vis vision.Vision,
|
|
||||||
val linkvalidator.LinkValidator,
|
|
||||||
launch browserlauncher.BrowserLauncher,
|
|
||||||
) Qrminator {
|
|
||||||
return &qrminatorImpl{
|
|
||||||
config: cfg,
|
|
||||||
log: log,
|
|
||||||
capturer: capt,
|
|
||||||
vision: vis,
|
|
||||||
validator: val,
|
|
||||||
launcher: launch,
|
|
||||||
active: false,
|
|
||||||
stats: Stats{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,7 +57,11 @@ type wholeScreenCapturer struct {
|
|||||||
|
|
||||||
// Get implements ScreenCapturer.
|
// Get implements ScreenCapturer.
|
||||||
func (w *wholeScreenCapturer) Get() (filepath string, err error) {
|
func (w *wholeScreenCapturer) Get() (filepath string, err error) {
|
||||||
if !w.initialized {
|
w.mu.RLock()
|
||||||
|
initialized := w.initialized
|
||||||
|
w.mu.RUnlock()
|
||||||
|
|
||||||
|
if !initialized {
|
||||||
return "", ErrNotInitialized
|
return "", ErrNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +77,9 @@ func (w *wholeScreenCapturer) Get() (filepath string, err error) {
|
|||||||
|
|
||||||
// Init implements ScreenCapturer.
|
// Init implements ScreenCapturer.
|
||||||
func (w *wholeScreenCapturer) Init() (err error) {
|
func (w *wholeScreenCapturer) Init() (err error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
if w.initialized {
|
if w.initialized {
|
||||||
w.log.Debug("wholescreencapturer already initialized, skipping initialization")
|
w.log.Debug("wholescreencapturer already initialized, skipping initialization")
|
||||||
return nil
|
return nil
|
||||||
@@ -104,6 +111,7 @@ func (w *wholeScreenCapturer) Init() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *wholeScreenCapturer) captureAndSave() (string, error) {
|
func (w *wholeScreenCapturer) captureAndSave() (string, error) {
|
||||||
|
w.mu.RLock()
|
||||||
img, err := screenshot.CaptureRect(w.displayBounds)
|
img, err := screenshot.CaptureRect(w.displayBounds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.log.Error("failed to capture screenshot", "error", err)
|
w.log.Error("failed to capture screenshot", "error", err)
|
||||||
@@ -113,6 +121,7 @@ func (w *wholeScreenCapturer) captureAndSave() (string, error) {
|
|||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
filename := fmt.Sprintf("%d.png", now)
|
filename := fmt.Sprintf("%d.png", now)
|
||||||
filePath := filepath.Join(w.tempDirectory, filename)
|
filePath := filepath.Join(w.tempDirectory, filename)
|
||||||
|
w.mu.RUnlock()
|
||||||
|
|
||||||
file, err := os.Create(filePath)
|
file, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -159,7 +168,11 @@ func NewWholeScreenCapturer(
|
|||||||
log: log,
|
log: log,
|
||||||
}
|
}
|
||||||
lc.Append(fx.StopHook(func(ctx context.Context) error {
|
lc.Append(fx.StopHook(func(ctx context.Context) error {
|
||||||
if !capturer.initialized {
|
capturer.mu.RLock()
|
||||||
|
initialized := capturer.initialized
|
||||||
|
capturer.mu.RUnlock()
|
||||||
|
|
||||||
|
if !initialized {
|
||||||
log.Debug("wholescreencapturer not initialized, nothing to do")
|
log.Debug("wholescreencapturer not initialized, nothing to do")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func (v *visionImpl) AnalyzeImage(filePath string) (data VisionData, err error)
|
|||||||
reader := qrcode.NewQRCodeReader()
|
reader := qrcode.NewQRCodeReader()
|
||||||
result, err := reader.Decode(bmp, nil)
|
result, err := reader.Decode(bmp, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.log.Debug("no qr code found in image", "filePath", filePath, "error", err)
|
v.log.Debug("no qr code found in image", "filePath", filePath)
|
||||||
return VisionData{}, nil
|
return VisionData{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user