chatto.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "strings"
  6. "github.com/joho/godotenv"
  7. "github.com/spf13/cobra"
  8. "gogs.netdome.biz/paul/cheapo/internal/chatto"
  9. "gogs.netdome.biz/paul/cheapo/internal/game"
  10. )
  11. // loadEnv reads a .env file from the working directory if present (godotenv).
  12. // Existing environment variables take precedence over .env values.
  13. func loadEnv() {
  14. _ = godotenv.Load()
  15. }
  16. // chattoClientFromEnv builds a Chatto client from the CHATTO_TOKEN
  17. // environment variable (or .env file); CHATTO_BASE_URL defaults to the public
  18. // Chatto server (https://chat.chatto.run). The token is provided by the
  19. // operator; this tool never fetches credentials itself.
  20. func chattoClientFromEnv() (*chatto.Client, error) {
  21. loadEnv()
  22. base := os.Getenv("CHATTO_BASE_URL")
  23. if base == "" {
  24. base = "https://chat.chatto.run"
  25. }
  26. token := os.Getenv("CHATTO_TOKEN")
  27. if token == "" {
  28. return nil, fmt.Errorf("CHATTO_TOKEN must be set (base URL: %s, overridable via CHATTO_BASE_URL)", base)
  29. }
  30. return chatto.NewClient(base, token), nil
  31. }
  32. // formatGames renders games as a Chatto Markdown message body.
  33. func formatGames(games []game.Game, upcoming bool) string {
  34. if len(games) == 0 {
  35. if upcoming {
  36. return "🗓 **Upcoming free games:** none announced."
  37. }
  38. return "🎮 **Current free games:** none on offer."
  39. }
  40. var b strings.Builder
  41. if upcoming {
  42. b.WriteString("## 🗓 Upcoming free games\n\n")
  43. } else {
  44. b.WriteString("## 🎮 Current free games\n\n")
  45. }
  46. // One table per store keeps columns uniform (dates only for epic).
  47. b.WriteString("| Game | Store | Period |\n")
  48. b.WriteString("|---|---|---|\n")
  49. for _, g := range games {
  50. period := "—"
  51. if g.Kind == "upcoming" && !g.StartDate.IsZero() && !g.EndDate.IsZero() {
  52. period = fmt.Sprintf("from %s, until %s",
  53. g.StartDate.Local().Format("02.01."),
  54. g.EndDate.Local().Format("02.01."))
  55. } else if !g.EndDate.IsZero() {
  56. period = "until " + g.EndDate.Local().Format("02.01.")
  57. }
  58. fmt.Fprintf(&b, "| **%s** | [%s](%s) | %s |\n",
  59. escapePipe(g.Title), g.Store, g.URL, period)
  60. }
  61. b.WriteString("\n_As of now — free from/until 17:00 Europe/Berlin each._")
  62. return b.String()
  63. }
  64. // escapePipe prevents table cell breakage via titles containing '|'.
  65. func escapePipe(s string) string {
  66. return strings.ReplaceAll(s, "|", "\\|")
  67. }
  68. // chattoSendCmd posts the current free games to a Chatto room.
  69. func chattoSendCmd() *cobra.Command {
  70. var (
  71. roomID string
  72. upcoming bool
  73. )
  74. cmd := &cobra.Command{
  75. Use: "chatto-send",
  76. Short: "Post current free games to a Chatto room",
  77. Long: `Fetches the free games and posts them as a message to the Chatto room.
  78. Credentials come from the CHATTO_BASE_URL and CHATTO_TOKEN environment
  79. variables (set by the operator, never fetched automatically).`,
  80. RunE: func(cmd *cobra.Command, args []string) error {
  81. ctx := cmd.Context()
  82. c, err := chattoClientFromEnv()
  83. if err != nil {
  84. return err
  85. }
  86. if roomID == "" {
  87. return fmt.Errorf("--room is required (room ID)")
  88. }
  89. var games []game.Game
  90. if upcoming {
  91. // Upcoming windows only exist for Epic.
  92. u := newEpic()
  93. games, err = u.GetUpcomingGames(ctx)
  94. if err != nil {
  95. return err
  96. }
  97. } else {
  98. games, err = fetchAll(ctx)
  99. if err != nil && len(games) == 0 {
  100. return err
  101. }
  102. }
  103. msg, err := c.CreateMessage(ctx, roomID, formatGames(games, upcoming), "")
  104. if err != nil {
  105. return err
  106. }
  107. fmt.Fprintf(cmd.OutOrStdout(), "✅ Sent: event %s\n", msg.EventID)
  108. return nil
  109. },
  110. }
  111. cmd.Flags().StringVar(&roomID, "room", "", "Chatto room ID (e.g. RtLrcOrNgvv3StA)")
  112. cmd.Flags().BoolVar(&upcoming, "upcoming", false, "Post upcoming instead of current free games (Epic only)")
  113. return cmd
  114. }
  115. // chattoRoomsCmd lists rooms visible to the token's user — verification helper.
  116. func chattoRoomsCmd() *cobra.Command {
  117. return &cobra.Command{
  118. Use: "chatto-rooms",
  119. Short: "List visible Chatto rooms (client verification)",
  120. RunE: func(cmd *cobra.Command, args []string) error {
  121. c, err := chattoClientFromEnv()
  122. if err != nil {
  123. return err
  124. }
  125. rooms, err := c.ListRooms(cmd.Context())
  126. if err != nil {
  127. return err
  128. }
  129. out := cmd.OutOrStdout()
  130. for _, r := range rooms {
  131. fmt.Fprintf(out, "%s\t%s\t%s\n", r.ID, r.Kind, r.Name)
  132. }
  133. fmt.Fprintf(out, "(%d rooms)\n", len(rooms))
  134. return nil
  135. },
  136. }
  137. }