Skip to content

Commit 6b0d66b

Browse files
huaaudioHua
authored andcommitted
Feat: Discord message length check and auto split (sipeed#143)
* feat: discord message auto split * make fmt * chore: remove failing discord_test.go --------- Co-authored-by: Hua <[email protected]>
1 parent 55ae7be commit 6b0d66b

1 file changed

Lines changed: 144 additions & 2 deletions

File tree

pkg/channels/discord.go

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"strings"
78
"time"
89

910
"github.com/bwmarrin/discordgo"
@@ -100,15 +101,156 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
100101
return fmt.Errorf("channel ID is empty")
101102
}
102103

103-
message := msg.Content
104+
runes := []rune(msg.Content)
105+
if len(runes) == 0 {
106+
return nil
107+
}
108+
109+
chunks := splitMessage(msg.Content, 1500) // Discord has a limit of 2000 characters per message, leave 500 for natural split e.g. code blocks
110+
111+
for _, chunk := range chunks {
112+
if err := c.sendChunk(ctx, channelID, chunk); err != nil {
113+
return err
114+
}
115+
}
116+
117+
return nil
118+
}
119+
120+
// splitMessage splits long messages into chunks, preserving code block integrity
121+
// Uses natural boundaries (newlines, spaces) and extends messages slightly to avoid breaking code blocks
122+
func splitMessage(content string, limit int) []string {
123+
var messages []string
124+
125+
for len(content) > 0 {
126+
if len(content) <= limit {
127+
messages = append(messages, content)
128+
break
129+
}
130+
131+
msgEnd := limit
132+
133+
// Find natural split point within the limit
134+
msgEnd = findLastNewline(content[:limit], 200)
135+
if msgEnd <= 0 {
136+
msgEnd = findLastSpace(content[:limit], 100)
137+
}
138+
if msgEnd <= 0 {
139+
msgEnd = limit
140+
}
141+
142+
// Check if this would end with an incomplete code block
143+
candidate := content[:msgEnd]
144+
unclosedIdx := findLastUnclosedCodeBlock(candidate)
145+
146+
if unclosedIdx >= 0 {
147+
// Message would end with incomplete code block
148+
// Try to extend to include the closing ``` (with some buffer)
149+
extendedLimit := limit + 500 // Allow 500 char buffer for code blocks
150+
if len(content) > extendedLimit {
151+
closingIdx := findNextClosingCodeBlock(content, msgEnd)
152+
if closingIdx > 0 && closingIdx <= extendedLimit {
153+
// Extend to include the closing ```
154+
msgEnd = closingIdx
155+
} else {
156+
// Can't find closing, split before the code block
157+
msgEnd = findLastNewline(content[:unclosedIdx], 200)
158+
if msgEnd <= 0 {
159+
msgEnd = findLastSpace(content[:unclosedIdx], 100)
160+
}
161+
if msgEnd <= 0 {
162+
msgEnd = unclosedIdx
163+
}
164+
}
165+
} else {
166+
// Remaining content fits within extended limit
167+
msgEnd = len(content)
168+
}
169+
}
170+
171+
if msgEnd <= 0 {
172+
msgEnd = limit
173+
}
174+
175+
messages = append(messages, content[:msgEnd])
176+
content = strings.TrimSpace(content[msgEnd:])
177+
}
178+
179+
return messages
180+
}
181+
182+
// findLastUnclosedCodeBlock finds the last opening ``` that doesn't have a closing ```
183+
// Returns the position of the opening ``` or -1 if all code blocks are complete
184+
func findLastUnclosedCodeBlock(text string) int {
185+
count := 0
186+
lastOpenIdx := -1
187+
188+
for i := 0; i < len(text); i++ {
189+
if i+2 < len(text) && text[i] == '`' && text[i+1] == '`' && text[i+2] == '`' {
190+
if count == 0 {
191+
lastOpenIdx = i
192+
}
193+
count++
194+
i += 2
195+
}
196+
}
197+
198+
// If odd number of ``` markers, last one is unclosed
199+
if count%2 == 1 {
200+
return lastOpenIdx
201+
}
202+
return -1
203+
}
204+
205+
// findNextClosingCodeBlock finds the next closing ``` starting from a position
206+
// Returns the position after the closing ``` or -1 if not found
207+
func findNextClosingCodeBlock(text string, startIdx int) int {
208+
for i := startIdx; i < len(text); i++ {
209+
if i+2 < len(text) && text[i] == '`' && text[i+1] == '`' && text[i+2] == '`' {
210+
return i + 3
211+
}
212+
}
213+
return -1
214+
}
215+
216+
// findLastNewline finds the last newline character within the last N characters
217+
// Returns the position of the newline or -1 if not found
218+
func findLastNewline(s string, searchWindow int) int {
219+
searchStart := len(s) - searchWindow
220+
if searchStart < 0 {
221+
searchStart = 0
222+
}
223+
for i := len(s) - 1; i >= searchStart; i-- {
224+
if s[i] == '\n' {
225+
return i
226+
}
227+
}
228+
return -1
229+
}
230+
231+
// findLastSpace finds the last space character within the last N characters
232+
// Returns the position of the space or -1 if not found
233+
func findLastSpace(s string, searchWindow int) int {
234+
searchStart := len(s) - searchWindow
235+
if searchStart < 0 {
236+
searchStart = 0
237+
}
238+
for i := len(s) - 1; i >= searchStart; i-- {
239+
if s[i] == ' ' || s[i] == '\t' {
240+
return i
241+
}
242+
}
243+
return -1
244+
}
104245

246+
func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content string) error {
105247
// 使用传入的 ctx 进行超时控制
106248
sendCtx, cancel := context.WithTimeout(ctx, sendTimeout)
107249
defer cancel()
108250

109251
done := make(chan error, 1)
110252
go func() {
111-
_, err := c.session.ChannelMessageSend(channelID, message)
253+
_, err := c.session.ChannelMessageSend(channelID, content)
112254
done <- err
113255
}()
114256

0 commit comments

Comments
 (0)