diff --git a/cmd/expira/main.go b/cmd/expira/main.go index c1f818e..7731cd1 100644 --- a/cmd/expira/main.go +++ b/cmd/expira/main.go @@ -2,13 +2,10 @@ package main import ( "fmt" - "net/http" + "log" "github.com/charmbracelet/lipgloss" - "github.com/iwa/Expira/internal/api" - "github.com/iwa/Expira/internal/cron" - "github.com/iwa/Expira/internal/state" - "github.com/iwa/Expira/internal/utils" + "github.com/iwa/Expira/internal/app" ) var titleStyle = lipgloss.NewStyle(). @@ -25,23 +22,8 @@ var titleStyle = lipgloss.NewStyle(). func main() { fmt.Println(titleStyle.Render("Domain Expiry Watcher")) - appState := state.GetInstance() - - utils.ImportEnv(appState) - - println("[INFO] Starting domain expiry watcher...") - - utils.UpdateDomains(appState) - - utils.ReportStatusInConsole(appState) - - utils.Notify(appState) - - http.HandleFunc("/health", api.HealthHandler) - http.HandleFunc("/status", api.StatusHandler) - go http.ListenAndServe("0.0.0.0:8080", nil) - - cron.StartCronLoop(appState) - - select {} // Keep the main goroutine running + app := app.New() + if err := app.Start(); err != nil { + log.Fatalf("Application error: %v", err) + } } diff --git a/internal/api/routes.go b/internal/api/routes.go index b45cb7c..676b5dd 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -9,30 +9,34 @@ import ( func HealthHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } fmt.Fprintf(w, "Service is running") } -// GET /status -// Get a status report of all monitored domains and their expiry dates. -func StatusHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - return - } +// StatusHandlerFactory creates a status handler with access to the domain store. +// This allows the handler to use dependency injection instead of global state. +func StatusHandlerFactory(store *state.DomainStore) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } - var status string = "Domain Expiry Watcher Status:\n\n" + var status string = "Domain Expiry Watcher Status:\n\n" - appState := state.GetInstance() + domains := store.GetAllDomains() - for _, domain := range appState.Domains { - if domain.ExpiryDate.IsZero() { - status += fmt.Sprintf("Domain %s: Expiry date not set\n", domain.Name) - } else { - status += fmt.Sprintf("Domain %s: Expires on %s\n", domain.Name, domain.ExpiryDate.Format("2006-01-02")) + for _, domain := range domains { + if domain.ExpiryDate.IsZero() { + status += fmt.Sprintf("Domain %s: Expiry date not set\n", domain.Name) + } else { + status += fmt.Sprintf("Domain %s: Expires on %s\n", domain.Name, domain.ExpiryDate.Format("2006-01-02")) + } } - } - fmt.Fprintf(w, "%s", status) + fmt.Fprintf(w, "%s", status) + } } diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 0000000..c92eb8c --- /dev/null +++ b/internal/api/server.go @@ -0,0 +1,52 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/iwa/Expira/internal/state" +) + +// Server wraps the HTTP server with graceful lifecycle management +type Server struct { + server *http.Server + errors chan error +} + +func NewServer(addr string, store *state.DomainStore) *Server { + mux := http.NewServeMux() + mux.HandleFunc("/health", HealthHandler) + mux.HandleFunc("/status", StatusHandlerFactory(store)) + + return &Server{ + server: &http.Server{ + Addr: addr, + Handler: mux, + }, + errors: make(chan error, 1), + } +} + +// Start begins listening for HTTP requests in a goroutine +func (s *Server) Start() { + go func() { + fmt.Printf("[INFO] HTTP server starting on %s\n", s.server.Addr) + if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + s.errors <- fmt.Errorf("[ERROR] HTTP server error: %w", err) + } + }() +} + +// Errors returns a channel that receives server errors +func (s *Server) Errors() <-chan error { + return s.errors +} + +// Shutdown gracefully stops the server with a timeout +func (s *Server) Shutdown(timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + return s.server.Shutdown(ctx) +} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..acf47a9 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,59 @@ +package app + +import ( + "fmt" + "time" + + "github.com/iwa/Expira/internal/api" + "github.com/iwa/Expira/internal/cron" + "github.com/iwa/Expira/internal/state" + "github.com/iwa/Expira/internal/utils" +) + +// App holds the application's core dependencies +type App struct { + Config *state.Config + Store *state.DomainStore +} + +// New creates and initializes a new App instance +func New() *App { + config, store := utils.LoadConfig() + return &App{ + Config: config, + Store: store, + } +} + +// Start runs the application +func (app *App) Start() error { + // Initial domain update and notification + utils.UpdateDomains(app.Store) + utils.ReportStatusInConsole(app.Store) + utils.Notify(app.Store, app.Config) + + // Start HTTP server + server := api.NewServer("0.0.0.0:8080", app.Store) + server.Start() + + // Start cron job in goroutine + cronErrors := make(chan error, 1) + go func() { + cron.StartCronLoop(app.Store, app.Config) + cronErrors <- fmt.Errorf("cron loop unexpectedly stopped") + }() + + // Block the main go routine with error handling + select { + case err := <-server.Errors(): + if shutdownErr := server.Shutdown(5 * time.Second); shutdownErr != nil { + return fmt.Errorf("server error: %w, shutdown error: %v", err, shutdownErr) + } + return err + case err := <-cronErrors: + if shutdownErr := server.Shutdown(5 * time.Second); shutdownErr != nil { + return fmt.Errorf("cron error: %w, shutdown error: %v", err, shutdownErr) + } + return err + } +} diff --git a/internal/cron/cron.go b/internal/cron/cron.go index 300d57d..5fc78d8 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -7,7 +7,9 @@ import ( "github.com/iwa/Expira/internal/utils" ) -func StartCronLoop(appState *state.AppState) { +// StartCronLoop starts an hourly cron job that runs domain updates at midnight. +// It uses dependency injection to access the domain store and configuration. +func StartCronLoop(store *state.DomainStore, config *state.Config) { println("[INFO] Starting cron job...") ticker := time.NewTicker(time.Hour) @@ -22,11 +24,11 @@ func StartCronLoop(appState *state.AppState) { if checkMidnight(t) { println("[INFO] Daily domains refresh cron job triggered at", t.Format("2006-01-02 15:04:05")) - utils.UpdateDomains(appState) + utils.UpdateDomains(store) - utils.Notify(appState) + utils.Notify(store, config) - utils.ReportStatusInConsole(appState) + utils.ReportStatusInConsole(store) } } } diff --git a/internal/state/AppState.go b/internal/state/AppState.go deleted file mode 100644 index 7b9efc6..0000000 --- a/internal/state/AppState.go +++ /dev/null @@ -1,27 +0,0 @@ -package state - -type AppState struct { - Domains map[string]Domain - NotificationDays []int - - TelegramNotification bool - TelegramChatID string - TelegramToken string - - DiscordNotification bool - DiscordWebhookURL string - - NtfyNotification bool - NtfyURL string -} - -var instance AppState - -// Initialize the singleton instance when the package is loaded -func init() { - instance = AppState{} -} - -func GetInstance() *AppState { - return &instance -} diff --git a/internal/state/Config.go b/internal/state/Config.go new file mode 100644 index 0000000..c874163 --- /dev/null +++ b/internal/state/Config.go @@ -0,0 +1,24 @@ +package state + +// Config holds configuration loaded from environment variables at startup. +// This configuration should never change during the application lifecycle. +type Config struct { + NotificationDays []int + + TelegramNotification bool + TelegramChatID string + TelegramToken string + + DiscordNotification bool + DiscordWebhookURL string + + NtfyNotification bool + NtfyURL string +} + +// NewConfig creates a new Config instance with default values +func NewConfig() *Config { + return &Config{ + NotificationDays: []int{}, + } +} diff --git a/internal/state/DomainStore.go b/internal/state/DomainStore.go new file mode 100644 index 0000000..9ff0300 --- /dev/null +++ b/internal/state/DomainStore.go @@ -0,0 +1,66 @@ +package state + +import ( + "maps" + "sync" +) + +type DomainStore struct { + mu sync.RWMutex + domains map[string]Domain +} + +func NewDomainStore() *DomainStore { + return &DomainStore{ + domains: make(map[string]Domain), + } +} + +// Returns the domain and true if found, or an empty domain and false if not found. +func (ds *DomainStore) GetDomain(name string) (Domain, bool) { + ds.mu.RLock() + defer ds.mu.RUnlock() + + domain, ok := ds.domains[name] + return domain, ok +} + +// This method is thread-safe and can be called from multiple goroutines. +func (ds *DomainStore) SetDomain(name string, domain Domain) { + ds.mu.Lock() + defer ds.mu.Unlock() + + ds.domains[name] = domain +} + +// GetAllDomains returns a copy of all domains. +// This ensures the returned map cannot cause race conditions if modified by the caller. +func (ds *DomainStore) GetAllDomains() map[string]Domain { + ds.mu.RLock() + defer ds.mu.RUnlock() + + // Create a copy to avoid exposing internal map + domainsCopy := make(map[string]Domain, len(ds.domains)) + maps.Copy(domainsCopy, ds.domains) + + return domainsCopy +} + +// SetBulkDomains replaces all domains with the provided map. +func (ds *DomainStore) SetBulkDomains(domains map[string]Domain) { + ds.mu.Lock() + defer ds.mu.Unlock() + + domainsCopy := make(map[string]Domain, len(domains)) + maps.Copy(domainsCopy, domains) + + ds.domains = domainsCopy +} + +// Count returns the number of domains in the store +func (ds *DomainStore) Count() int { + ds.mu.RLock() + defer ds.mu.RUnlock() + + return len(ds.domains) +} diff --git a/internal/utils/cli_report.go b/internal/utils/cli_report.go index 7580fcc..10674bd 100644 --- a/internal/utils/cli_report.go +++ b/internal/utils/cli_report.go @@ -6,14 +6,17 @@ import ( "github.com/iwa/Expira/internal/state" ) -func ReportStatusInConsole(appState *state.AppState) { +// ReportStatusInConsole displays the current status of all domains in the console. +// It uses the provided store to read domain data. +func ReportStatusInConsole(store *state.DomainStore) { println("[INFO] Generating domains report...") println("\n --- Current Domains Status ---") currentTime := time.Now() - for domain, domainData := range appState.Domains { + domains := store.GetAllDomains() + for domain, domainData := range domains { daysLeft := int(domainData.ExpiryDate.Sub(currentTime).Hours()/24) + 1 println("Domain:", domain, "- In", daysLeft, "Days - Expiry date:", domainData.ExpiryDate.Format("2006-01-02 15:04:05")) } diff --git a/internal/utils/env_import.go b/internal/utils/env_import.go index e8a10a5..af6751f 100644 --- a/internal/utils/env_import.go +++ b/internal/utils/env_import.go @@ -13,7 +13,12 @@ import ( "github.com/iwa/Expira/internal/state" ) -func ImportEnv(appState *state.AppState) { +// LoadConfig loads configuration from environment variables. +// Returns a Config instance and a DomainStore initialized with domains from env. +func LoadConfig() (*state.Config, *state.DomainStore) { + config := state.NewConfig() + store := state.NewDomainStore() + t := table.New(). Border(lipgloss.RoundedBorder()). BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4"))). @@ -27,16 +32,18 @@ func ImportEnv(appState *state.AppState) { }). Headers(" Configuration from environment variables ") - t.Row(importDomains(appState)) - t.Row(importNotificationDaysConfig(appState)) - importTelegramConfig(appState) - importDiscordConfig(appState) - importNtfyConfig(appState) + t.Row(importDomains(store)) + t.Row(importNotificationDaysConfig(config)) + importTelegramConfig(config) + importDiscordConfig(config) + importNtfyConfig(config) fmt.Println(t) + + return config, store } -func importDomains(appState *state.AppState) string { +func importDomains(store *state.DomainStore) string { log := "" domainsEnv := os.Getenv("DOMAINS") @@ -49,37 +56,39 @@ func importDomains(appState *state.AppState) string { domains[i] = strings.TrimSpace(domains[i]) } - appState.Domains = make(map[string]state.Domain, len(domains)) + domainMap := make(map[string]state.Domain, len(domains)) for _, domain := range domains { if domain == "" { log = fmt.Sprintln(log, "[WARN] Empty domain found in the DOMAINS environment variable, skipping.") continue } - appState.Domains[domain] = state.Domain{ + domainMap[domain] = state.Domain{ Name: domain, ExpiryDate: time.Unix(0, 0), // Default expiry date } } - if len(appState.Domains) == 0 { + if len(domainMap) == 0 { panic("[ERROR] No valid domains found in the DOMAINS environment variable.") } - log = fmt.Sprintln(log, "Imported domains:", len(appState.Domains)) + store.SetBulkDomains(domainMap) + + log = fmt.Sprintln(log, "Imported domains:", len(domainMap)) return log } -func importNotificationDaysConfig(appState *state.AppState) string { +func importNotificationDaysConfig(config *state.Config) string { log := "" daysEnv := os.Getenv("NOTIFICATION_DAYS") if daysEnv == "" { - appState.NotificationDays = []int{30, 15, 7, 1} // Default values + config.NotificationDays = []int{30, 15, 7, 1} // Default values log = fmt.Sprintln(log, "No NOTIFICATION_DAYS environment variable found, using default values...") - log = fmt.Sprintln(log, fmt.Sprint("Notification will be sent this many days before expiry: ", appState.NotificationDays)) + log = fmt.Sprintln(log, fmt.Sprint("Notification will be sent this many days before expiry: ", config.NotificationDays)) } else { daysStr := strings.Split(daysEnv, ",") @@ -87,7 +96,7 @@ func importNotificationDaysConfig(appState *state.AppState) string { panic("[ERROR] No valid days found in NOTIFICATION_DAYS environment variable.") } - appState.NotificationDays = make([]int, 0, len(daysStr)) + config.NotificationDays = make([]int, 0, len(daysStr)) for _, day := range daysStr { value, err := strconv.Atoi(strings.TrimSpace(day)) @@ -101,62 +110,62 @@ func importNotificationDaysConfig(appState *state.AppState) string { // Check for duplicates in the slice alreadyExists := false - for j := range len(appState.NotificationDays) { - if appState.NotificationDays[j] == value { + for j := range len(config.NotificationDays) { + if config.NotificationDays[j] == value { alreadyExists = true break } } if !alreadyExists { - appState.NotificationDays = append(appState.NotificationDays, value) + config.NotificationDays = append(config.NotificationDays, value) } } - slices.Sort(appState.NotificationDays) + slices.Sort(config.NotificationDays) - log = fmt.Sprintln(log, fmt.Sprint("Notification will be sent this many days before expiry:", appState.NotificationDays)) + log = fmt.Sprintln(log, fmt.Sprint("Notification will be sent this many days before expiry:", config.NotificationDays)) } return log } -func importTelegramConfig(appState *state.AppState) { - appState.TelegramNotification = os.Getenv("TELEGRAM_NOTIFICATION") == "true" - appState.TelegramChatID = os.Getenv("TELEGRAM_CHAT_ID") - appState.TelegramToken = os.Getenv("TELEGRAM_TOKEN") +func importTelegramConfig(config *state.Config) { + config.TelegramNotification = os.Getenv("TELEGRAM_NOTIFICATION") == "true" + config.TelegramChatID = os.Getenv("TELEGRAM_CHAT_ID") + config.TelegramToken = os.Getenv("TELEGRAM_TOKEN") - if appState.TelegramNotification && (appState.TelegramChatID == "" || appState.TelegramToken == "") { + if config.TelegramNotification && (config.TelegramChatID == "" || config.TelegramToken == "") { panic("[ERROR] Telegram notification is enabled but chat ID or token is not set.") } - if appState.TelegramNotification && appState.TelegramChatID != "" && appState.TelegramToken != "" { - println("[INFO] │ Telegram notification enabled to channel", appState.TelegramChatID) + if config.TelegramNotification && config.TelegramChatID != "" && config.TelegramToken != "" { + println("[INFO] │ Telegram notification enabled to channel", config.TelegramChatID) } } -func importDiscordConfig(appState *state.AppState) { - appState.DiscordNotification = os.Getenv("DISCORD_NOTIFICATION") == "true" - appState.DiscordWebhookURL = os.Getenv("DISCORD_WEBHOOK_URL") +func importDiscordConfig(config *state.Config) { + config.DiscordNotification = os.Getenv("DISCORD_NOTIFICATION") == "true" + config.DiscordWebhookURL = os.Getenv("DISCORD_WEBHOOK_URL") - if appState.DiscordNotification && appState.DiscordWebhookURL == "" { + if config.DiscordNotification && config.DiscordWebhookURL == "" { panic("[ERROR] Discord notification is enabled but webhook URL is not set.") } - if appState.DiscordNotification && appState.DiscordWebhookURL != "" { - println("[INFO] │ Discord notification enabled to webhook", appState.DiscordWebhookURL) + if config.DiscordNotification && config.DiscordWebhookURL != "" { + println("[INFO] │ Discord notification enabled to webhook", config.DiscordWebhookURL) } } -func importNtfyConfig(appState *state.AppState) { - appState.NtfyNotification = os.Getenv("NTFY_NOTIFICATION") == "true" - appState.NtfyURL = os.Getenv("NTFY_URL") +func importNtfyConfig(config *state.Config) { + config.NtfyNotification = os.Getenv("NTFY_NOTIFICATION") == "true" + config.NtfyURL = os.Getenv("NTFY_URL") - if appState.NtfyNotification && appState.NtfyURL == "" { + if config.NtfyNotification && config.NtfyURL == "" { panic("[ERROR] Ntfy notification is enabled but webhook URL is not set.") } - if appState.NtfyNotification && appState.NtfyURL != "" { - println("[INFO] Ntfy notification enabled to webhook", appState.NtfyURL) + if config.NtfyNotification && config.NtfyURL != "" { + println("[INFO] Ntfy notification enabled to webhook", config.NtfyURL) } } diff --git a/internal/utils/notifications.go b/internal/utils/notifications.go index 11a7122..d25005b 100644 --- a/internal/utils/notifications.go +++ b/internal/utils/notifications.go @@ -8,35 +8,39 @@ import ( "github.com/iwa/Expira/internal/utils/providers" ) -func Notify(appState *state.AppState) { +// Notify checks all domains and sends notifications if they are approaching expiry. +// It uses the provided store for domain data and config for notification settings. +func Notify(store *state.DomainStore, config *state.Config) { println("[INFO] Sending notifications...") - for domain, domainData := range appState.Domains { - daysUntil, shouldNotify := checkDaysForNotification(domainData.ExpiryDate, appState.NotificationDays) + domains := store.GetAllDomains() + + for domain, domainData := range domains { + daysUntil, shouldNotify := checkDaysForNotification(domainData.ExpiryDate, config.NotificationDays) if shouldNotify { - if appState.TelegramNotification && (appState.TelegramChatID != "" && appState.TelegramToken != "") { + if config.TelegramNotification && (config.TelegramChatID != "" && config.TelegramToken != "") { message := fmt.Sprintf("⚠️ Domain %s will expire in %d days \nExpiry date: %s", domain, daysUntil, domainData.ExpiryDate.Format("2006-01-02 15:04:05")) - err := providers.SendTelegramMessage(appState, message) + err := providers.SendTelegramMessage(config, message) if err != nil { println("[ERROR] Failed to send notification for domain", domain, ":", err) } } - if appState.DiscordNotification && appState.DiscordWebhookURL != "" { + if config.DiscordNotification && config.DiscordWebhookURL != "" { message := fmt.Sprintf("**⚠️ Domain %s will expire in %d days**\nExpiry date: `%s`", domain, daysUntil, domainData.ExpiryDate.Format("2006-01-02 15:04:05")) - err := providers.SendDiscordMessage(appState, message) + err := providers.SendDiscordMessage(config, message) if err != nil { println("[ERROR] Failed to send notification for domain", domain, ":", err) } } - if appState.NtfyNotification && appState.NtfyURL != "" { + if config.NtfyNotification && config.NtfyURL != "" { message := fmt.Sprintf("Domain %s will expire in %d days \nExpiry date: %s", domain, daysUntil, domainData.ExpiryDate.Format("2006-01-02 15:04:05")) - err := providers.SendNtfyMessage(appState, message) + err := providers.SendNtfyMessage(config, message) if err != nil { println("[ERROR] Failed to send notification for domain", domain, ":", err) } diff --git a/internal/utils/providers/discord.go b/internal/utils/providers/discord.go index 3c82fb9..0e2336c 100644 --- a/internal/utils/providers/discord.go +++ b/internal/utils/providers/discord.go @@ -14,7 +14,9 @@ type DiscordMessage struct { Content string `json:"content"` } -func SendDiscordMessage(appState *state.AppState, message string) error { +// SendDiscordMessage sends a notification message via Discord webhook. +// It uses configuration from the provided Config instance. +func SendDiscordMessage(config *state.Config, message string) error { payload := DiscordMessage{ Content: message, } @@ -24,7 +26,7 @@ func SendDiscordMessage(appState *state.AppState, message string) error { return err } - resp, err := http.Post(appState.DiscordWebhookURL, "application/json", bytes.NewBuffer(data)) + resp, err := http.Post(config.DiscordWebhookURL, "application/json", bytes.NewBuffer(data)) if err != nil { return err } diff --git a/internal/utils/providers/ntfy.go b/internal/utils/providers/ntfy.go index b99d6ab..d704cbb 100644 --- a/internal/utils/providers/ntfy.go +++ b/internal/utils/providers/ntfy.go @@ -9,9 +9,13 @@ import ( "github.com/iwa/Expira/internal/state" ) -func SendNtfyMessage(appState *state.AppState, message string) error { - req, _ := http.NewRequest("POST", appState.NtfyURL, - strings.NewReader(message)) +// SendNtfyMessage sends a notification message via Ntfy. +// It uses configuration from the provided Config instance. +func SendNtfyMessage(config *state.Config, message string) error { + req, err := http.NewRequest("POST", config.NtfyURL, strings.NewReader(message)) + if err != nil { + return err + } req.Header.Set("Title", "Domain expiry alert") req.Header.Set("Priority", "urgent") diff --git a/internal/utils/providers/telegram.go b/internal/utils/providers/telegram.go index 1292fe4..d2f7a4b 100644 --- a/internal/utils/providers/telegram.go +++ b/internal/utils/providers/telegram.go @@ -18,9 +18,11 @@ type TelegramMessage struct { ProtectContent bool `json:"protect_content"` } -func SendTelegramMessage(appState *state.AppState, message string) error { +// SendTelegramMessage sends a notification message via Telegram API. +// It uses configuration from the provided Config instance. +func SendTelegramMessage(config *state.Config, message string) error { payload := TelegramMessage{ - ChatID: appState.TelegramChatID, + ChatID: config.TelegramChatID, Text: message, ParseMode: "HTML", DisableNotification: true, @@ -32,7 +34,7 @@ func SendTelegramMessage(appState *state.AppState, message string) error { return err } - resp, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", appState.TelegramToken), "application/json", bytes.NewBuffer(data)) + resp, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.TelegramToken), "application/json", bytes.NewBuffer(data)) if err != nil { return err } diff --git a/internal/utils/whois.go b/internal/utils/whois.go index 678068a..ac83d19 100644 --- a/internal/utils/whois.go +++ b/internal/utils/whois.go @@ -12,21 +12,26 @@ import ( "github.com/iwa/Expira/internal/state" ) -func UpdateDomains(appState *state.AppState) { +// UpdateDomains queries WHOIS servers for all domains in the store and updates their expiry dates. +// This function is thread-safe and can be called concurrently with other store operations. +func UpdateDomains(store *state.DomainStore) { println("[INFO] Updating domains...") var wg sync.WaitGroup - for domain, domainData := range appState.Domains { + // Get all domains from the store + domains := store.GetAllDomains() + + for domain, domainData := range domains { wg.Add(1) - go updateDomainExpiry(appState, domain, domainData, &wg) + go updateDomainExpiry(store, domain, domainData, &wg) } wg.Wait() println("[INFO] All domains updated.") } -func updateDomainExpiry(appState *state.AppState, domain string, domainData state.Domain, wg *sync.WaitGroup) { +func updateDomainExpiry(store *state.DomainStore, domain string, domainData state.Domain, wg *sync.WaitGroup) { defer wg.Done() res, err := getDomainExpiry(domain) @@ -41,7 +46,7 @@ func updateDomainExpiry(appState *state.AppState, domain string, domainData stat } domainData.ExpiryDate = res - appState.Domains[domain] = domainData + store.SetDomain(domain, domainData) println("[INFO] Domain:", domain, "- Expiry date:", res.Format("2006-01-02 15:04:05")) }