Files
NanamiAdmin 057d7d584d feat: add incoming webhook API support with configurable endpoints
- Implemented the incoming webhook API to handle alerts from external applications.
- Added configuration options for webhook listening address, port, and endpoints in config.go.
- Created WebhookReceiverConfig and WebhookEndpointConfig structures to manage webhook settings.
- Developed WebhookHandler to process incoming requests, validate tokens, and deliver alerts to specified channels.
- Enhanced existing controller interfaces to support alert delivery.
- Updated message rendering to respect Markdown settings for different channels.
- Added tests for webhook functionality and ensured proper error handling.
2026-09-24 11:21:38 +08:00

94 lines
2.6 KiB
Go

package telegram
import (
"context"
"strings"
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"nukumizu-backend/internal/controller"
)
// maxMessageLen is the safe chunk size for outbound messages. Telegram's hard
// limit is 4096 characters; staying under it leaves headroom for encoding.
const maxMessageLen = 4000
// sendMessage sends a text message to a chat, splitting it into chunks that fit
// Telegram's 4096-character limit. When the channel has markdown enabled the
// message is sent with parse_mode=Markdown so fenced code blocks and inline
// formatting render as rich text; templates must then stay valid under
// Telegram's legacy Markdown, because unpaired '*' or '_' characters (e.g. a
// lone '*Event: ...' label) make the API reject the whole message. With
// markdown disabled the message is sent without a parse mode, so it is
// delivered verbatim whatever it contains.
func (t *TelegramController) sendMessage(message controller.Message) error {
if t.client == nil {
return nil
}
if strings.TrimSpace(message.Content) == "" {
return nil
}
for _, chunk := range splitMessage(message.Content, maxMessageLen) {
if err := t.sendMessageChunk(message.ChatID, chunk); err != nil {
return err
}
}
return nil
}
// sendMessageChunk issues a single sendMessage API call for one chunk.
func (t *TelegramController) sendMessageChunk(chatID int64, text string) error {
ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
defer cancel()
params := &bot.SendMessageParams{
ChatID: chatID,
Text: text,
}
if t.cfg.Markdown {
params.ParseMode = models.ParseModeMarkdownV1 // Telegram legacy Markdown
}
_, err := t.client.SendMessage(ctx, params)
return err
}
// splitMessage splits text into chunks of at most maxLen runes, keeping
// complete lines when possible.
func splitMessage(text string, maxLen int) []string {
if maxLen <= 0 {
return []string{text}
}
remaining := []rune(text)
if len(remaining) <= maxLen {
return []string{text}
}
var chunks []string
for len(remaining) > maxLen {
// Prefer the last newline within the window so messages aren't cut
// mid-line; fall back to a hard rune cut for overlong lines.
cut := maxLen
if nl := lastIndexRune(remaining[:maxLen], '\n'); nl > 0 {
cut = nl + 1
}
chunks = append(chunks, string(remaining[:cut]))
remaining = remaining[cut:]
}
if len(remaining) > 0 {
chunks = append(chunks, string(remaining))
}
return chunks
}
func lastIndexRune(s []rune, r rune) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == r {
return i
}
}
return -1
}