Full rewrite of the gaming deals Matrix bot in Go for better performance and a single static binary. Includes all original functionality (CheapShark, ITAD, Epic free games, multi-currency pricing, dedup, preflight checks, presence heartbeat) plus a new watchlist feature where users can DM the bot to watch for specific game deals with 180-day auto-expiry. New: systemd service file, DB migration script for Python-to-Go transition. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
97 lines
1.9 KiB
Go
97 lines
1.9 KiB
Go
package currency
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
apiURL = "https://api.frankfurter.dev/v1/latest?base=USD&symbols=CAD,EUR,GBP"
|
|
cacheTTL = 12 * time.Hour
|
|
)
|
|
|
|
var symbols = map[string]string{
|
|
"USD": "$",
|
|
"CAD": "C$",
|
|
"EUR": "€",
|
|
"GBP": "£",
|
|
}
|
|
|
|
var currencies = []string{"CAD", "EUR", "GBP"}
|
|
|
|
type Converter struct {
|
|
mu sync.Mutex
|
|
rates map[string]float64
|
|
lastFetched time.Time
|
|
}
|
|
|
|
func NewConverter() *Converter {
|
|
return &Converter{}
|
|
}
|
|
|
|
func (c *Converter) fetchRates() error {
|
|
resp, err := http.Get(apiURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("frankfurter API returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
var result struct {
|
|
Rates map[string]float64 `json:"rates"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.rates = result.Rates
|
|
c.lastFetched = time.Now()
|
|
return nil
|
|
}
|
|
|
|
// EnsureRates fetches exchange rates if the cache is stale or empty.
|
|
func (c *Converter) EnsureRates() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if time.Since(c.lastFetched) < cacheTTL && c.rates != nil {
|
|
return
|
|
}
|
|
|
|
if err := c.fetchRates(); err != nil {
|
|
slog.Warn("failed to fetch exchange rates, using USD only", "error", err)
|
|
}
|
|
}
|
|
|
|
// FormatPrice converts a USD amount to multi-currency display string.
|
|
func (c *Converter) FormatPrice(usd float64) string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
parts := []string{fmt.Sprintf("$%.2f", usd)}
|
|
|
|
for _, cur := range currencies {
|
|
if rate, ok := c.rates[cur]; ok {
|
|
sym := symbols[cur]
|
|
parts = append(parts, fmt.Sprintf("%s%.2f", sym, usd*rate))
|
|
}
|
|
}
|
|
|
|
return strings.Join(parts, " · ")
|
|
}
|
|
|
|
// HasRates returns true if exchange rates are available.
|
|
func (c *Converter) HasRates() bool {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.rates != nil && len(c.rates) > 0
|
|
}
|