345 lines
12 KiB
Go
345 lines
12 KiB
Go
package tg
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"log/slog"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.vakhrushev.me/av/transcriber/internal/contract"
|
||
"git.vakhrushev.me/av/transcriber/internal/service"
|
||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||
)
|
||
|
||
type TelegramController struct {
|
||
// deps
|
||
bot *tgbotapi.BotAPI
|
||
transcribeService *service.TranscribeService
|
||
jobRepo contract.TranscriptJobRepository
|
||
logger *slog.Logger
|
||
// params
|
||
updateTimeout int
|
||
}
|
||
|
||
type TelegramConfig struct {
|
||
BotToken string
|
||
UpdateTimeout int
|
||
}
|
||
|
||
func NewTelegramController(
|
||
config TelegramConfig,
|
||
transcribeService *service.TranscribeService,
|
||
jobRepo contract.TranscriptJobRepository,
|
||
logger *slog.Logger,
|
||
) (*TelegramController, error) {
|
||
botToken := config.BotToken
|
||
if botToken == "" {
|
||
return nil, &EmptyBotTokenError{}
|
||
}
|
||
|
||
bot, err := tgbotapi.NewBotAPI(botToken)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
controller := &TelegramController{
|
||
bot: bot,
|
||
transcribeService: transcribeService,
|
||
jobRepo: jobRepo,
|
||
logger: logger,
|
||
updateTimeout: config.UpdateTimeout,
|
||
}
|
||
|
||
return controller, nil
|
||
}
|
||
|
||
func (c *TelegramController) Start() {
|
||
c.logger.Info("Telegram bot started", "username", c.bot.Self.UserName)
|
||
|
||
u := tgbotapi.NewUpdate(0)
|
||
u.Timeout = c.updateTimeout
|
||
|
||
updates := c.bot.GetUpdatesChan(u)
|
||
|
||
for update := range updates {
|
||
if update.Message == nil { // ignore any non-Message updates
|
||
continue
|
||
}
|
||
|
||
author := update.Message.From.String()
|
||
c.logger.Info("New incoming message", "author", author)
|
||
|
||
// Handle commands
|
||
if update.Message.IsCommand() {
|
||
// Extract the command from the Message
|
||
switch update.Message.Command() {
|
||
case "start":
|
||
c.handleStartCommand(update.Message)
|
||
case "help":
|
||
c.handleHelpCommand(update.Message)
|
||
}
|
||
continue
|
||
}
|
||
|
||
// Handle audio messages and files
|
||
if update.Message.Audio != nil {
|
||
c.handleAudioMessage(update.Message)
|
||
} else if update.Message.Voice != nil {
|
||
c.handleVoiceMessage(update.Message)
|
||
} else if update.Message.Document != nil {
|
||
c.handleDocumentMessage(update.Message)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (c *TelegramController) Stop() {
|
||
c.bot.StopReceivingUpdates()
|
||
}
|
||
|
||
func (c *TelegramController) handleStartCommand(message *tgbotapi.Message) {
|
||
msg := tgbotapi.NewMessage(message.Chat.ID, "Привет! Я бот для расшифровки аудиосообщений. Отправь мне голосовое сообщение или аудиофайл, и я пришлю тебе текст.")
|
||
msg.ReplyToMessageID = message.MessageID
|
||
|
||
c.bot.Send(msg)
|
||
}
|
||
|
||
func (c *TelegramController) handleHelpCommand(message *tgbotapi.Message) {
|
||
helpText := `Я бот для расшифровки аудиосообщений и аудиофайлов.
|
||
|
||
Просто отправь мне:
|
||
- Голосовое сообщение
|
||
- Аудиофайл (mp3, wav, ogg и др.)
|
||
|
||
Я пришлю тебе текст расшифровки.
|
||
|
||
Команды:
|
||
/start - Начало работы с ботом
|
||
/help - Показать эту справку`
|
||
|
||
msg := tgbotapi.NewMessage(message.Chat.ID, helpText)
|
||
msg.ReplyToMessageID = message.MessageID
|
||
|
||
c.bot.Send(msg)
|
||
}
|
||
|
||
func (c *TelegramController) handleAudioMessage(message *tgbotapi.Message) {
|
||
// Отправляем сообщение о начале обработки
|
||
progressMsg := tgbotapi.NewMessage(message.Chat.ID, "Обрабатываю аудиофайл...")
|
||
progressMsg.ReplyToMessageID = message.MessageID
|
||
sentProgressMsg, err := c.bot.Send(progressMsg)
|
||
if err != nil {
|
||
c.logger.Error("Failed to send progress message", "error", err)
|
||
return
|
||
}
|
||
|
||
// Скачиваем файл
|
||
fileReader, fileName, err := c.downloadAudioFile(message.Audio.FileID)
|
||
if err != nil {
|
||
c.logger.Error("Failed to download audio file", "error", err)
|
||
errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при скачивании аудиофайла. Попробуйте еще раз.")
|
||
c.bot.Send(errorMsg)
|
||
return
|
||
}
|
||
defer fileReader.Close()
|
||
|
||
// Обрабатываем файл
|
||
job, err := c.transcribeService.CreateTranscribeJob(fileReader, fileName)
|
||
if err != nil {
|
||
c.logger.Error("Failed to create transcribe job", "error", err)
|
||
errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при создании задачи на расшифровку. Попробуйте еще раз.")
|
||
c.bot.Send(errorMsg)
|
||
return
|
||
}
|
||
|
||
// Отправляем сообщение об успешном создании задачи
|
||
successMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("Задача на расшифровку создана. ID задачи: %s", job.Id))
|
||
successMsg.ReplyToMessageID = message.MessageID
|
||
c.bot.Send(successMsg)
|
||
|
||
// Отправляем результат расшифровки (асинхронно)
|
||
go c.sendTranscriptionResult(message.Chat.ID, job.Id, sentProgressMsg.MessageID)
|
||
}
|
||
|
||
func (c *TelegramController) handleVoiceMessage(message *tgbotapi.Message) {
|
||
// Отправляем сообщение о начале обработки
|
||
progressMsg := tgbotapi.NewMessage(message.Chat.ID, "Обрабатываю голосовое сообщение...")
|
||
progressMsg.ReplyToMessageID = message.MessageID
|
||
sentProgressMsg, err := c.bot.Send(progressMsg)
|
||
if err != nil {
|
||
c.logger.Error("Failed to send progress message", "error", err)
|
||
return
|
||
}
|
||
|
||
// Скачиваем файл
|
||
fileReader, fileName, err := c.downloadAudioFile(message.Voice.FileID)
|
||
if err != nil {
|
||
c.logger.Error("Failed to download voice file", "error", err)
|
||
errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при скачивании голосового сообщения. Попробуйте еще раз.")
|
||
c.bot.Send(errorMsg)
|
||
return
|
||
}
|
||
defer fileReader.Close()
|
||
|
||
// Обрабатываем файл
|
||
job, err := c.transcribeService.CreateTranscribeJob(fileReader, fileName)
|
||
if err != nil {
|
||
c.logger.Error("Failed to create transcribe job", "error", err)
|
||
errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при создании задачи на расшифровку. Попробуйте еще раз.")
|
||
c.bot.Send(errorMsg)
|
||
return
|
||
}
|
||
|
||
// Отправляем сообщение об успешном создании задачи
|
||
successMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("Задача на расшифровку создана. ID задачи: %s", job.Id))
|
||
successMsg.ReplyToMessageID = message.MessageID
|
||
c.bot.Send(successMsg)
|
||
|
||
// Отправляем результат расшифровки (асинхронно)
|
||
go c.sendTranscriptionResult(message.Chat.ID, job.Id, sentProgressMsg.MessageID)
|
||
}
|
||
|
||
func (c *TelegramController) handleDocumentMessage(message *tgbotapi.Message) {
|
||
// Проверяем, является ли документ аудиофайлом
|
||
if !c.isAudioDocument(message.Document) {
|
||
return
|
||
}
|
||
|
||
// Отправляем сообщение о начале обработки
|
||
progressMsg := tgbotapi.NewMessage(message.Chat.ID, "Обрабатываю аудиофайл...")
|
||
progressMsg.ReplyToMessageID = message.MessageID
|
||
sentProgressMsg, err := c.bot.Send(progressMsg)
|
||
if err != nil {
|
||
c.logger.Error("Failed to send progress message", "error", err)
|
||
return
|
||
}
|
||
|
||
// Скачиваем файл
|
||
fileReader, fileName, err := c.downloadAudioFile(message.Document.FileID)
|
||
if err != nil {
|
||
c.logger.Error("Failed to download document file", "error", err)
|
||
errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при скачивании аудиофайла. Попробуйте еще раз.")
|
||
c.bot.Send(errorMsg)
|
||
return
|
||
}
|
||
defer fileReader.Close()
|
||
|
||
// Обрабатываем файл
|
||
job, err := c.transcribeService.CreateTranscribeJob(fileReader, fileName)
|
||
if err != nil {
|
||
c.logger.Error("Failed to create transcribe job", "error", err)
|
||
errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при создании задачи на расшифровку. Попробуйте еще раз.")
|
||
c.bot.Send(errorMsg)
|
||
return
|
||
}
|
||
|
||
// Отправляем сообщение об успешном создании задачи
|
||
successMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("Задача на расшифровку создана. ID задачи: %s", job.Id))
|
||
successMsg.ReplyToMessageID = message.MessageID
|
||
c.bot.Send(successMsg)
|
||
|
||
// Отправляем результат расшифровки (асинхронно)
|
||
go c.sendTranscriptionResult(message.Chat.ID, job.Id, sentProgressMsg.MessageID)
|
||
}
|
||
|
||
func (c *TelegramController) downloadAudioFile(fileID string) (io.ReadCloser, string, error) {
|
||
// Получаем информацию о файле
|
||
file, err := c.bot.GetFile(tgbotapi.FileConfig{FileID: fileID})
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("failed to get file info: %w", err)
|
||
}
|
||
|
||
// Скачиваем файл
|
||
fileURL := file.Link(c.bot.Token)
|
||
resp, err := http.Get(fileURL)
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("failed to download file: %w", err)
|
||
}
|
||
|
||
// Получаем имя файла из URL
|
||
fileName := file.FilePath
|
||
if fileName == "" {
|
||
fileName = "audio.ogg"
|
||
}
|
||
|
||
return resp.Body, fileName, nil
|
||
}
|
||
|
||
func (c *TelegramController) sendTranscriptionResult(chatID int64, jobID string, progressMessageID int) {
|
||
// Периодически проверяем статус задачи
|
||
ticker := time.NewTicker(5 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
timeout := time.After(10 * time.Minute) // Максимальное время ожидания 10 минут
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
// Проверяем статус задачи
|
||
job, err := c.jobRepo.GetByID(jobID)
|
||
if err != nil {
|
||
c.logger.Error("Failed to get job", "job_id", jobID, "error", err)
|
||
continue
|
||
}
|
||
|
||
switch job.State {
|
||
case "done":
|
||
// Отправляем результат
|
||
if job.TranscriptionText != nil {
|
||
resultMsg := tgbotapi.NewMessage(chatID, *job.TranscriptionText)
|
||
resultMsg.ReplyToMessageID = progressMessageID
|
||
c.bot.Send(resultMsg)
|
||
} else {
|
||
resultMsg := tgbotapi.NewMessage(chatID, "Расшифровка завершена, но текст пуст.")
|
||
resultMsg.ReplyToMessageID = progressMessageID
|
||
c.bot.Send(resultMsg)
|
||
}
|
||
return
|
||
case "failed":
|
||
// Отправляем сообщение об ошибке
|
||
var errorMsg string
|
||
if job.ErrorText != nil {
|
||
errorMsg = fmt.Sprintf("Ошибка при расшифровке: %s", *job.ErrorText)
|
||
} else {
|
||
errorMsg = "Ошибка при расшифровке аудиофайла."
|
||
}
|
||
resultMsg := tgbotapi.NewMessage(chatID, errorMsg)
|
||
resultMsg.ReplyToMessageID = progressMessageID
|
||
c.bot.Send(resultMsg)
|
||
return
|
||
}
|
||
case <-timeout:
|
||
// Время ожидания истекло
|
||
resultMsg := tgbotapi.NewMessage(chatID, "Время ожидания результата расшифровки истекло. Попробуйте позже проверить статус задачи.")
|
||
resultMsg.ReplyToMessageID = progressMessageID
|
||
c.bot.Send(resultMsg)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
|
||
func (c *TelegramController) isAudioDocument(document *tgbotapi.Document) bool {
|
||
// Проверяем MIME-тип документа
|
||
if document.MimeType != "" {
|
||
return strings.HasPrefix(document.MimeType, "audio/") || strings.HasPrefix(document.MimeType, "video/")
|
||
}
|
||
|
||
// Проверяем расширение файла
|
||
audioExtensions := []string{".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac", ".wma"}
|
||
filename := document.FileName
|
||
for _, ext := range audioExtensions {
|
||
if len(filename) >= len(ext) && strings.ToLower(filename[len(filename)-len(ext):]) == ext {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
type EmptyBotTokenError struct{}
|
||
|
||
func (e *EmptyBotTokenError) Error() string {
|
||
return "telegram bot token is empty"
|
||
}
|