fixed gitignore
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-06-23 16:29:45 +03:00
parent 0d5fcabebd
commit 4005371767
3 changed files with 262 additions and 0 deletions

77
internal/bot/app.go Normal file
View File

@@ -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)
}

121
internal/bot/handler.go Normal file
View File

@@ -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)
}
}

64
internal/bot/telegram.go Normal file
View File

@@ -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
}