package bot import ( "fmt" "html" "strconv" "strings" "time" openai "github.com/tiennm99/openai-status-bot/internal/openai" "github.com/tiennm99/openai-status-bot/internal/poller" ) const statusURL = "https://status.openai.com/" const helpText = `OpenAI Status Bot /start - subscribe this chat or topic /stop - unsubscribe this chat or topic /status [component] - show current OpenAI status /components - show all component statuses /subscribe <incident|component|all> - set notification types /subscribe component <name|id|all> - filter component updates /history [count] - show recent incidents, default 5, max 10 /uptime - show component health overview /info - show chat and subscription details /help - show this help` func formatStatus(summary openai.Summary) string { lines := []string{ "OpenAI Status", "", fmt.Sprintf("Overall: %s", escape(summary.Status.Description)), } duplicates := poller.DuplicateComponentNames(summary.Components) degraded := make([]string, 0) for _, component := range summary.Components { if component.Group || component.Status == "operational" { continue } degraded = append(degraded, fmt.Sprintf("- %s: %s", escape(poller.ComponentLabel(component, duplicates[component.Name])), escape(poller.StatusLabel(component.Status)))) } if len(degraded) == 0 { lines = append(lines, "", "All listed components are operational.") } else { lines = append(lines, "", "Affected components:") lines = append(lines, degraded...) } lines = append(lines, "", fmt.Sprintf("View full status page", statusURL)) return strings.Join(lines, "\n") } func formatComponentStatus(component openai.Component, duplicate bool) string { return fmt.Sprintf( "%s\n\nStatus: %s\nLast change: %s UTC\n\nView full status page", escape(poller.ComponentLabel(component, duplicate)), escape(poller.StatusLabel(component.Status)), escape(formatTime(component.UpdatedAt)), statusURL, ) } func formatComponents(summary openai.Summary) string { lines := []string{"OpenAI Components", ""} duplicates := poller.DuplicateComponentNames(summary.Components) for _, component := range summary.Components { if component.Group { continue } lines = append(lines, fmt.Sprintf("- %s: %s", escape(poller.ComponentLabel(component, duplicates[component.Name])), escape(poller.StatusLabel(component.Status)))) } return truncateMessage(strings.Join(lines, "\n")) } func formatHistory(incidents []openai.Incident, count int) string { if len(incidents) == 0 { return "No recent incidents found." } if count > len(incidents) { count = len(incidents) } lines := []string{"Recent OpenAI Incidents", ""} for i := 0; i < count; i++ { incident := incidents[i] link := incident.Shortlink if link == "" && incident.ID != "" { link = statusURL + "incidents/" + incident.ID } entry := fmt.Sprintf( "%d. [%s] %s\n Created: %s UTC\n Status: %s", i+1, escape(strings.ToUpper(emptyDefault(incident.Impact, "unknown"))), escape(incident.Name), escape(formatDate(incident.CreatedAt)), escape(poller.StatusLabel(incident.Status)), ) if link != "" { entry += fmt.Sprintf("\n Details", escape(link)) } lines = append(lines, entry) } lines = append(lines, "", fmt.Sprintf("View full history", statusURL)) return truncateMessage(strings.Join(lines, "\n\n")) } func formatUptime(summary openai.Summary) string { lines := []string{"OpenAI Component Health", ""} duplicates := poller.DuplicateComponentNames(summary.Components) for _, component := range summary.Components { if component.Group { continue } lines = append(lines, fmt.Sprintf( "%s\n Status: %s\n Last change: %s UTC", escape(poller.ComponentLabel(component, duplicates[component.Name])), escape(poller.StatusLabel(component.Status)), escape(formatTime(component.UpdatedAt)), )) } lines = append(lines, "", "Uptime percentage is not available from the public Statuspage API.") lines = append(lines, fmt.Sprintf("View full status page", statusURL)) return truncateMessage(strings.Join(lines, "\n")) } func parseHistoryCount(fields []string) int { const ( defaultCount = 5 maxCount = 10 ) if len(fields) < 2 { return defaultCount } count, err := strconv.Atoi(fields[1]) if err != nil || count < 1 { return defaultCount } if count > maxCount { return maxCount } return count } const ( telegramMessageLimit = 3900 truncationNotice = "\n\n… truncated" ) // truncateMessage trims an HTML-formatted message to Telegram's length limit. // Every formatter assembles messages from newline-delimited fragments whose // HTML tags are individually balanced, so cutting on a newline boundary keeps // the markup valid. A naive mid-string cut could split a tag () or an // entity (&am|p;), or drop a closing tag, making Telegram reject the entire // message with "can't parse entities". func truncateMessage(value string) string { runes := []rune(value) if len(runes) <= telegramMessageLimit { return value } budget := telegramMessageLimit - len([]rune(truncationNotice)) truncated := string(runes[:budget]) if idx := strings.LastIndexByte(truncated, '\n'); idx >= 0 { truncated = truncated[:idx] } else { truncated = dropPartialMarkup(truncated) } return strings.TrimRight(truncated, "\n ") + truncationNotice } // dropPartialMarkup removes a trailing incomplete HTML tag or entity left by a // hard cut, so the result never ends inside "<...>" or "&...;". Only reached // for a single fragment longer than the whole limit, which the formatters do // not produce in practice. func dropPartialMarkup(s string) string { if lt := strings.LastIndexByte(s, '<'); lt > strings.LastIndexByte(s, '>') { s = s[:lt] } if amp := strings.LastIndexByte(s, '&'); amp > strings.LastIndexByte(s, ';') { s = s[:amp] } return s } func formatDate(value string) string { parsed, err := time.Parse(time.RFC3339, value) if err != nil { return emptyDefault(value, "unknown") } return parsed.UTC().Format("Jan 2, 2006") } func formatTime(value string) string { parsed, err := time.Parse(time.RFC3339, value) if err != nil { return emptyDefault(value, "unknown") } return parsed.UTC().Format("Jan 2, 2006 15:04") } func emptyDefault(value, fallback string) string { if strings.TrimSpace(value) == "" { return fallback } return value } func escape(value string) string { return html.EscapeString(value) }