Add basic telegram bot
This commit is contained in:
@@ -19,3 +19,7 @@ S3_ENDPOINT=
|
||||
YANDEX_CLOUD_API_KEY=your_api_key_here
|
||||
# ID папки в Yandex Cloud (получить в консоли Yandex Cloud)
|
||||
YANDEX_CLOUD_FOLDER_ID=your_folder_id_here
|
||||
|
||||
# Telegram Bot Configuration
|
||||
# Токен Telegram бота (получить у @BotFather в Telegram)
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
|
1
go.mod
1
go.mod
@@ -10,6 +10,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.86.0
|
||||
github.com/doug-martin/goqu/v9 v9.19.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
|
2
go.sum
2
go.sum
@@ -77,6 +77,8 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
|
||||
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
|
328
internal/controller/tg/tg.go
Normal file
328
internal/controller/tg/tg.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"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 {
|
||||
bot *tgbotapi.BotAPI
|
||||
transcribeService *service.TranscribeService
|
||||
jobRepo contract.TranscriptJobRepository
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewTelegramController(transcribeService *service.TranscribeService, jobRepo contract.TranscriptJobRepository, logger *slog.Logger) (*TelegramController, error) {
|
||||
botToken := os.Getenv("TELEGRAM_BOT_TOKEN")
|
||||
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,
|
||||
}
|
||||
|
||||
return controller, nil
|
||||
}
|
||||
|
||||
func (c *TelegramController) Start() {
|
||||
c.logger.Info("Telegram bot started", "username", c.bot.Self.UserName)
|
||||
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := c.bot.GetUpdatesChan(u)
|
||||
|
||||
for update := range updates {
|
||||
if update.Message == nil { // ignore any non-Message updates
|
||||
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.logger.Info("Telegram bot stopped")
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
33
main.go
33
main.go
@@ -17,6 +17,7 @@ import (
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/recognizer/yandex"
|
||||
"git.vakhrushev.me/av/transcriber/internal/adapter/repo/sqlite"
|
||||
httpcontroller "git.vakhrushev.me/av/transcriber/internal/controller/http"
|
||||
tgcontroller "git.vakhrushev.me/av/transcriber/internal/controller/tg"
|
||||
"git.vakhrushev.me/av/transcriber/internal/controller/worker"
|
||||
"git.vakhrushev.me/av/transcriber/internal/service"
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
@@ -93,6 +94,31 @@ func main() {
|
||||
// Создаем сервисы
|
||||
transcribeService := service.NewTranscribeService(jobRepo, fileRepo, metaviewer, converter, recognizer, logger)
|
||||
|
||||
// Создаем контекст для graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Создаем WaitGroup для ожидания завершения всех воркеров
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Создаем Telegram бот
|
||||
tgController, err := tgcontroller.NewTelegramController(transcribeService, jobRepo, logger)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create Telegram controller", "error", err)
|
||||
// Не останавливаем приложение, если Telegram бот не создан
|
||||
} else {
|
||||
// Запускаем Telegram бот в отдельной горутине
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
logger.Info("Starting Telegram bot")
|
||||
tgController.Start()
|
||||
}()
|
||||
|
||||
// Добавляем функцию остановки бота в контекст завершения
|
||||
defer tgController.Stop()
|
||||
}
|
||||
|
||||
// Создаем воркеры
|
||||
conversionWorker := worker.NewCallbackWorker("conversion_worker", transcribeService.FindAndRunConversionJob, logger)
|
||||
transcribeWorker := worker.NewCallbackWorker("transcribe_worker", transcribeService.FindAndRunTranscribeJob, logger)
|
||||
@@ -104,13 +130,6 @@ func main() {
|
||||
checkWorker,
|
||||
}
|
||||
|
||||
// Создаем контекст для graceful shutdown
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Создаем WaitGroup для ожидания завершения всех воркеров
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Запускаем воркеры в отдельных горутинах
|
||||
for _, w := range workers {
|
||||
wg.Add(1)
|
||||
|
Reference in New Issue
Block a user