Split long messages when send to telegram
This commit is contained in:
@@ -6,6 +6,10 @@ import (
|
|||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TextLengthLimit = 4000
|
||||||
|
)
|
||||||
|
|
||||||
type TelegramMessageSender struct {
|
type TelegramMessageSender struct {
|
||||||
bot *tgbotapi.BotAPI
|
bot *tgbotapi.BotAPI
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
@@ -24,6 +28,32 @@ func NewTelegramMessageSender(botToken string, logger *slog.Logger) (*TelegramMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *TelegramMessageSender) Send(text string, chatId int64, replyToMessageId *int) error {
|
func (s *TelegramMessageSender) Send(text string, chatId int64, replyToMessageId *int) error {
|
||||||
|
// If message is short enough, send it directly
|
||||||
|
if len([]rune(text)) <= TextLengthLimit {
|
||||||
|
return s.sendSingleMessage(text, chatId, replyToMessageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split long message into parts
|
||||||
|
parts := s.splitMessageByWords(text, TextLengthLimit)
|
||||||
|
|
||||||
|
// Send each part
|
||||||
|
for i, part := range parts {
|
||||||
|
var replyId *int
|
||||||
|
// Only use replyToMessageId for the first part
|
||||||
|
if i == 0 {
|
||||||
|
replyId = replyToMessageId
|
||||||
|
}
|
||||||
|
err := s.sendSingleMessage(part, chatId, replyId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendSingleMessage sends a single message
|
||||||
|
func (s *TelegramMessageSender) sendSingleMessage(text string, chatId int64, replyToMessageId *int) error {
|
||||||
resultMsg := tgbotapi.NewMessage(chatId, text)
|
resultMsg := tgbotapi.NewMessage(chatId, text)
|
||||||
if replyToMessageId != nil {
|
if replyToMessageId != nil {
|
||||||
resultMsg.ReplyToMessageID = *replyToMessageId
|
resultMsg.ReplyToMessageID = *replyToMessageId
|
||||||
|
62
internal/adapter/telegram/split.go
Normal file
62
internal/adapter/telegram/split.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
// splitMessageByWords splits a message into parts of maxLen UTF-8 characters
|
||||||
|
// splitting by words to avoid cutting words in the middle
|
||||||
|
func (s *TelegramMessageSender) splitMessageByWords(text string, maxLen int) []string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
// If text is already short enough, return as is
|
||||||
|
if len([]rune(text)) <= maxLen {
|
||||||
|
return []string{text}
|
||||||
|
}
|
||||||
|
|
||||||
|
runes := []rune(text)
|
||||||
|
|
||||||
|
for len(runes) > 0 {
|
||||||
|
// Determine the end position for this part
|
||||||
|
end := len(runes)
|
||||||
|
if end > maxLen {
|
||||||
|
end = maxLen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find a good split point (word boundary)
|
||||||
|
splitPoint := end
|
||||||
|
for i := end - 1; i > end-20 && i > 0; i-- { // Look back up to 20 characters
|
||||||
|
// Check if this is a good split point (after a space)
|
||||||
|
if runes[i] == ' ' {
|
||||||
|
splitPoint = i + 1 // Include the space in the previous part
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we couldn't find a good split point, just split at maxLen
|
||||||
|
if splitPoint == end && end == maxLen {
|
||||||
|
// Check if we're in the middle of a word
|
||||||
|
if end < len(runes) && runes[end] != ' ' && runes[end-1] != ' ' {
|
||||||
|
// Try to find a split point going forward
|
||||||
|
for i := end; i < len(runes) && i < end+20; i++ {
|
||||||
|
if runes[i] == ' ' {
|
||||||
|
splitPoint = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no good split point, use the original end
|
||||||
|
if splitPoint > len(runes) {
|
||||||
|
splitPoint = len(runes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this part
|
||||||
|
parts = append(parts, string(runes[:splitPoint]))
|
||||||
|
|
||||||
|
// Move to the next part
|
||||||
|
if splitPoint >= len(runes) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
runes = runes[splitPoint:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
125
internal/adapter/telegram/split_test.go
Normal file
125
internal/adapter/telegram/split_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTelegramMessageSender_splitMessageByWords(t *testing.T) {
|
||||||
|
sender := &TelegramMessageSender{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
maxLen int
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Short text should return as is",
|
||||||
|
text: "Привет мир",
|
||||||
|
maxLen: 25,
|
||||||
|
expected: []string{
|
||||||
|
"Привет мир",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text exactly at limit",
|
||||||
|
text: "Это тестовый текст который ровно соответствует лимиту",
|
||||||
|
maxLen: 35,
|
||||||
|
expected: []string{
|
||||||
|
"Это тестовый текст который ровно ",
|
||||||
|
"соответствует ",
|
||||||
|
"лимиту",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text with word boundaries",
|
||||||
|
text: "Это очень длинный текст для проверки работы функции разделения сообщения",
|
||||||
|
maxLen: 25,
|
||||||
|
expected: []string{
|
||||||
|
"Это очень длинный текст ",
|
||||||
|
"для проверки работы ",
|
||||||
|
"функции разделения ",
|
||||||
|
"сообщения",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text with long words",
|
||||||
|
text: "Этот текст содержит оченьдлинноеслово которое не должно быть разбито",
|
||||||
|
maxLen: 20,
|
||||||
|
expected: []string{
|
||||||
|
"Этот текст содержит ",
|
||||||
|
"оченьдлинноеслово ",
|
||||||
|
"которое не должно ",
|
||||||
|
"быть ",
|
||||||
|
"разбито",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text with multiple spaces",
|
||||||
|
text: "Этот текст имеет много пробелов",
|
||||||
|
maxLen: 20,
|
||||||
|
expected: []string{
|
||||||
|
"Этот текст ",
|
||||||
|
"имеет много ",
|
||||||
|
"пробелов",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text with Russian characters and punctuation",
|
||||||
|
text: "Привет! Как дела? Это тестовая строка для проверки работы функции.",
|
||||||
|
maxLen: 25,
|
||||||
|
expected: []string{
|
||||||
|
"Привет! Как дела? Это ",
|
||||||
|
"тестовая строка для ",
|
||||||
|
"проверки работы ",
|
||||||
|
"функции.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Single word longer than maxLen",
|
||||||
|
text: "Некотороедлинноеслово",
|
||||||
|
maxLen: 10,
|
||||||
|
expected: []string{
|
||||||
|
"Некотороед",
|
||||||
|
"линноеслов",
|
||||||
|
"о",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text with mixed Russian and English",
|
||||||
|
text: "Привет Hello мир World текст для проверки",
|
||||||
|
maxLen: 20,
|
||||||
|
expected: []string{
|
||||||
|
"Привет Hello мир ",
|
||||||
|
"World текст для ",
|
||||||
|
"проверки",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text with special characters",
|
||||||
|
text: "Тест с символами: @#$%^&*()_+-=[]{}|;':\",./<>?",
|
||||||
|
maxLen: 25,
|
||||||
|
expected: []string{
|
||||||
|
"Тест с символами: ",
|
||||||
|
"@#$%^&*()_+-=[]{}|;':\",./",
|
||||||
|
"<>?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := sender.splitMessageByWords(tt.text, tt.maxLen)
|
||||||
|
if len(result) != len(tt.expected) {
|
||||||
|
t.Errorf("splitMessageByWords() length = %d, want %d", len(result), len(tt.expected))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, expectedPart := range tt.expected {
|
||||||
|
if result[i] != expectedPart {
|
||||||
|
t.Errorf("splitMessageByWords() part %d = %q, want %q", i, result[i], expectedPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user