package main import ( "context" "database/sql" "encoding/json" "errors" "fmt" "github.com/disgoorg/disgo" "github.com/disgoorg/disgo/discord" "github.com/disgoorg/disgo/rest" "github.com/disgoorg/disgo/webhook" "github.com/disgoorg/snowflake/v2" "grow.rievo.dev/discordBots" "grow.rievo.dev/discordBots/cmd/dealsbot/api" "grow.rievo.dev/discordBots/pkg/db" "log/slog" "os" "os/signal" "reflect" "strings" "syscall" "time" _ "modernc.org/sqlite" ) var ( webhookID = snowflake.GetEnv("webhook_id") webhookToken = os.Getenv("webhook_token") ) var logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) var release string func main() { // query different sources store to db // try to incorporate operagx apiUrl: // - https://gx-proxy.operacdn.com/content/free-games?_limit=300&_sort=order%3AASC // send messages to discord // ideas: // - https://github.com/TheLovinator1/discord-free-game-notifier // - https://gg.deals/news/free-gog-games/ // - origin // - need new ubisoft api if it even exists // - add more supported types for steam (bundles,software,dlc) logger.Info("starting dealsbot...", slog.String("disgo version", disgo.Version)) client := webhook.New(webhookID, webhookToken) defer client.Close(context.TODO()) ctx := context.Background() con, err := sql.Open("sqlite", "file:db/deals.db?cache=shared") if err != nil { logger.Error("error opening db", slog.Any("error", err)) panic(err) } defer con.Close() // create tables if _, err := con.ExecContext(ctx, discordBots.Schema); err != nil { logger.Error("error creating db schema", slog.Any("error", err)) panic(err) } query := db.New(con) ticker := time.NewTicker(1 * time.Minute) quit := make(chan struct{}) go func() { for { select { case <-ticker.C: var apis []api.Api apis = append(apis, api.NewEpicApi(logger), api.NewSteamApi(logger), api.NewGogFrontApi(logger), api.NewGogApi(logger), api.NewHumbleBundleApi(logger), ) for _, a := range apis { err := a.Load() if err != nil { logger.Error("failed loading api", slog.Any("error", err)) } } var deals []api.Deal for _, a := range apis { apiDeals := a.Get() deals = append(deals, apiDeals...) } for _, deal := range deals { retrievedDealJson, dbErr := query.GetItem(ctx, deal.Id) retrievedDeal := api.Deal{} if err := json.Unmarshal(retrievedDealJson.Data, &retrievedDeal); !errors.Is(dbErr, sql.ErrNoRows) && err != nil { logger.Error("failed unmarshalling deal", slog.Any("error", err)) } if deal.Id == retrievedDeal.Id { logger.Debug("deal is already published", slog.String("deal", deal.Id)) } else if reflect.DeepEqual(deal, retrievedDeal) { logger.Error("deal is published but not equal", slog.String("deal", deal.Id)) } else { logger.Info("deal is new and will be published", slog.String("deal", deal.Id)) go sendWebhook(client, deal) dealJson, _ := json.Marshal(deal) err = query.CreateItem(ctx, db.CreateItemParams{ ID: deal.Id, Data: dealJson, }) if err != nil { logger.Error("failed saving deal", slog.Any("error", err)) } } } case <-quit: ticker.Stop() return } } }() logger.Info("dealsbot is now running. Press CTRL-C to exit.", slog.String("version", release)) s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) <-s } func sendWebhook(client webhook.Client, deal api.Deal) { content := "currently free:" message := discord.NewWebhookMessageCreateBuilder() if deal.Image == "" { message.SetContent(fmt.Sprintf("%v %v\n", content, deal.Url)) } else { embed := discord.NewEmbedBuilder(). SetTitle(deal.Title). SetURL(deal.Url). SetImage(deal.Image) var author, icon string switch { case strings.HasPrefix(deal.Id, "epic-"): author = "Epic Games" icon = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Epic_Games_logo.svg/207px-Epic_Games_logo.svg.png" case strings.HasPrefix(deal.Id, "gog-"): author = "GOG.com" icon = "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/GOG.com_logo.svg/391px-GOG.com_logo.svg.png" case strings.HasPrefix(deal.Id, "humblebundle-"): author = "Humble Bundle" icon = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Humble_Bundle_logo.svg/320px-Humble_Bundle_logo.svg.png" case strings.HasPrefix(deal.Id, "steam-"): author = "Steam" icon = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Steam_icon_logo.svg/240px-Steam_icon_logo.svg.png" case strings.HasPrefix(deal.Id, "ubisoft-"): author = "Ubisoft" icon = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/78/Ubisoft_logo.svg/262px-Ubisoft_logo.svg.png" } embed.SetAuthor(author, "", icon) message.SetContent(content).SetEmbeds(embed.Build()) } if _, err := client.CreateMessage(message.Build(), rest.WithDelay(2*time.Second), ); err != nil { logger.Error("sending message failed", slog.Any("error", err)) } }