// Copyright (c) 2025 Nikolai Papin // // This file is part of the Auto Attendance app that looks for // self-attend QR-codes during lectures and opens their URLs in your // browser. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package config import ( "fmt" "os" "path/filepath" "git.weirdcat.su/weirdcat/auto-attendance/internal/constants" "github.com/spf13/viper" ) type Config struct { App AppConfig `mapstructure:"app"` Screenshot ScreenshotConfig `mapstructure:"screenshot"` Communication CommunicationConfig `mapstructure:"communication"` Telemetry TelemetryConfig `mapstructure:"telemetry"` Logging LoggingConfig `mapstructure:"logging"` } type AppConfig struct { SettingsReviewed bool `mapstructure:"settings_reviewed"` EnableAlarm bool `mapstructure:"enable_alarm"` EnableLinkOpening bool `mapstructure:"enable_link_opening"` UseAttendanceJounralApi bool `mapstructure:"use_attendance_journal_api"` UseCustomBrowserCommand bool `mapstructure:"use_custom_browser_command"` BrowserOpenCommand string `mapstructure:"browser_open_command"` EnableCheckingUpdates bool `mapstructure:"enable_checking_updates"` } type ScreenshotConfig struct { ScreenIndex int `mapstructure:"screen_index"` Interval int `mapstructure:"interval"` Directory string `mapstructure:"directory"` BufferCount int `mapstructure:"buffer_count"` } type CommunicationConfig struct { QrUrl string `mapstructure:"self_approve_url"` QrQueryToken string `mapstructure:"qr_query_token"` ApiSelfApproveMethod string `mapstructure:"api_self_approve_method"` } type TelemetryConfig struct { EnableStatisticsCollection bool `mapstructure:"enable_anonymous_statistics_collection"` EnableAnonymousErrorReports bool `mapstructure:"enable_anonymous_error_reports"` } type LoggingConfig struct { Level string `mapstructure:"level"` Output string `mapstructure:"output"` } func getTempDirectoryPath() string { return os.TempDir() } func getDefaultConfig() Config { return Config{ App: AppConfig{ SettingsReviewed: false, EnableAlarm: false, EnableLinkOpening: true, UseAttendanceJounralApi: false, UseCustomBrowserCommand: false, BrowserOpenCommand: "firefox %s", EnableCheckingUpdates: true, }, Screenshot: ScreenshotConfig{ ScreenIndex: 0, Interval: 5, Directory: getTempDirectoryPath(), BufferCount: 5, }, Logging: LoggingConfig{ Level: "info", Output: "stdout", }, Telemetry: TelemetryConfig{ EnableStatisticsCollection: true, EnableAnonymousErrorReports: true, }, } } func getConfigDir(appName string) (string, error) { configDir, err := os.UserConfigDir() if err != nil { return "", fmt.Errorf("failed to get user config directory: %w", err) } appConfigDir := filepath.Join(configDir, appName) if err := os.MkdirAll(appConfigDir, 0755); err != nil { return "", fmt.Errorf("failed to create config directory: %w", err) } return appConfigDir, nil } func initializeViper(appName string) (*viper.Viper, string, error) { configDir, err := getConfigDir(appName) if err != nil { return nil, "", err } configFile := filepath.Join(configDir, appName+".toml") v := viper.New() v.SetConfigFile(configFile) v.SetConfigType("toml") defaults := getDefaultConfig() v.SetDefault("app.settings_reviewed", defaults.App.SettingsReviewed) v.SetDefault("app.enable_alarm", defaults.App.EnableAlarm) v.SetDefault("app.enable_link_opening", defaults.App.EnableLinkOpening) v.SetDefault("app.use_attendance_journal_api", defaults.App.UseAttendanceJounralApi) v.SetDefault("app.use_custom_browser_command", defaults.App.UseCustomBrowserCommand) v.SetDefault("app.browser_open_command", defaults.App.BrowserOpenCommand) v.SetDefault("app.enable_checking_updates", defaults.App.EnableCheckingUpdates) v.SetDefault("screenshot.screen_index", defaults.Screenshot.ScreenIndex) v.SetDefault("screenshot.interval", defaults.Screenshot.Interval) v.SetDefault("screenshot.directory", defaults.Screenshot.Directory) v.SetDefault("screenshot.buffer_count", defaults.Screenshot.BufferCount) v.SetDefault("logging.level", defaults.Logging.Level) v.SetDefault("logging.output", defaults.Logging.Output) v.SetDefault("telemetry.enable_statistics_collection", defaults.Telemetry.EnableStatisticsCollection) v.SetDefault("telemetry.enable_anonymous_error_reports", defaults.Telemetry.EnableAnonymousErrorReports) return v, configFile, nil } func (c *Config) Save() error { v, _, err := initializeViper(constants.AppName) if err != nil { return fmt.Errorf("failed to initialize viper: %w", err) } v.Set("app.settings_reviewed", c.App.SettingsReviewed) v.Set("app.enable_alarm", c.App.EnableAlarm) v.Set("app.enable_link_opening", c.App.EnableLinkOpening) v.Set("app.use_attendance_journal_api", c.App.UseAttendanceJounralApi) v.Set("app.use_custom_browser_command", c.App.UseCustomBrowserCommand) v.Set("app.browser_open_command", c.App.BrowserOpenCommand) v.Set("app.enable_checking_updates", c.App.EnableCheckingUpdates) v.Set("screenshot.screen_index", c.Screenshot.ScreenIndex) v.Set("screenshot.interval", c.Screenshot.Interval) v.Set("screenshot.directory", c.Screenshot.Directory) v.Set("screenshot.buffer_count", c.Screenshot.BufferCount) v.Set("logging.level", c.Logging.Level) v.Set("logging.output", c.Logging.Output) v.Set("telemetry.enable_statistics_collection", c.Telemetry.EnableStatisticsCollection) v.Set("telemetry.enable_anonymous_error_reports", c.Telemetry.EnableAnonymousErrorReports) if c.Communication.QrUrl != "" { v.Set("communication.self_approve_url", c.Communication.QrUrl) } if c.Communication.QrQueryToken != "" { v.Set("communication.qr_query_token", c.Communication.QrQueryToken) } if c.Communication.ApiSelfApproveMethod != "" { v.Set("communication.api_self_approve_method", c.Communication.ApiSelfApproveMethod) } return v.WriteConfig() } func NewConfig() (*Config, error) { v, configFile, err := initializeViper(constants.AppName) if err != nil { return nil, fmt.Errorf("failed to initialize viper: %w", err) } if _, err := os.Stat(configFile); os.IsNotExist(err) { file, err := os.Create(configFile) if err != nil { return nil, fmt.Errorf("failed to create config file: %w", err) } file.Close() if err := v.WriteConfig(); err != nil { return nil, fmt.Errorf("failed to write default config: %w", err) } fmt.Printf("Created new config file at: %s\n", configFile) } else if err != nil { return nil, fmt.Errorf("failed to check config file: %w", err) } if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } var config Config if err := v.Unmarshal(&config); err != nil { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } return &config, nil }