@@ -7,9 +7,11 @@ import (
77 "html/template"
88 "log"
99 "net/http"
10+ "regexp"
1011 "strconv"
1112 "strings"
1213 "time"
14+ "io"
1315)
1416
1517func debug (s string , x ... interface {}) {
@@ -95,47 +97,125 @@ type DiscordEmbedFooter struct {
9597
9698// format_discord_embeds builds one or more embeds from GotifyMessage.
9799// It will split long descriptions into multiple embeds if necessary.
98- func format_discord_embeds (msg * GotifyMessage ) []DiscordEmbed {
99- title := msg .Title
100- body := msg .Message
101-
102- // decide color by priority (example mapping)
103- color := 0x2ECC71 // green default
104- switch msg .Priority {
100+ // helper: choose color based on priority
101+ func discordColorForPriority (p uint32 ) int {
102+ switch p {
105103 case 5 :
106- color = 0xFF0000 // red
104+ return 0xFF0000
107105 case 4 :
108- color = 0xFFA500 // orange
106+ return 0xFFA500
109107 case 3 :
110- color = 0xFFFF00 // yellow
108+ return 0xFFFF00
111109 case 2 :
112- color = 0x3498DB // blue
110+ return 0x3498DB
111+ default :
112+ return 0x2ECC71
113113 }
114+ }
114115
115- // Discord embed description limit is 4096 chars; split into chunks safely
116- maxDesc := 3800
117- runes := []rune (body )
118- var embeds []DiscordEmbed
119- for i := 0 ; i < len (runes ); i += maxDesc {
120- end := i + maxDesc
121- if end > len (runes ) {
122- end = len (runes )
116+ // helper: compute allowed description rune count per embed
117+ func allowedDescFor (title , footer string ) int {
118+ const maxPerEmbed = 6000
119+ const overheadMargin = 200
120+ titleLen := len ([]rune (title ))
121+ footerLen := len ([]rune (footer ))
122+ allowed := maxPerEmbed - titleLen - footerLen - overheadMargin
123+ if allowed < 200 {
124+ allowed = 200
125+ }
126+ return allowed
127+ }
128+
129+ // parse message into segments of code blocks and plain text
130+ type segment struct { isCode bool ; lang , text string }
131+
132+ func parseSegments (body string ) []segment {
133+ codeRe := regexp .MustCompile ("(?s)```.*?```" )
134+ idxs := codeRe .FindAllStringIndex (body , - 1 )
135+ segs := []segment {}
136+ last := 0
137+ for _ , id := range idxs {
138+ if id [0 ] > last {
139+ segs = append (segs , segment {isCode : false , text : body [last :id [0 ]]})
123140 }
124- desc := string (runes [i :end ])
125- embed := DiscordEmbed {
126- Title : title ,
127- Description : desc ,
128- Color : color ,
129- Timestamp : msg .Date ,
130- Footer : & DiscordEmbedFooter {
131- Text : fmt .Sprintf ("Gotify Id: %d" , msg .Id ),
132- },
141+ block := body [id [0 ]:id [1 ]]
142+ inner := strings .TrimPrefix (strings .TrimSuffix (block , "```" ), "```" )
143+ lang := ""
144+ code := inner
145+ if n := strings .Index (inner , "\n " ); n >= 0 {
146+ lang = strings .TrimSpace (inner [:n ])
147+ code = inner [n + 1 :]
133148 }
134- // For subsequent chunks, omit the title to avoid repetition
135- if i > 0 {
136- embed .Title = ""
149+ segs = append (segs , segment {isCode : true , lang : lang , text : code })
150+ last = id [1 ]
151+ }
152+ if last < len (body ) {
153+ segs = append (segs , segment {isCode : false , text : body [last :]})
154+ }
155+ return segs
156+ }
157+
158+ // build embeds from segments while preserving code blocks
159+ func buildEmbedsFromSegments (title string , segs []segment , footerText string , priority uint32 ) []DiscordEmbed {
160+ color := discordColorForPriority (priority )
161+ allowed := allowedDescFor (title , footerText )
162+ var embeds []DiscordEmbed
163+ cur := []rune {}
164+ hasTitle := true
165+ flush := func () {
166+ t := ""
167+ if hasTitle {
168+ t = title
169+ }
170+ embeds = append (embeds , DiscordEmbed {Title : t , Description : string (cur ), Color : color , Timestamp : "" , Footer : & DiscordEmbedFooter {Text : footerText }})
171+ hasTitle = false
172+ cur = []rune {}
173+ allowed = allowedDescFor ("" , footerText )
174+ }
175+
176+ for _ , s := range segs {
177+ if s .isCode {
178+ opener := "```"
179+ if s .lang != "" { opener += s .lang + "\n " } else { opener += "\n " }
180+ closer := "```"
181+ r := []rune (s .text )
182+ i := 0
183+ for i < len (r ) {
184+ remaining := allowed - len (cur ) - len ([]rune (opener )) - len ([]rune (closer ))
185+ if remaining <= 0 { flush (); continue }
186+ take := remaining
187+ if take > len (r )- i { take = len (r )- i }
188+ frag := string (r [i : i + take ])
189+ cur = append (cur , []rune (opener + frag + "\n " + closer )... )
190+ i += take
191+ if i < len (r ) { flush () }
192+ }
193+ } else {
194+ r := []rune (s .text )
195+ i := 0
196+ for i < len (r ) {
197+ remaining := allowed - len (cur )
198+ if remaining <= 0 { flush (); continue }
199+ take := remaining
200+ if take > len (r )- i { take = len (r )- i }
201+ cur = append (cur , r [i :i + take ]... )
202+ i += take
203+ if i < len (r ) { flush () }
204+ }
137205 }
138- embeds = append (embeds , embed )
206+ }
207+ if len (cur ) > 0 || len (embeds ) == 0 { flush () }
208+ return embeds
209+ }
210+
211+ func format_discord_embeds (msg * GotifyMessage ) []DiscordEmbed {
212+ title := msg .Title
213+ footerText := fmt .Sprintf ("Gotify Id: %d" , msg .Id )
214+ segs := parseSegments (msg .Message )
215+ embeds := buildEmbedsFromSegments (title , segs , footerText , msg .Priority )
216+ // set timestamps to message date
217+ for i := range embeds {
218+ embeds [i ].Timestamp = msg .Date
139219 }
140220 return embeds
141221}
@@ -167,20 +247,19 @@ func send_msg_to_discord(embeds []DiscordEmbed, webhookURL string, username stri
167247 log .Println ("Create discord json false" )
168248 return
169249 }
170- body := bytes .NewReader (payloadBytes )
171-
172- req , err := http .NewRequest ("POST" , webhookURL , body )
173- if err != nil {
174- log .Println ("Create discord request false" )
175- return
176- }
177- req .Header .Set ("Content-Type" , "application/json" )
178250
179251 // Retry loop with exponential backoff and special handling for 429
180252 var resp * http.Response
181253 var attempt int
182254 maxRetries := 5
183255 for attempt = 0 ; attempt <= maxRetries ; attempt ++ {
256+ req , err := http .NewRequest ("POST" , webhookURL , bytes .NewReader (payloadBytes ))
257+ if err != nil {
258+ log .Println ("Create discord request false" )
259+ return
260+ }
261+ req .Header .Set ("Content-Type" , "application/json" )
262+
184263 resp , err = client .Do (req )
185264 if err != nil {
186265 // network error, retry
@@ -192,7 +271,10 @@ func send_msg_to_discord(embeds []DiscordEmbed, webhookURL string, username stri
192271 // handle 429 (rate limit)
193272 if resp .StatusCode == 429 {
194273 ra := resp .Header .Get ("Retry-After" )
274+ // read and log a small part of body for debugging
275+ b , _ := io .ReadAll (io .LimitReader (resp .Body , 1024 ))
195276 resp .Body .Close ()
277+ log .Printf ("discord: rate limited (429). Retry-After=%s; body=%s" , ra , strings .TrimSpace (string (b )))
196278 var wait time.Duration
197279 if ra != "" {
198280 // try seconds first
@@ -215,24 +297,33 @@ func send_msg_to_discord(embeds []DiscordEmbed, webhookURL string, username stri
215297
216298 // retry on 5xx server errors
217299 if resp .StatusCode >= 500 && resp .StatusCode < 600 {
300+ // read part of body for debug
301+ b , _ := io .ReadAll (io .LimitReader (resp .Body , 1024 ))
218302 resp .Body .Close ()
303+ log .Printf ("discord: server error %d, body=%s" , resp .StatusCode , strings .TrimSpace (string (b )))
219304 backoff := time .Duration (1 << attempt ) * 500 * time .Millisecond
220305 time .Sleep (backoff )
221306 continue
222307 }
223308
224- // other status codes (2xx or 4xx) - do not retry
225- resp .Body .Close ()
309+ // other status codes (2xx success or 4xx client error) - do not retry
310+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
311+ b , _ := io .ReadAll (io .LimitReader (resp .Body , 2048 ))
312+ resp .Body .Close ()
313+ log .Printf ("discord: unexpected status %d. body=%s" , resp .StatusCode , strings .TrimSpace (string (b )))
314+ } else {
315+ resp .Body .Close ()
316+ }
226317 break
227318 }
228319
229320 if err != nil {
230- fmt .Printf ("Send discord request false : %v\n " , err )
321+ log .Printf ("discord: request failed : %v" , err )
231322 return
232323 }
233324 // if we exhausted retries, log and continue to next batch
234325 if attempt > maxRetries {
235- fmt .Printf ("Send discord request failed after %d attempts\n " , maxRetries )
326+ log .Printf ("discord: send failed after %d attempts" , maxRetries )
236327 }
237328 }
238329}
0 commit comments