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"))
}