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:
160
internal/preflight/preflight.go
Normal file
160
internal/preflight/preflight.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/prosolis/Pastel/internal/config"
|
||||
"github.com/prosolis/Pastel/internal/matrix"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Name string
|
||||
Status string // "pass", "fail", "skip"
|
||||
Detail string
|
||||
}
|
||||
|
||||
// Run executes all preflight checks and returns results.
|
||||
func Run(cfg *config.Config) []Result {
|
||||
var results []Result
|
||||
|
||||
results = append(results, checkMatrix(cfg))
|
||||
results = append(results, checkFrankfurter())
|
||||
|
||||
if cfg.HasSource("cheapshark") {
|
||||
results = append(results, checkCheapShark())
|
||||
} else {
|
||||
results = append(results, Result{"CheapShark", "skip", "not in DEAL_SOURCES"})
|
||||
}
|
||||
|
||||
results = append(results, checkEpic())
|
||||
|
||||
if cfg.ITADAPIKey != "" || cfg.HasSource("itad") {
|
||||
results = append(results, checkITAD(cfg))
|
||||
} else {
|
||||
results = append(results, Result{"IsThereAnyDeal", "skip", "no API key and not in DEAL_SOURCES"})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// PrintResults prints preflight results and returns true if all passed.
|
||||
func PrintResults(results []Result) bool {
|
||||
allPass := true
|
||||
for _, r := range results {
|
||||
var icon string
|
||||
switch r.Status {
|
||||
case "pass":
|
||||
icon = "✓"
|
||||
case "fail":
|
||||
icon = "✗"
|
||||
allPass = false
|
||||
case "skip":
|
||||
icon = "–"
|
||||
}
|
||||
fmt.Printf(" %s %s: %s\n", icon, r.Name, r.Detail)
|
||||
}
|
||||
return allPass
|
||||
}
|
||||
|
||||
func checkMatrix(cfg *config.Config) Result {
|
||||
client, err := matrix.New(cfg.MatrixHomeserverURL, cfg.MatrixBotUserID, cfg.MatrixBotAccessToken, "")
|
||||
if err != nil {
|
||||
return Result{"Matrix", "fail", fmt.Sprintf("client error: %v", err)}
|
||||
}
|
||||
|
||||
who, err := client.Whoami()
|
||||
if err != nil {
|
||||
return Result{"Matrix", "fail", fmt.Sprintf("whoami failed: %v", err)}
|
||||
}
|
||||
|
||||
rooms, err := client.JoinedRooms()
|
||||
if err != nil {
|
||||
return Result{"Matrix", "fail", fmt.Sprintf("joined_rooms failed: %v", err)}
|
||||
}
|
||||
|
||||
inRoom := false
|
||||
for _, r := range rooms {
|
||||
if r == cfg.MatrixDealsRoomID {
|
||||
inRoom = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !inRoom {
|
||||
return Result{"Matrix", "fail", fmt.Sprintf("bot %s is not in room %s", who, cfg.MatrixDealsRoomID)}
|
||||
}
|
||||
|
||||
return Result{"Matrix", "pass", fmt.Sprintf("authenticated as %s, in target room", who)}
|
||||
}
|
||||
|
||||
func checkCheapShark() Result {
|
||||
resp, err := http.Get("https://www.cheapshark.com/api/1.0/deals?pageSize=1")
|
||||
if err != nil {
|
||||
return Result{"CheapShark", "fail", fmt.Sprintf("request failed: %v", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var deals []json.RawMessage
|
||||
if err := json.NewDecoder(resp.Body).Decode(&deals); err != nil || len(deals) == 0 {
|
||||
return Result{"CheapShark", "fail", "unexpected response format"}
|
||||
}
|
||||
|
||||
return Result{"CheapShark", "pass", "API reachable"}
|
||||
}
|
||||
|
||||
func checkEpic() Result {
|
||||
resp, err := http.Get("https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US")
|
||||
if err != nil {
|
||||
return Result{"Epic Games", "fail", fmt.Sprintf("request failed: %v", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Data struct {
|
||||
Catalog struct {
|
||||
SearchStore struct {
|
||||
Elements []json.RawMessage `json:"elements"`
|
||||
} `json:"searchStore"`
|
||||
} `json:"Catalog"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return Result{"Epic Games", "fail", "unexpected response format"}
|
||||
}
|
||||
|
||||
return Result{"Epic Games", "pass", fmt.Sprintf("API reachable, %d elements", len(result.Data.Catalog.SearchStore.Elements))}
|
||||
}
|
||||
|
||||
func checkFrankfurter() Result {
|
||||
resp, err := http.Get("https://api.frankfurter.dev/v1/latest?base=USD&symbols=CAD,EUR,GBP")
|
||||
if err != nil {
|
||||
return Result{"Frankfurter", "fail", fmt.Sprintf("request failed: %v", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result struct {
|
||||
Rates map[string]float64 `json:"rates"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || len(result.Rates) == 0 {
|
||||
return Result{"Frankfurter", "fail", "no rates returned"}
|
||||
}
|
||||
|
||||
return Result{"Frankfurter", "pass", fmt.Sprintf("rates: %v", result.Rates)}
|
||||
}
|
||||
|
||||
func checkITAD(cfg *config.Config) Result {
|
||||
url := fmt.Sprintf("https://api.isthereanydeal.com/games/lookup/v1?key=%s&appid=220", cfg.ITADAPIKey)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return Result{"IsThereAnyDeal", "fail", fmt.Sprintf("request failed: %v", err)}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 401 || resp.StatusCode == 403 {
|
||||
return Result{"IsThereAnyDeal", "fail", "invalid API key"}
|
||||
}
|
||||
|
||||
return Result{"IsThereAnyDeal", "pass", "API key valid"}
|
||||
}
|
||||
Reference in New Issue
Block a user