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)
}