Files
Pastel/internal/preflight/preflight.go
prosolis d6c8d4f599 Add token auto-refresh, device persistence, and cross-signing
Following gogobee's auth pattern: if MATRIX_BOT_PASSWORD is set, the bot
persists device credentials to device.json, validates tokens on startup,
re-logs in automatically when tokens expire, and bootstraps cross-signing
so the device shows as verified without manual emoji verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:56:26 -07:00

165 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(matrix.ClientConfig{
HomeserverURL: cfg.MatrixHomeserverURL,
UserID: cfg.MatrixBotUserID,
AccessToken: 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"}
}