package tg import ( "fmt" "io" "log/slog" "net/http" "slices" "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 transcribeService *service.TranscribeService jobRepo contract.TranscriptJobRepository logger *slog.Logger // params bot *tgbotapi.BotAPI userWhiteList []string updateTimeout int } type TelegramConfig struct { BotToken string UpdateTimeout int UserWhiteList []string } 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, userWhiteList: config.UserWhiteList, } 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) if !slices.Contains(c.userWhiteList, author) { c.logger.Info("User is not in white list, reject", "author", author) c.handleForbiddenUser(update.Message) continue } // 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) handleForbiddenUser(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" }