From 2e98b25dc2d95d5a2c01d58485ba475f55738d99 Mon Sep 17 00:00:00 2001 From: Freekers <1370857+Freekers@users.noreply.github.com> Date: Tue, 14 Jan 2025 22:06:59 +0100 Subject: [PATCH 1/2] Add support for Markdown formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I couldn’t get MarkdownV2 to work, likely due to issues with escaping certain characters. However, I did manage to get Markdown working, which is sufficient for my use case. --- plugin.go | 63 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/plugin.go b/plugin.go index 8f94539..63489c1 100644 --- a/plugin.go +++ b/plugin.go @@ -17,66 +17,67 @@ import ( // GetGotifyPluginInfo returns gotify plugin info func GetGotifyPluginInfo() plugin.Info { return plugin.Info{ - Version: "1.0", - Author: "Anh Bui", - Name: "Gotify 2 Telegram", - Description: "Telegram message fowarder for gotify", + Version: "1.0", + Author: "Anh Bui", + Name: "Gotify 2 Telegram", + Description: "Telegram message fowarder for gotify", ModulePath: "https://github.com/anhbh310/gotify2telegram", } } // Plugin is the plugin instance type Plugin struct { - ws *websocket.Conn; - msgHandler plugin.MessageHandler; - debugLogger *log.Logger; - chatid string; - telegram_bot_token string; - gotify_host string; + ws *websocket.Conn + msgHandler plugin.MessageHandler + debugLogger *log.Logger + chatid string + telegram_bot_token string + gotify_host string } type GotifyMessage struct { - Id uint32; - Appid uint32; - Message string; - Title string; - Priority uint32; - Date string; + Id uint32 + Appid uint32 + Message string + Title string + Priority uint32 + Date string } type Payload struct { - ChatID string `json:"chat_id"` - Text string `json:"text"` + ChatID string `json:"chat_id"` + Text string `json:"text"` + ParseMode string `json:"parse_mode"` } func (p *Plugin) send_msg_to_telegram(msg string) { step_size := 4090 sending_message := "" - for i:=0; i Date: Sun, 19 Jan 2025 20:55:52 +0100 Subject: [PATCH 2/2] Add support for MarkdownV2 formatting --- README.md | 9 ++- plugin.go | 160 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 127 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 7e0c0e3..c4b95c2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Gotify 2 Telegram -This Gotify plugin forwards all received messages to Telegram through the Telegram bot. +This Gotify plugin forwards all received messages to Telegram through the Telegram bot with support for MarkdownV2 formatting. ## Prerequisite - A Telegram bot, bot token, and chat ID from bot conversation. You can get that information by following this [blog](https://medium.com/linux-shots/setup-telegram-bot-to-get-alert-notifications-90be7da4444). @@ -36,6 +36,13 @@ This Gotify plugin forwards all received messages to Telegram through the Telegr - In the BotFather chat, list your created bots and select the respective bot for which you want to change the Group Privacy setting. - Turn off the Group Privacy setting. +## Please note +I am not a developer. In fact, the implementation for MarkdownV2 formatting support was entirely written by Claude AI (as ChatGPT was unable to figure it out). I have tested it and it works perfectly for my use case: forwarding notifications from my Proxmox instance to Telegram. + +The code properly handles large messages by intelligently splitting them while preserving MarkdownV2 formatting. It also splits messages at natural boundaries (such as newlines and words) whenever possible to avoid exceeding Telegram's message size limit. Each part of a split message is labeled with "(n/N)" at the top, making it easier to track the message parts. Additionally, the code includes a small delay between messages to prevent hitting Telegram's rate limits. + +I have built the plugin for Gotify V2.6.1 for AMD64, ARM7, and ARM64 platforms and added them as a release to this fork. + ## Appendix Mandatory secrets. diff --git a/plugin.go b/plugin.go index 63489c1..c409e67 100644 --- a/plugin.go +++ b/plugin.go @@ -3,11 +3,13 @@ package main import ( "bytes" "encoding/json" + "fmt" "io" "log" "net/http" "net/http/httputil" "os" + "strings" "time" "github.com/gotify/plugin-api" @@ -17,22 +19,22 @@ import ( // GetGotifyPluginInfo returns gotify plugin info func GetGotifyPluginInfo() plugin.Info { return plugin.Info{ - Version: "1.0", - Author: "Anh Bui", - Name: "Gotify 2 Telegram", + Version: "1.2", + Author: "Anh Bui", + Name: "Gotify 2 Telegram", Description: "Telegram message fowarder for gotify", - ModulePath: "https://github.com/anhbh310/gotify2telegram", + ModulePath: "https://github.com/anhbh310/gotify2telegram", } } // Plugin is the plugin instance type Plugin struct { - ws *websocket.Conn - msgHandler plugin.MessageHandler - debugLogger *log.Logger - chatid string + ws *websocket.Conn + msgHandler plugin.MessageHandler + debugLogger *log.Logger + chatid string telegram_bot_token string - gotify_host string + gotify_host string } type GotifyMessage struct { @@ -50,50 +52,129 @@ type Payload struct { ParseMode string `json:"parse_mode"` } -func (p *Plugin) send_msg_to_telegram(msg string) { - step_size := 4090 - sending_message := "" +// escapeMarkdownV2 escapes special characters for Telegram's MarkdownV2 format +func escapeMarkdownV2(text string) string { + // Special characters that need to be escaped in MarkdownV2 + specialChars := []string{"_", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"} + + // Don't escape characters inside code blocks + parts := strings.Split(text, "```") + for i := 0; i < len(parts); i++ { + if i%2 == 0 { // Outside code block + for _, char := range specialChars { + parts[i] = strings.ReplaceAll(parts[i], char, "\\"+char) + } + } + } + + return strings.Join(parts, "```") +} + +// splitMessage splits a message into chunks while preserving code blocks +func (p *Plugin) splitMessage(msg string, maxSize int) []string { + if len(msg) <= maxSize { + return []string{msg} + } + + var chunks []string + lines := strings.Split(msg, "\n") + currentChunk := "" + inCodeBlock := false + + for _, line := range lines { + // Check for code block markers + if strings.HasPrefix(strings.TrimSpace(line), "```") { + inCodeBlock = !inCodeBlock + } + + // If adding this line would exceed the limit + if len(currentChunk)+len(line)+1 > maxSize { + // If we're in a code block, close it in current chunk and reopen in next + if inCodeBlock { + currentChunk += "```" + chunks = append(chunks, currentChunk) + currentChunk = "```\n" + line + "\n" + } else { + chunks = append(chunks, currentChunk) + currentChunk = line + "\n" + } + } else { + currentChunk += line + "\n" + } + } + + // Add the final chunk if there's content + if len(currentChunk) > 0 { + chunks = append(chunks, currentChunk) + } + + // Trim chunks and ensure code blocks are properly closed + for i := range chunks { + chunks[i] = strings.TrimSpace(chunks[i]) + + // Count backticks to check if code block is properly closed + count := strings.Count(chunks[i], "```") + if count%2 == 1 { + chunks[i] += "\n```" + } + } + + return chunks +} - for i := 0; i < len(msg); i += step_size { - if i+step_size < len(msg) { - sending_message = msg[i : i+step_size] +func (p *Plugin) send_msg_to_telegram(msg string) { + // Telegram's maximum message length is 4096 characters + // We use 3800 to leave room for formatting + const maxMessageSize = 3800 + + // Split the message into chunks + messageChunks := p.splitMessage(msg, maxMessageSize) + + totalChunks := len(messageChunks) + + for i, chunk := range messageChunks { + // Add part number for multi-part messages + var messageText string + if totalChunks > 1 { + messageText = fmt.Sprintf("(%d/%d)\n%s", i+1, totalChunks, chunk) } else { - sending_message = msg[i:len(msg)] + messageText = chunk } + // Escape markdown characters + escapedText := escapeMarkdownV2(messageText) + data := Payload{ ChatID: p.chatid, - Text: sending_message, - ParseMode: "Markdown", + Text: escapedText, + ParseMode: "MarkdownV2", // Use MarkdownV2 instead of Markdown } + payloadBytes, err := json.Marshal(data) if err != nil { - p.debugLogger.Println("Create JSON failed") - return + p.debugLogger.Printf("Create JSON failed: %v\n", err) + continue } + body := bytes.NewBuffer(payloadBytes) - // For future debugging - backup_body := bytes.NewBuffer(body.Bytes()) + backup_body := bytes.NewBuffer(payloadBytes) req, err := http.NewRequest("POST", "https://api.telegram.org/bot"+p.telegram_bot_token+"/sendMessage", body) if err != nil { - p.debugLogger.Println("Create request failed") - return + p.debugLogger.Printf("Create request failed: %v\n", err) + continue } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) - if err != nil { p.debugLogger.Printf("Send request failed: %v\n", err) - return + continue } - p.debugLogger.Println("HTTP request was sent successfully") if resp.StatusCode == http.StatusOK { - p.debugLogger.Println("The message was forwarded successfully to Telegram") + p.debugLogger.Printf("Part %d/%d was forwarded successfully to Telegram\n", i+1, totalChunks) } else { - // Log info for debugging p.debugLogger.Println("============== Request ==============") pretty_print, err := httputil.DumpRequest(req, true) if err != nil { @@ -105,10 +186,14 @@ func (p *Plugin) send_msg_to_telegram(msg string) { p.debugLogger.Println("============== Response ==============") bodyBytes, err := io.ReadAll(resp.Body) p.debugLogger.Printf("%v\n", string(bodyBytes)) - } - defer resp.Body.Close() + resp.Body.Close() + + // Add a small delay between messages to avoid rate limiting + if totalChunks > 1 && i < totalChunks-1 { + time.Sleep(500 * time.Millisecond) + } } } @@ -120,7 +205,7 @@ func (p *Plugin) connect_websocket() { break } p.debugLogger.Printf("Cannot connect to websocket: %v\n", err) - time.Sleep(5) + time.Sleep(5 * time.Second) } p.debugLogger.Println("WebSocket connected successfully, ready for forwarding") } @@ -137,7 +222,7 @@ func (p *Plugin) get_websocket_msg(url string, token string) { for { msg := &GotifyMessage{} if p.ws == nil { - time.Sleep(3) + time.Sleep(3 * time.Second) continue } err := p.ws.ReadJSON(msg) @@ -150,8 +235,6 @@ func (p *Plugin) get_websocket_msg(url string, token string) { } } -// SetMessageHandler implements plugin.Messenger -// Invoked during initialization func (p *Plugin) SetMessageHandler(h plugin.MessageHandler) { p.debugLogger = log.New(os.Stdout, "Gotify 2 Telegram: ", log.Lshortfile) p.msgHandler = h @@ -162,7 +245,6 @@ func (p *Plugin) Enable() error { return nil } -// Disable implements plugin.Plugin func (p *Plugin) Disable() error { if p.ws != nil { p.ws.Close() @@ -170,14 +252,10 @@ func (p *Plugin) Disable() error { return nil } -// NewGotifyPluginInstance creates a plugin instance for a user context. func NewGotifyPluginInstance(ctx plugin.UserContext) plugin.Plugin { return &Plugin{} } func main() { panic("this should be built as go plugin") - // For testing - // p := &Plugin{nil, nil, "", "", ""} - // p.get_websocket_msg(os.Getenv("GOTIFY_HOST"), os.Getenv("GOTIFY_CLIENT_TOKEN")) -} +} \ No newline at end of file