|
|
@@ -1,6 +1,7 @@
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
+ "context"
|
|
|
"fmt"
|
|
|
"os"
|
|
|
"strings"
|
|
|
@@ -35,25 +36,33 @@ func chattoClientFromEnv() (*chatto.Client, error) {
|
|
|
return chatto.NewClient(base, token), nil
|
|
|
}
|
|
|
|
|
|
-// formatGames renders games as a Chatto Markdown message body.
|
|
|
-func formatGames(games []game.Game, upcoming bool) string {
|
|
|
+// formatGames renders games as a Chatto Markdown message body. mode is ""
|
|
|
+// (free now), "upcoming" or "deals".
|
|
|
+func formatGames(games []game.Game, mode string) string {
|
|
|
if len(games) == 0 {
|
|
|
- if upcoming {
|
|
|
+ switch mode {
|
|
|
+ case "upcoming":
|
|
|
return "🗓 **Upcoming free games:** none announced."
|
|
|
+ case "deals":
|
|
|
+ return "🏷 **Current deals:** none found."
|
|
|
+ default:
|
|
|
+ return "🎮 **Current free games:** none on offer."
|
|
|
}
|
|
|
- return "🎮 **Current free games:** none on offer."
|
|
|
}
|
|
|
|
|
|
var b strings.Builder
|
|
|
- if upcoming {
|
|
|
+ switch mode {
|
|
|
+ case "upcoming":
|
|
|
b.WriteString("## 🗓 Upcoming free games\n\n")
|
|
|
- } else {
|
|
|
+ case "deals":
|
|
|
+ b.WriteString("## 🏷 Current deals\n\n")
|
|
|
+ default:
|
|
|
b.WriteString("## 🎮 Current free games\n\n")
|
|
|
}
|
|
|
|
|
|
// One table per store keeps columns uniform (dates only for epic).
|
|
|
- b.WriteString("| Game | Store | Period |\n")
|
|
|
- b.WriteString("|---|---|---|\n")
|
|
|
+ b.WriteString("| Game | Store | Price | Period |\n")
|
|
|
+ b.WriteString("|---|---|---|---|\n")
|
|
|
for _, g := range games {
|
|
|
period := "—"
|
|
|
if g.Kind == "upcoming" && !g.StartDate.IsZero() && !g.EndDate.IsZero() {
|
|
|
@@ -63,24 +72,67 @@ func formatGames(games []game.Game, upcoming bool) string {
|
|
|
} else if !g.EndDate.IsZero() {
|
|
|
period = "until " + g.EndDate.Local().Format("02.01.")
|
|
|
}
|
|
|
- fmt.Fprintf(&b, "| **%s** | [%s](%s) | %s |\n",
|
|
|
- escapePipe(g.Title), g.Store, g.URL, period)
|
|
|
+ fmt.Fprintf(&b, "| **%s** | [%s](%s) | %s | %s |\n",
|
|
|
+ escapePipe(g.Title), g.Store, g.URL, priceCell(g), period)
|
|
|
}
|
|
|
|
|
|
b.WriteString("\n_As of now — free from/until 17:00 Europe/Berlin each._")
|
|
|
return b.String()
|
|
|
}
|
|
|
|
|
|
+// priceCell renders the price column: "0 €" for free games, original → final
|
|
|
+// with percent for deals.
|
|
|
+func priceCell(g game.Game) string {
|
|
|
+ switch {
|
|
|
+ case g.DiscountPct > 0 && g.FinalPriceFmt != "":
|
|
|
+ return fmt.Sprintf("%s → **%s** (−%d %%)", g.PriceFmt, g.FinalPriceFmt, g.DiscountPct)
|
|
|
+ case g.Kind == "free-now" || g.Kind == "free-to-keep":
|
|
|
+ return "**0 €**"
|
|
|
+ case g.PriceFmt != "":
|
|
|
+ return g.PriceFmt
|
|
|
+ default:
|
|
|
+ return "—"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// escapePipe prevents table cell breakage via titles containing '|'.
|
|
|
func escapePipe(s string) string {
|
|
|
return strings.ReplaceAll(s, "|", "\\|")
|
|
|
}
|
|
|
|
|
|
+// fetchDeals queries all selected providers for discounted deals (any
|
|
|
+// discount); a failing provider is reported on stderr but does not abort.
|
|
|
+func fetchDeals(ctx context.Context) ([]game.Game, error) {
|
|
|
+ provs, err := newProviders()
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ var all []game.Game
|
|
|
+ var hadErr error
|
|
|
+ for _, p := range provs {
|
|
|
+ d, ok := p.(interface {
|
|
|
+ GetDeals(ctx context.Context) ([]game.Game, error)
|
|
|
+ })
|
|
|
+ if !ok {
|
|
|
+ continue // provider has no deals concept (gog)
|
|
|
+ }
|
|
|
+ games, err := d.GetDeals(ctx)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Fprintf(os.Stderr, "⚠ %s: %v\n", p.Name(), err)
|
|
|
+ hadErr = err
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ all = append(all, games...)
|
|
|
+ }
|
|
|
+ return all, hadErr
|
|
|
+}
|
|
|
+
|
|
|
// chattoSendCmd posts the current free games to a Chatto room.
|
|
|
func chattoSendCmd() *cobra.Command {
|
|
|
var (
|
|
|
roomID string
|
|
|
upcoming bool
|
|
|
+ deals bool
|
|
|
)
|
|
|
cmd := &cobra.Command{
|
|
|
Use: "chatto-send",
|
|
|
@@ -100,21 +152,27 @@ variables (set by the operator, never fetched automatically).`,
|
|
|
}
|
|
|
|
|
|
var games []game.Game
|
|
|
- if upcoming {
|
|
|
+ switch {
|
|
|
+ case deals:
|
|
|
+ games, err = fetchDeals(ctx)
|
|
|
+ if err != nil && len(games) == 0 {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ case upcoming:
|
|
|
// Upcoming windows only exist for Epic.
|
|
|
u := newEpic()
|
|
|
games, err = u.GetUpcomingGames(ctx)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- } else {
|
|
|
+ default:
|
|
|
games, err = fetchAll(ctx)
|
|
|
if err != nil && len(games) == 0 {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- msg, err := c.CreateMessage(ctx, roomID, formatGames(games, upcoming), "")
|
|
|
+ msg, err := c.CreateMessage(ctx, roomID, formatGames(games, modeOf(upcoming, deals)), "")
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
@@ -124,9 +182,22 @@ variables (set by the operator, never fetched automatically).`,
|
|
|
}
|
|
|
cmd.Flags().StringVar(&roomID, "room", "", "Chatto room ID (e.g. RtLrcOrNgvv3StA)")
|
|
|
cmd.Flags().BoolVar(&upcoming, "upcoming", false, "Post upcoming instead of current free games (Epic only)")
|
|
|
+ cmd.Flags().BoolVar(&deals, "deals", false, "Post discounted games (deals) instead of free games")
|
|
|
return cmd
|
|
|
}
|
|
|
|
|
|
+// modeOf picks the heading mode for formatGames.
|
|
|
+func modeOf(upcoming, deals bool) string {
|
|
|
+ switch {
|
|
|
+ case upcoming:
|
|
|
+ return "upcoming"
|
|
|
+ case deals:
|
|
|
+ return "deals"
|
|
|
+ default:
|
|
|
+ return ""
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// chattoRoomsCmd lists rooms visible to the token's user — verification helper.
|
|
|
func chattoRoomsCmd() *cobra.Command {
|
|
|
return &cobra.Command{
|