commands.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package telegram
  2. import (
  3. "fmt"
  4. "strings"
  5. "time"
  6. "git3.techno-world.net/lrosales/broad-announce/internal/alert"
  7. )
  8. // Command is a parsed bot command. Exactly one of the fields
  9. // is non-nil per command. The handler dispatches on which
  10. // one is set.
  11. type Command struct {
  12. Raw string
  13. Start *StartCmd
  14. Subscribe *SubscribeCmd
  15. Unsubscribe *UnsubscribeCmd
  16. Preferences bool
  17. Status *StatusCmd
  18. Mute *MuteCmd
  19. Unmute bool
  20. Unknown string
  21. }
  22. // StartCmd is /start <invite_code>. Empty invite_code is OK
  23. // at parse time; the handler returns a friendly "please pass
  24. // your invite code" reply.
  25. type StartCmd struct {
  26. InviteCode string
  27. }
  28. // SubscribeCmd is /subscribe <source_id> [<min_severity>].
  29. type SubscribeCmd struct {
  30. SourceID string
  31. MinSeverity string
  32. }
  33. // UnsubscribeCmd is /unsubscribe <source_id>.
  34. type UnsubscribeCmd struct {
  35. SourceID string
  36. }
  37. // StatusCmd is /status [N] with N defaulting to 5.
  38. type StatusCmd struct {
  39. Limit int
  40. }
  41. // MuteCmd is /mute <duration>. We accept:
  42. // /mute 2h
  43. // /mute 30m
  44. // /mute 90s
  45. // /mute until 18:00 (HH:MM today, tomorrow if past)
  46. type MuteCmd struct {
  47. Duration time.Duration // for /mute 2h
  48. Until time.Time // for /mute until HH:MM
  49. }
  50. // Parse turns a raw message text into a Command. The
  51. // message MUST start with '/'. Text without a leading slash
  52. // is returned as a Command{Unknown: <text>}; the handler
  53. // treats Unknown as "ignore, don't reply".
  54. func Parse(text string) Command {
  55. out := Command{Raw: text}
  56. t := strings.TrimSpace(text)
  57. if t == "" {
  58. out.Unknown = ""
  59. return out
  60. }
  61. if !strings.HasPrefix(t, "/") {
  62. out.Unknown = t
  63. return out
  64. }
  65. // Strip the leading '/' and split into parts. Telegram
  66. // commands are space-separated and may include @<bot>
  67. // suffix (e.g. /start@acme_x_bot).
  68. parts := strings.Fields(strings.TrimPrefix(t, "/"))
  69. if len(parts) == 0 {
  70. out.Unknown = t
  71. return out
  72. }
  73. cmd := parts[0]
  74. if at := strings.Index(cmd, "@"); at >= 0 {
  75. cmd = cmd[:at]
  76. }
  77. rest := parts[1:]
  78. switch strings.ToLower(cmd) {
  79. case "start":
  80. c := StartCmd{}
  81. if len(rest) >= 1 {
  82. c.InviteCode = rest[0]
  83. }
  84. out.Start = &c
  85. case "subscribe":
  86. if len(rest) < 1 {
  87. out.Unknown = "subscribe requires <source_id> [<min_severity>]"
  88. return out
  89. }
  90. c := SubscribeCmd{SourceID: rest[0]}
  91. if len(rest) >= 2 {
  92. c.MinSeverity = rest[1]
  93. } else {
  94. c.MinSeverity = "info" // default
  95. }
  96. if _, ok := alert.ValidSeverities[alert.Severity(c.MinSeverity)]; !ok {
  97. out.Unknown = fmt.Sprintf("min_severity must be one of info|warning|critical|inminent_colapse, got %q", c.MinSeverity)
  98. return out
  99. }
  100. out.Subscribe = &c
  101. case "unsubscribe":
  102. if len(rest) < 1 {
  103. out.Unknown = "unsubscribe requires <source_id>"
  104. return out
  105. }
  106. out.Unsubscribe = &UnsubscribeCmd{SourceID: rest[0]}
  107. case "preferences":
  108. out.Preferences = true
  109. case "status":
  110. c := StatusCmd{Limit: 5}
  111. if len(rest) >= 1 {
  112. var n int
  113. if _, err := fmt.Sscanf(rest[0], "%d", &n); err == nil && n > 0 && n <= 50 {
  114. c.Limit = n
  115. }
  116. }
  117. out.Status = &c
  118. case "mute":
  119. if len(rest) < 1 {
  120. out.Unknown = "mute requires <duration> e.g. 2h, 30m, 90s, or 'until 18:00'"
  121. return out
  122. }
  123. if rest[0] == "until" && len(rest) >= 2 {
  124. now := time.Now().UTC()
  125. t, err := time.ParseInLocation("15:04", rest[1], time.UTC)
  126. if err != nil {
  127. out.Unknown = fmt.Sprintf("can't parse time %q (expected HH:MM): %v", rest[1], err)
  128. return out
  129. }
  130. until := time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), 0, 0, time.UTC)
  131. if until.Before(now) {
  132. until = until.Add(24 * time.Hour) // tomorrow
  133. }
  134. out.Mute = &MuteCmd{Until: until}
  135. return out
  136. }
  137. d, err := time.ParseDuration(rest[0])
  138. if err != nil {
  139. out.Unknown = fmt.Sprintf("can't parse duration %q (e.g. 2h, 30m, 90s): %v", rest[0], err)
  140. return out
  141. }
  142. out.Mute = &MuteCmd{Duration: d}
  143. case "unmute":
  144. out.Unmute = true
  145. default:
  146. out.Unknown = "unknown command: /" + cmd
  147. }
  148. return out
  149. }