package telegram import ( "fmt" "strings" "time" "git3.techno-world.net/lrosales/broad-announce/internal/alert" ) // Command is a parsed bot command. Exactly one of the fields // is non-nil per command. The handler dispatches on which // one is set. type Command struct { Raw string Start *StartCmd Subscribe *SubscribeCmd Unsubscribe *UnsubscribeCmd Preferences bool Status *StatusCmd Mute *MuteCmd Unmute bool Unknown string } // StartCmd is /start . Empty invite_code is OK // at parse time; the handler returns a friendly "please pass // your invite code" reply. type StartCmd struct { InviteCode string } // SubscribeCmd is /subscribe []. type SubscribeCmd struct { SourceID string MinSeverity string } // UnsubscribeCmd is /unsubscribe . type UnsubscribeCmd struct { SourceID string } // StatusCmd is /status [N] with N defaulting to 5. type StatusCmd struct { Limit int } // MuteCmd is /mute . We accept: // /mute 2h // /mute 30m // /mute 90s // /mute until 18:00 (HH:MM today, tomorrow if past) type MuteCmd struct { Duration time.Duration // for /mute 2h Until time.Time // for /mute until HH:MM } // Parse turns a raw message text into a Command. The // message MUST start with '/'. Text without a leading slash // is returned as a Command{Unknown: }; the handler // treats Unknown as "ignore, don't reply". func Parse(text string) Command { out := Command{Raw: text} t := strings.TrimSpace(text) if t == "" { out.Unknown = "" return out } if !strings.HasPrefix(t, "/") { out.Unknown = t return out } // Strip the leading '/' and split into parts. Telegram // commands are space-separated and may include @ // suffix (e.g. /start@acme_x_bot). parts := strings.Fields(strings.TrimPrefix(t, "/")) if len(parts) == 0 { out.Unknown = t return out } cmd := parts[0] if at := strings.Index(cmd, "@"); at >= 0 { cmd = cmd[:at] } rest := parts[1:] switch strings.ToLower(cmd) { case "start": c := StartCmd{} if len(rest) >= 1 { c.InviteCode = rest[0] } out.Start = &c case "subscribe": if len(rest) < 1 { out.Unknown = "subscribe requires []" return out } c := SubscribeCmd{SourceID: rest[0]} if len(rest) >= 2 { c.MinSeverity = rest[1] } else { c.MinSeverity = "info" // default } if _, ok := alert.ValidSeverities[alert.Severity(c.MinSeverity)]; !ok { out.Unknown = fmt.Sprintf("min_severity must be one of info|warning|critical|inminent_colapse, got %q", c.MinSeverity) return out } out.Subscribe = &c case "unsubscribe": if len(rest) < 1 { out.Unknown = "unsubscribe requires " return out } out.Unsubscribe = &UnsubscribeCmd{SourceID: rest[0]} case "preferences": out.Preferences = true case "status": c := StatusCmd{Limit: 5} if len(rest) >= 1 { var n int if _, err := fmt.Sscanf(rest[0], "%d", &n); err == nil && n > 0 && n <= 50 { c.Limit = n } } out.Status = &c case "mute": if len(rest) < 1 { out.Unknown = "mute requires e.g. 2h, 30m, 90s, or 'until 18:00'" return out } if rest[0] == "until" && len(rest) >= 2 { now := time.Now().UTC() t, err := time.ParseInLocation("15:04", rest[1], time.UTC) if err != nil { out.Unknown = fmt.Sprintf("can't parse time %q (expected HH:MM): %v", rest[1], err) return out } until := time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), 0, 0, time.UTC) if until.Before(now) { until = until.Add(24 * time.Hour) // tomorrow } out.Mute = &MuteCmd{Until: until} return out } d, err := time.ParseDuration(rest[0]) if err != nil { out.Unknown = fmt.Sprintf("can't parse duration %q (e.g. 2h, 30m, 90s): %v", rest[0], err) return out } out.Mute = &MuteCmd{Duration: d} case "unmute": out.Unmute = true default: out.Unknown = "unknown command: /" + cmd } return out }