226 lines
6.7 KiB
Go
226 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
"time"
|
|
|
|
maxbot "github.com/max-messenger/max-bot-api-client-go"
|
|
maxschemes "github.com/max-messenger/max-bot-api-client-go/schemes"
|
|
)
|
|
|
|
const mediaGroupTimeout = 1 * time.Second
|
|
|
|
// mediaGroupItem хранит данные одного сообщения из альбома TG.
|
|
type mediaGroupItem struct {
|
|
photoSizes []PhotoSize
|
|
videoFileID string // для видео в альбомах
|
|
caption string
|
|
replyToMsg *TGMessage
|
|
entities []Entity
|
|
msg *TGMessage
|
|
maxChatID int64 // если задан — используется напрямую (crosspost)
|
|
crosspost bool // кросспостинг: без prefix, другой caption формат
|
|
}
|
|
|
|
// mediaGroupBuffer накапливает сообщения альбома перед отправкой.
|
|
type mediaGroupBuffer struct {
|
|
mu sync.Mutex
|
|
items []mediaGroupItem
|
|
timer *time.Timer
|
|
}
|
|
|
|
// bufferMediaGroup добавляет сообщение в буфер альбома.
|
|
// Если это первое сообщение — запускает таймер.
|
|
func (b *Bridge) bufferMediaGroup(ctx context.Context, groupID string, item mediaGroupItem) {
|
|
b.mgMu.Lock()
|
|
|
|
buf, ok := b.mgBuffers[groupID]
|
|
if !ok {
|
|
buf = &mediaGroupBuffer{}
|
|
b.mgBuffers[groupID] = buf
|
|
// Добавляем первый item до запуска таймера — исключает гонку
|
|
buf.items = append(buf.items, item)
|
|
buf.timer = time.AfterFunc(mediaGroupTimeout, func() {
|
|
b.flushMediaGroup(ctx, groupID)
|
|
})
|
|
b.mgMu.Unlock()
|
|
return
|
|
}
|
|
|
|
b.mgMu.Unlock()
|
|
|
|
buf.mu.Lock()
|
|
buf.items = append(buf.items, item)
|
|
buf.mu.Unlock()
|
|
}
|
|
|
|
// flushMediaGroup отправляет все накопленные фото/видео альбома одним сообщением в MAX.
|
|
func (b *Bridge) flushMediaGroup(ctx context.Context, groupID string) {
|
|
b.mgMu.Lock()
|
|
buf, ok := b.mgBuffers[groupID]
|
|
if !ok {
|
|
b.mgMu.Unlock()
|
|
return
|
|
}
|
|
delete(b.mgBuffers, groupID)
|
|
b.mgMu.Unlock()
|
|
|
|
buf.mu.Lock()
|
|
buf.timer.Stop()
|
|
items := buf.items
|
|
buf.mu.Unlock()
|
|
|
|
if len(items) == 0 {
|
|
return
|
|
}
|
|
|
|
// Определяем maxChatID
|
|
isCrosspost := items[0].crosspost
|
|
maxChatID := items[0].maxChatID
|
|
if maxChatID == 0 {
|
|
var linked bool
|
|
maxChatID, linked = b.repo.GetMaxChat(items[0].msg.Chat.ID)
|
|
if !linked {
|
|
slog.Warn("media group: chat not linked", "tgChat", items[0].msg.Chat.ID)
|
|
return
|
|
}
|
|
}
|
|
|
|
uid := tgUserID(items[0].msg)
|
|
prefix := !isCrosspost && b.repo.HasPrefix("tg", items[0].msg.Chat.ID)
|
|
|
|
// Caption и entities берём из первого элемента, у которого caption не пустой
|
|
var caption string
|
|
var entities []Entity
|
|
for _, it := range items {
|
|
if it.caption != "" {
|
|
caption = it.caption
|
|
entities = it.entities
|
|
break
|
|
}
|
|
}
|
|
|
|
// Reply ID из первого элемента с reply
|
|
var replyTo string
|
|
for _, it := range items {
|
|
if it.replyToMsg != nil {
|
|
if maxReplyID, ok := b.repo.LookupMaxMsgID(it.msg.Chat.ID, it.replyToMsg.MessageID); ok {
|
|
replyTo = maxReplyID
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// Форматируем caption
|
|
mdCaption := caption
|
|
if entities != nil {
|
|
mdCaption = tgEntitiesToMarkdown(caption, entities)
|
|
}
|
|
|
|
m := maxbot.NewMessage().SetChat(maxChatID).SetText(mdCaption)
|
|
if mdCaption != caption {
|
|
m.SetFormat("markdown")
|
|
}
|
|
if replyTo != "" {
|
|
m.SetReply(mdCaption, replyTo)
|
|
}
|
|
|
|
// Загружаем и добавляем все фото
|
|
photosSent := 0
|
|
for _, it := range items {
|
|
if len(it.photoSizes) > 0 {
|
|
photo := it.photoSizes[len(it.photoSizes)-1]
|
|
fileURL, err := b.tgFileURL(ctx, photo.FileID)
|
|
if err != nil {
|
|
slog.Error("media group: tgFileURL failed", "err", err)
|
|
continue
|
|
}
|
|
// Если custom TG API — MAX не может скачать по URL, скачиваем сами
|
|
if b.cfg.TgAPIURL != "" {
|
|
uploaded, err := b.uploadTgPhotoToMax(ctx, photo.FileID)
|
|
if err != nil {
|
|
slog.Error("media group: photo upload failed", "err", err)
|
|
continue
|
|
}
|
|
m.AddPhoto(uploaded)
|
|
} else {
|
|
uploaded, err := b.maxApi.Uploads.UploadPhotoFromUrl(ctx, fileURL)
|
|
if err != nil {
|
|
slog.Error("media group: photo upload failed", "err", err)
|
|
continue
|
|
}
|
|
m.AddPhoto(uploaded)
|
|
}
|
|
photosSent++
|
|
}
|
|
}
|
|
|
|
// Загружаем видео из альбома через direct API
|
|
videosSent := 0
|
|
var videoTokens []string
|
|
for _, it := range items {
|
|
if it.videoFileID != "" {
|
|
uploaded, err := b.uploadTgMediaToMax(ctx, it.videoFileID, maxschemes.VIDEO, "video.mp4")
|
|
if err != nil {
|
|
slog.Error("media group: video upload failed", "err", err)
|
|
continue
|
|
}
|
|
videoTokens = append(videoTokens, uploaded.Token)
|
|
videosSent++
|
|
}
|
|
}
|
|
|
|
totalMedia := photosSent + videosSent
|
|
if totalMedia == 0 {
|
|
slog.Warn("media group: no media uploaded, skipping")
|
|
return
|
|
}
|
|
|
|
slog.Info("TG→MAX sending media group", "photos", photosSent, "videos", videosSent, "uid", uid, "tgChat", items[0].msg.Chat.ID, "maxChat", maxChatID)
|
|
|
|
// Если есть фото — отправляем через SDK (поддерживает AddPhoto)
|
|
if photosSent > 0 {
|
|
result, err := b.maxApi.Messages.SendWithResult(ctx, m)
|
|
if err != nil {
|
|
slog.Error("TG→MAX media group send failed", "err", err)
|
|
if b.cbFail(maxChatID) {
|
|
b.tg.SendMessage(ctx, items[0].msg.Chat.ID,
|
|
fmt.Sprintf("Не удалось переслать альбом в MAX. Пересылка приостановлена на %d мин. Проверьте, что бот добавлен в MAX-чат и является админом.", int(cbCooldown.Minutes())), nil)
|
|
}
|
|
// Fallback — по одному
|
|
for _, it := range items {
|
|
var cap string
|
|
if isCrosspost {
|
|
cap = formatTgCrosspostCaption(it.msg)
|
|
} else {
|
|
cap = formatTgCaption(it.msg, prefix, b.cfg.MessageNewline)
|
|
}
|
|
go b.forwardTgToMax(ctx, it.msg, maxChatID, cap)
|
|
}
|
|
return
|
|
}
|
|
b.cbSuccess(maxChatID)
|
|
slog.Info("TG→MAX media group sent", "mid", result.Body.Mid, "photos", photosSent)
|
|
b.repo.SaveMsg(items[0].msg.Chat.ID, items[0].msg.MessageID, maxChatID, result.Body.Mid)
|
|
}
|
|
|
|
// Видео отправляем отдельно через direct API (SDK не поддерживает AddVideo)
|
|
for i, token := range videoTokens {
|
|
videoCaption := ""
|
|
if i == 0 && photosSent == 0 {
|
|
videoCaption = mdCaption // caption на первое видео если нет фото
|
|
}
|
|
mid, err := b.sendMaxDirectFormatted(ctx, maxChatID, videoCaption, "video", token, "", "")
|
|
if err != nil {
|
|
slog.Error("TG→MAX media group video send failed", "err", err)
|
|
continue
|
|
}
|
|
if i == 0 && photosSent == 0 {
|
|
b.repo.SaveMsg(items[0].msg.Chat.ID, items[0].msg.MessageID, maxChatID, mid)
|
|
}
|
|
}
|
|
}
|