package tgBot import ( "bytes" "context" "fmt" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" log "github.com/sirupsen/logrus" "io" "net/http" "path/filepath" "sync" "tg-disc-bot/config" "tg-disc-bot/dto" "time" ) type TgBot struct { ctx context.Context bot *bot.Bot chatID int64 token string msgChan chan dto.TelegramDTO mediaGroupCache map[string][]dto.Image cacheMutex sync.Mutex } func NewTgBot(ctx context.Context, config config.TelegramConfig) (*TgBot, error) { tgBot := &TgBot{ ctx: ctx, chatID: config.ChatID, token: config.Token, msgChan: make(chan dto.TelegramDTO, 100), mediaGroupCache: make(map[string][]dto.Image, 10), cacheMutex: sync.Mutex{}, } var err error tgBot.bot, err = bot.New(config.Token) if err != nil { return nil, err } return tgBot, nil } func (b *TgBot) Start(fromDiscord chan dto.DiscordDTO) chan dto.TelegramDTO { b.bot.RegisterHandler(bot.HandlerTypeMessageText, "", bot.MatchTypeContains, b.mainHandler) go func() { log.Info("Starting telegram bot...") b.bot.Start(b.ctx) }() go func() { for msg := range fromDiscord { log.WithField("content", msg).Debug("TG | Message from Discord") m := fmt.Sprintf("*\\[ %s \\]*\n%s", msg.AuthorName, msg.Content) b.bot.SendMessage(b.ctx, &bot.SendMessageParams{ ChatID: b.chatID, Text: m, ParseMode: "", }) if msg.Images != nil { var mediaGroup []models.InputMedia for _, img := range *msg.Images { photo := &models.InputMediaPhoto{ Media: fmt.Sprintf("attach://%s", img.Filename), //Caption: "test caption", MediaAttachment: bytes.NewReader(img.Data), } mediaGroup = append(mediaGroup, photo) } _, err := b.bot.SendMediaGroup(b.ctx, &bot.SendMediaGroupParams{ ChatID: b.chatID, Media: mediaGroup, }) if err != nil { log.Error("TG | Failed to send media group") } } } }() return b.msgChan } func (b *TgBot) mainHandler(ctx context.Context, bt *bot.Bot, update *models.Update) { if update.Message != nil && !update.Message.From.IsBot { mediaGroupID := update.Message.MediaGroupID if mediaGroupID != "" { b.processMediaGroup(update, mediaGroupID, b.msgChan) } else { b.proccessSimple(update, b.msgChan) } } } func (b *TgBot) getImage(photo models.PhotoSize) *dto.Image { fileID := &bot.GetFileParams{FileID: photo.FileID} file, err := b.bot.GetFile(b.ctx, fileID) if err != nil { log.Errorf("GetFile error: %s", err) return nil } if file.FilePath == "" { log.Errorf("FilePath is empty for fileID: %s", photo.FileID) return nil } resp, err := http.Get(fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", b.token, file.FilePath)) if err != nil { log.Errorf("Download error: %s", err) return nil } defer resp.Body.Close() imgBytes, err := io.ReadAll(resp.Body) if err != nil { log.Errorf("ReadAll error: %s", err) return nil } image := dto.Image{ Filename: filepath.Base(file.FilePath), Data: imgBytes, } return &image } func (b *TgBot) proccessSimple(update *models.Update, msgChan chan<- dto.TelegramDTO) { var images []dto.Image content := update.Message.Text if update.Message.Photo != nil { largest := update.Message.Photo[len(update.Message.Photo)-1] images = append(images, *b.getImage(largest)) content = update.Message.Caption } msg := dto.TelegramDTO{ AuthorName: b.getName(update), Content: content, Images: &images, } msgChan <- msg } func (b *TgBot) processMediaGroup(update *models.Update, mediaGroupID string, msgChan chan<- dto.TelegramDTO) { b.cacheMutex.Lock() defer b.cacheMutex.Unlock() if update.Message.Photo != nil { largest := update.Message.Photo[len(update.Message.Photo)-1] image := b.getImage(largest) b.mediaGroupCache[mediaGroupID] = append(b.mediaGroupCache[mediaGroupID], *image) } var content string if update.Message.Caption != "" { content = update.Message.Caption } else { content = update.Message.Text } go func() { time.Sleep(2 * time.Second) b.cacheMutex.Lock() defer b.cacheMutex.Unlock() images, exists := b.mediaGroupCache[mediaGroupID] if !exists || len(images) == 0 { return } delete(b.mediaGroupCache, mediaGroupID) msg := dto.TelegramDTO{ AuthorName: b.getName(update), Content: content, Images: &images, } msgChan <- msg }() } func (b *TgBot) getName(update *models.Update) string { switch { case update.Message.From.Username != "": return update.Message.From.Username case update.Message.From.FirstName != "": return update.Message.From.FirstName case update.Message.From.LastName != "": return update.Message.From.LastName default: return "No name" } }