- контекст проложен от воркера и обоих входов до внешних вызовов: ffmpeg и ffprobe заводятся через exec.CommandContext, SpeechKit и Object Storage принимают ctx вместо context.Background, скачивание записи идёт запросом с контекстом. Прежде остановка сервиса не доходила до чужой работы вовсе - прерванный шаг приговора не выносит: убитый по контексту ffmpeg отдаёт «signal: killed», от настоящего отказа неотличимо ни типом, ни errors.Is, и различает их только ctx.Err(). Задача остаётся на повтор, попытку не тратит и отправителю о несуществующем сбое не сообщает; воркер не считает остановку отказом, а задача не забирается вовсе, если нас уже остановили - клиента Bot API заводит единая точка internal/adapter/telegram: токен стоит в пути каждого обращения, а http.Client кладёт адрес в *url.Error целиком. Чистка на месте употребления закрывала один вызов из пяти — теперь свой Do чистит отказ, подменённый логгер вычищает токен из строк самой библиотеки, а транспорт бота токена не получает вовсе - принятие операции распознавания защищено от отмены своим пределом: SpeechKit мог её принять и начать считать деньги, а потерянный идентификатор заставил бы повтор оплатить ту же запись второй раз - приём по HTTP доводит запись до задачи независимо от отправителя: на контексте запроса один обрыв соединения терял полностью загруженную запись - ответ Telegram с не-2xx кодом больше не становится записью: прежде тело отказа доезжало до хранилища и умирало на ffprobe, уводя диагностику
70 lines
1.7 KiB
Go
70 lines
1.7 KiB
Go
package telegram
|
|
|
|
import (
|
|
"log/slog"
|
|
|
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
)
|
|
|
|
const (
|
|
TextLengthLimit = 4000
|
|
)
|
|
|
|
type TelegramMessageSender struct {
|
|
bot *tgbotapi.BotAPI
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewTelegramMessageSender(botToken string, logger *slog.Logger) (*TelegramMessageSender, error) {
|
|
// Клиент заводится единой точкой: её отказ не несёт токена, а отказ
|
|
// конструктора несёт — `NewBotAPI` зовёт `getMe`.
|
|
bot, err := NewBot(botToken, logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &TelegramMessageSender{
|
|
bot: bot,
|
|
logger: logger,
|
|
}, nil
|
|
}
|
|
|
|
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)
|
|
if replyToMessageId != nil {
|
|
resultMsg.ReplyToMessageID = *replyToMessageId
|
|
}
|
|
_, err := s.bot.Send(resultMsg)
|
|
if err != nil {
|
|
s.logger.Error("Failed to send message to tg bot", "error", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|