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:
147
internal/deals/epic.go
Normal file
147
internal/deals/epic.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package deals
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EpicFreeGame struct {
|
||||
ID string
|
||||
Title string
|
||||
Desc string
|
||||
URL string
|
||||
EndDate *time.Time
|
||||
Upcoming bool
|
||||
DedupID string
|
||||
}
|
||||
|
||||
type epicResponse struct {
|
||||
Data struct {
|
||||
Catalog struct {
|
||||
SearchStore struct {
|
||||
Elements []epicElement `json:"elements"`
|
||||
} `json:"searchStore"`
|
||||
} `json:"Catalog"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type epicElement struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
ProductSlug string `json:"productSlug"`
|
||||
URLSlug string `json:"urlSlug"`
|
||||
CatalogNs struct {
|
||||
Mappings []struct {
|
||||
PageSlug string `json:"pageSlug"`
|
||||
} `json:"mappings"`
|
||||
} `json:"catalogNs"`
|
||||
Promotions *struct {
|
||||
PromotionalOffers []epicOfferGroup `json:"promotionalOffers"`
|
||||
UpcomingPromotionalOffers []epicOfferGroup `json:"upcomingPromotionalOffers"`
|
||||
} `json:"promotions"`
|
||||
}
|
||||
|
||||
type epicOfferGroup struct {
|
||||
PromotionalOffers []epicOffer `json:"promotionalOffers"`
|
||||
}
|
||||
|
||||
type epicOffer struct {
|
||||
StartDate string `json:"startDate"`
|
||||
EndDate string `json:"endDate"`
|
||||
DiscountSetting struct {
|
||||
DiscountPercentage int `json:"discountPercentage"`
|
||||
} `json:"discountSetting"`
|
||||
}
|
||||
|
||||
// FetchEpicFreeGames fetches current and upcoming free games from Epic Games Store.
|
||||
func FetchEpicFreeGames() ([]EpicFreeGame, error) {
|
||||
resp, err := http.Get("https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("epic request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("epic returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result epicResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, fmt.Errorf("epic decode failed: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var games []EpicFreeGame
|
||||
|
||||
for _, elem := range result.Data.Catalog.SearchStore.Elements {
|
||||
if elem.Promotions == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
slug := elem.ProductSlug
|
||||
if slug == "" {
|
||||
slug = elem.URLSlug
|
||||
}
|
||||
if slug == "" && len(elem.CatalogNs.Mappings) > 0 {
|
||||
slug = elem.CatalogNs.Mappings[0].PageSlug
|
||||
}
|
||||
storeURL := fmt.Sprintf("https://store.epicgames.com/en-US/p/%s", slug)
|
||||
|
||||
// Check current free offers
|
||||
for _, group := range elem.Promotions.PromotionalOffers {
|
||||
for _, offer := range group.PromotionalOffers {
|
||||
if offer.DiscountSetting.DiscountPercentage != 0 {
|
||||
continue
|
||||
}
|
||||
start, _ := time.Parse(time.RFC3339, offer.StartDate)
|
||||
end, _ := time.Parse(time.RFC3339, offer.EndDate)
|
||||
|
||||
if start.After(now) || end.Before(now) {
|
||||
continue
|
||||
}
|
||||
|
||||
game := EpicFreeGame{
|
||||
ID: elem.ID,
|
||||
Title: elem.Title,
|
||||
Desc: elem.Description,
|
||||
URL: storeURL,
|
||||
Upcoming: false,
|
||||
DedupID: fmt.Sprintf("epic-%s", elem.ID),
|
||||
}
|
||||
if !end.IsZero() {
|
||||
game.EndDate = &end
|
||||
}
|
||||
games = append(games, game)
|
||||
}
|
||||
}
|
||||
|
||||
// Check upcoming free offers
|
||||
for _, group := range elem.Promotions.UpcomingPromotionalOffers {
|
||||
for _, offer := range group.PromotionalOffers {
|
||||
if offer.DiscountSetting.DiscountPercentage != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
game := EpicFreeGame{
|
||||
ID: elem.ID,
|
||||
Title: elem.Title,
|
||||
Desc: elem.Description,
|
||||
URL: storeURL,
|
||||
Upcoming: true,
|
||||
DedupID: fmt.Sprintf("epic-%s", elem.ID),
|
||||
}
|
||||
if offer.EndDate != "" {
|
||||
if end, err := time.Parse(time.RFC3339, offer.EndDate); err == nil {
|
||||
game.EndDate = &end
|
||||
}
|
||||
}
|
||||
games = append(games, game)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return games, nil
|
||||
}
|
||||
Reference in New Issue
Block a user