package tg import ( "fmt" "io" "log/slog" "net/http" "slices" "strings" "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) send(chattable tgbotapi.Chattable) (tgbotapi.Message, error) { msg, err := c.bot.Send(chattable) if err != nil { c.logger.Error("Failed to send message to tg bot", "error", err) } return msg, err } func (c *TelegramController) handleStartCommand(message *tgbotapi.Message) { msg := tgbotapi.NewMessage(message.Chat.ID, "Привет! Я бот для расшифровки аудиосообщений. Отправь мне голосовое сообщение или аудиофайл, и я пришлю тебе текст.") msg.ReplyToMessageID = message.MessageID c.send(msg) } func (c *TelegramController) handleForbiddenUser(message *tgbotapi.Message) { msg := tgbotapi.NewMessage(message.Chat.ID, "Извини, тебе нельзя пользоваться этим ботом. Обратись к владельцу бота.") msg.ReplyToMessageID = message.MessageID c.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.send(msg) } func (c *TelegramController) handleAudioMessage(message *tgbotapi.Message) { // Отправляем сообщение о начале обработки progressMsg := tgbotapi.NewMessage(message.Chat.ID, "Обрабатываю аудиофайл...") progressMsg.ReplyToMessageID = message.MessageID sentProgressMsg, err := c.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.send(errorMsg) return } defer fileReader.Close() // Обрабатываем файл job, err := c.transcribeService.CreateJobFromTelegram(fileReader, fileName, message.Chat.ID, sentProgressMsg.MessageID) if err != nil { c.logger.Error("Failed to create transcribe job", "error", err) errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при создании задачи на расшифровку. Попробуйте еще раз.") c.send(errorMsg) return } // Отправляем сообщение об успешном создании задачи successMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("Задача на расшифровку создана. ID задачи: %s", job.Id)) successMsg.ReplyToMessageID = message.MessageID c.send(successMsg) } func (c *TelegramController) handleVoiceMessage(message *tgbotapi.Message) { // Отправляем сообщение о начале обработки progressMsg := tgbotapi.NewMessage(message.Chat.ID, "Обрабатываю голосовое сообщение...") progressMsg.ReplyToMessageID = message.MessageID sentProgressMsg, err := c.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.send(errorMsg) return } defer fileReader.Close() // Обрабатываем файл job, err := c.transcribeService.CreateJobFromTelegram(fileReader, fileName, message.Chat.ID, sentProgressMsg.MessageID) if err != nil { c.logger.Error("Failed to create transcribe job", "error", err) errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при создании задачи на расшифровку. Попробуйте еще раз.") c.send(errorMsg) return } // Отправляем сообщение об успешном создании задачи successMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("Задача на расшифровку создана. ID задачи: %s", job.Id)) successMsg.ReplyToMessageID = message.MessageID c.send(successMsg) } 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.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.send(errorMsg) return } defer fileReader.Close() // Обрабатываем файл job, err := c.transcribeService.CreateJobFromTelegram(fileReader, fileName, message.Chat.ID, sentProgressMsg.MessageID) if err != nil { c.logger.Error("Failed to create transcribe job", "error", err) errorMsg := tgbotapi.NewMessage(message.Chat.ID, "Ошибка при создании задачи на расшифровку. Попробуйте еще раз.") c.send(errorMsg) return } // Отправляем сообщение об успешном создании задачи successMsg := tgbotapi.NewMessage(message.Chat.ID, fmt.Sprintf("Задача на расшифровку создана. ID задачи: %s", job.Id)) successMsg.ReplyToMessageID = message.MessageID c.send(successMsg) } 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) 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" }