From 400537176716164b09f8b394ac090a4a5caf6d21 Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Mon, 23 Jun 2025 16:29:45 +0300 Subject: [PATCH] fixed gitignore --- internal/bot/app.go | 77 +++++++++++++++++++++++++ internal/bot/handler.go | 121 +++++++++++++++++++++++++++++++++++++++ internal/bot/telegram.go | 64 +++++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 internal/bot/app.go create mode 100644 internal/bot/handler.go create mode 100644 internal/bot/telegram.go diff --git a/internal/bot/app.go b/internal/bot/app.go new file mode 100644 index 0000000..cbcadd7 --- /dev/null +++ b/internal/bot/app.go @@ -0,0 +1,77 @@ +package bot + +import ( + "context" + "log/slog" + "os" + "os/signal" + "slices" + "syscall" + + "gitea.mrixs.me/Mrixs/yamusic-bot/internal/admin" + "gitea.mrixs.me/Mrixs/yamusic-bot/internal/config" + "gitea.mrixs.me/Mrixs/yamusic-bot/internal/interfaces" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +// App - главное приложение бота. +type App struct { + cfg *config.Config + api *tgbotapi.BotAPI + storage interfaces.TrackStorage + adminHandler *admin.Handler + inlineHandler *InlineHandler +} + +// NewApp создает новый экземпляр приложения. +func NewApp(cfg *config.Config, api *tgbotapi.BotAPI, storage interfaces.TrackStorage, adminHandler *admin.Handler, inlineHandler *InlineHandler) *App { + return &App{ + cfg: cfg, + api: api, + storage: storage, + adminHandler: adminHandler, + inlineHandler: inlineHandler, + } +} + +// Run запускает основной цикл бота. +func (a *App) Run(ctx context.Context) { + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) + defer cancel() + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + updates := a.api.GetUpdatesChan(u) + + slog.Info("Bot is running and waiting for updates...") + + for { + select { + case update := <-updates: + a.handleUpdate(ctx, update) + case <-ctx.Done(): + slog.Info("Shutting down...") + a.api.StopReceivingUpdates() + if err := a.storage.Close(); err != nil { + slog.Error("Failed to close storage", "error", err) + } + return + } + } +} + +func (a *App) handleUpdate(ctx context.Context, update tgbotapi.Update) { + if update.InlineQuery != nil { + go a.inlineHandler.HandleInlineQuery(ctx, update.InlineQuery) + } else if update.Message != nil && update.Message.IsCommand() { + if a.isAdmin(update.Message.From.ID) { + go a.adminHandler.HandleCommand(ctx, update.Message) + } else { + slog.Warn("Unauthorized command attempt", "user_id", update.Message.From.ID) + } + } +} + +func (a *App) isAdmin(userID int64) bool { + return slices.Contains(a.cfg.TelegramAdminIDs, userID) +} diff --git a/internal/bot/handler.go b/internal/bot/handler.go new file mode 100644 index 0000000..dee462b --- /dev/null +++ b/internal/bot/handler.go @@ -0,0 +1,121 @@ +package bot + +import ( + "context" + "log/slog" + "strconv" + "sync" + + "gitea.mrixs.me/Mrixs/yamusic-bot/internal/interfaces" + "gitea.mrixs.me/Mrixs/yamusic-bot/internal/model" + "gitea.mrixs.me/Mrixs/yamusic-bot/internal/processor" + "gitea.mrixs.me/Mrixs/yamusic-bot/pkg/yamusic" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +// InlineHandler обрабатывает inline-запросы. +type InlineHandler struct { + yandex interfaces.YandexMusicClient + processor *processor.TrackProcessor + telegram interfaces.TelegramClient +} + +// NewInlineHandler создает новый обработчик inline-запросов. +func NewInlineHandler(yandex interfaces.YandexMusicClient, processor *processor.TrackProcessor, telegram interfaces.TelegramClient) *InlineHandler { + return &InlineHandler{ + yandex: yandex, + processor: processor, + telegram: telegram, + } +} + +// HandleInlineQuery обрабатывает входящий inline-запрос. +func (h *InlineHandler) HandleInlineQuery(ctx context.Context, query *tgbotapi.InlineQuery) { + slog.Info("Handling inline query", "user_id", query.From.ID, "query", query.Query) + + urlInfo, err := yamusic.ParseYandexURL(query.Query) + if err != nil { + h.answerWithError(ctx, query.ID, "Неверный формат ссылки. Поддерживаются ссылки на треки, альбомы и исполнителей Yandex.Music.") + return + } + + var trackInfos []*model.TrackInfo + switch urlInfo.Type { + case "track": + info, err := h.yandex.GetTrackInfo(ctx, urlInfo.TrackID) + if err != nil { + slog.Error("Failed to get track info", "track_id", urlInfo.TrackID, "error", err) + h.answerWithError(ctx, query.ID, "Не удалось получить информацию о треке.") + return + } + trackInfos = append(trackInfos, info) + case "album": + infos, err := h.yandex.GetAlbumTrackInfos(ctx, urlInfo.AlbumID) + if err != nil { + slog.Error("Failed to get album info", "album_id", urlInfo.AlbumID, "error", err) + h.answerWithError(ctx, query.ID, "Не удалось получить информацию об альбоме.") + return + } + trackInfos = infos + case "artist": + infos, err := h.yandex.GetArtistTrackInfos(ctx, urlInfo.ArtistID) + if err != nil { + slog.Error("Failed to get artist info", "artist_id", urlInfo.ArtistID, "error", err) + h.answerWithError(ctx, query.ID, "Не удалось получить информацию об исполнителе.") + return + } + trackInfos = infos + } + + if len(trackInfos) == 0 { + h.answerWithError(ctx, query.ID, "По этой ссылке ничего не найдено.") + return + } + + h.processAndAnswer(ctx, query.ID, trackInfos) +} + +func (h *InlineHandler) processAndAnswer(ctx context.Context, queryID string, trackInfos []*model.TrackInfo) { + var wg sync.WaitGroup + resultsChan := make(chan interface{}, len(trackInfos)) + + for i, info := range trackInfos { + wg.Add(1) + go func(trackInfo *model.TrackInfo, resultID int) { + defer wg.Done() + fileID, err := h.processor.Process(ctx, trackInfo) + if err != nil { + slog.Error("Failed to process track", "track_id", trackInfo.YandexTrackID, "error", err) + return + } + result := tgbotapi.NewInlineQueryResultAudio(strconv.Itoa(resultID), fileID, trackInfo.Title) + result.Performer = trackInfo.Artist + resultsChan <- result + }(info, i) + } + + wg.Wait() + close(resultsChan) + + var finalResults []interface{} + for result := range resultsChan { + finalResults = append(finalResults, result) + } + + if len(finalResults) == 0 { + slog.Warn("No results to send after processing", "query_id", queryID) + h.answerWithError(ctx, queryID, "Не удалось обработать ни один трек.") + return + } + + if err := h.telegram.AnswerInlineQuery(ctx, queryID, finalResults); err != nil { + slog.Error("Failed to send final answer to inline query", "error", err) + } +} + +func (h *InlineHandler) answerWithError(ctx context.Context, queryID, message string) { + article := tgbotapi.NewInlineQueryResultArticle(queryID, "Ошибка", message) + if err := h.telegram.AnswerInlineQuery(ctx, queryID, []interface{}{article}); err != nil { + slog.Error("Failed to answer with error", "error", err) + } +} diff --git a/internal/bot/telegram.go b/internal/bot/telegram.go new file mode 100644 index 0000000..13e87ef --- /dev/null +++ b/internal/bot/telegram.go @@ -0,0 +1,64 @@ +package bot + +import ( + "context" + "fmt" + "log/slog" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +// TelegramClientAdapter адаптирует библиотеку tgbotapi под наш интерфейс interfaces.TelegramClient. +type TelegramClientAdapter struct { + api *tgbotapi.BotAPI + cacheChatID int64 +} + +// NewTelegramClientAdapter создает новый адаптер. +func NewTelegramClientAdapter(api *tgbotapi.BotAPI, cacheChatID int64) *TelegramClientAdapter { + return &TelegramClientAdapter{ + api: api, + cacheChatID: cacheChatID, + } +} + +// SendAudioToCacheChannel загружает аудиофайл в кэш-канал и возвращает его FileID. +func (t *TelegramClientAdapter) SendAudioToCacheChannel(ctx context.Context, audioPath, title, performer string) (string, error) { + audio := tgbotapi.NewAudio(t.cacheChatID, tgbotapi.FilePath(audioPath)) + audio.Title = title + audio.Performer = performer + + msg, err := t.api.Send(audio) + if err != nil { + return "", fmt.Errorf("failed to send audio to cache channel: %w", err) + } + if msg.Audio == nil { + return "", fmt.Errorf("sent message does not contain audio") + } + + slog.Debug("Audio sent to cache channel", "file_id", msg.Audio.FileID) + return msg.Audio.FileID, nil +} + +// AnswerInlineQuery отвечает на inline-запрос. +func (t *TelegramClientAdapter) AnswerInlineQuery(ctx context.Context, queryID string, results []interface{}) error { + inlineConfig := tgbotapi.InlineConfig{ + InlineQueryID: queryID, + Results: results, + CacheTime: 1, // Кэшируем результат на стороне Telegram на 1 секунду + } + + if _, err := t.api.Request(inlineConfig); err != nil { + return fmt.Errorf("failed to answer inline query: %w", err) + } + return nil +} + +// SendMessage отправляет текстовое сообщение. +func (t *TelegramClientAdapter) SendMessage(ctx context.Context, chatID int64, text string) error { + msg := tgbotapi.NewMessage(chatID, text) + if _, err := t.api.Send(msg); err != nil { + return fmt.Errorf("failed to send message: %w", err) + } + return nil +}