Rewrite Pastel from Python to Go with watchlist feature
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>
This commit is contained in:
96
internal/currency/currency.go
Normal file
96
internal/currency/currency.go
Normal file
@@ -0,0 +1,96 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user