diff --git a/tg_bot/bot/bot.go b/tg_bot/bot/bot.go
index fba8ab9..e74bed0 100644
--- a/tg_bot/bot/bot.go
+++ b/tg_bot/bot/bot.go
@@ -341,6 +341,7 @@ func (b *Bot) Update(u *echotron.Update) {
log.Error().Err(err).Msg("SendMessage failed while sending restart notice")
}
b.SetState(b.commandRouter("/start"), EditMessage)
+ return
}
// === Обработка входящего события ===
diff --git a/tg_bot/screens/add_creative.go b/tg_bot/screens/add_creative.go
index 7443666..2ee18cf 100644
--- a/tg_bot/screens/add_creative.go
+++ b/tg_bot/screens/add_creative.go
@@ -17,8 +17,9 @@ import (
type AddCreativeCtx struct {
CreativeEditorFields
- ProjectID string
- BackState bot.State
+ ProjectID string
+ BackState bot.State
+ WaitMessageSent bool
}
type AddCreativeStart struct{ Ctx *AddCreativeCtx }
@@ -40,9 +41,9 @@ const msgWaitCreative = `
const msgConfirmCreativeFormat = `
➕ Добавляем этот креатив?
-Название креатива: %s
+Название: %s
-Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.
+Чтобы изменить название, отправь мне сообщение 👇:
`
const msgDownloadMediaError = `
@@ -93,9 +94,31 @@ const msgCreativeCreatedButtons = `
`
+func filterCreateButtons(rows [][]echotron.InlineKeyboardButton) [][]echotron.InlineKeyboardButton {
+ filtered := make([][]echotron.InlineKeyboardButton, 0, len(rows))
+ for _, row := range rows {
+ skip := false
+ for _, btn := range row {
+ switch btn.CallbackData {
+ case "edit_text", "add_media", "delete_media":
+ skip = true
+ }
+ }
+ if !skip {
+ filtered = append(filtered, row)
+ }
+ }
+ return filtered
+}
+
func (s *AddCreativeStart) Enter(b *bot.Bot, mode bot.RenderMode) {
+ if s.Ctx.WaitMessageSent {
+ return
+ }
+
kb := Keyboard(Row(Button("« Отмена", "cancel")))
b.Render(msgWaitCreative, kb, mode)
+ s.Ctx.WaitMessageSent = true
}
func (s *AddCreativeStart) HandleCallback(b *bot.Bot, u *echotron.Update) {
@@ -118,7 +141,15 @@ func (s *AddCreativeStart) HandleMessage(b *bot.Bot, u *echotron.Update) {
s.Ctx.Name = &name
}
- s.Ctx.DeleteUserMessage(b, u.Message.ID)
+ if u.Message.MediaGroupID != "" {
+ groupID := u.Message.MediaGroupID
+ s.Ctx.scheduleMediaGroupAction(groupID, func() {
+ b.SetState(&AddCreativeEdit{ctx: s.Ctx}, bot.NewMessage)
+ })
+ b.MarkHandled()
+ return
+ }
+
b.SetState(&AddCreativeEdit{ctx: s.Ctx}, bot.NewMessage)
}
@@ -153,8 +184,7 @@ func (s *AddCreativeEdit) HandleCallback(b *bot.Bot, u *echotron.Update) {
s.ctx.InputMode = inputModeMedia
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
case data == "delete_media":
- s.ctx.MediaType = ""
- s.ctx.MediaFileID = ""
+ s.ctx.MediaItems = nil
s.ctx.MediaChanged = true
s.ctx.UpdateCreativePreview(b)
s.Enter(b, bot.EditMessage)
@@ -175,7 +205,24 @@ func (s *AddCreativeEdit) HandleCallback(b *bot.Bot, u *echotron.Update) {
}
func (s *AddCreativeEdit) HandleMessage(b *bot.Bot, u *echotron.Update) {
- if u.Message == nil || u.Message.Text == "" {
+ if u.Message == nil {
+ return
+ }
+
+ if u.Message.Text == "" {
+ if s.ctx.SetMediaFromMessage(u.Message) {
+ if u.Message.MediaGroupID != "" {
+ groupID := u.Message.MediaGroupID
+ s.ctx.scheduleMediaGroupAction(groupID, func() {
+ s.ctx.UpdateCreativePreview(b)
+ s.Enter(b, bot.EditMessage)
+ })
+ b.MarkHandled()
+ return
+ }
+ s.ctx.UpdateCreativePreview(b)
+ s.Enter(b, bot.EditMessage)
+ }
return
}
@@ -284,10 +331,19 @@ func (s *AddCreativeInput) HandleMessage(b *bot.Bot, u *echotron.Update) {
if !s.ctx.SetMediaFromMessage(u.Message) {
return
}
- s.ctx.DeleteUserMessage(b, u.Message.ID)
+ if u.Message.MediaGroupID != "" {
+ groupID := u.Message.MediaGroupID
+ s.ctx.scheduleMediaGroupAction(groupID, func() {
+ s.ctx.UpdateCreativePreview(b)
+ s.ctx.ShowMediaPanel(b)
+ b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
+ })
+ b.MarkHandled()
+ return
+ }
s.ctx.UpdateCreativePreview(b)
- s.ctx.ClearInputMode()
- b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
+ s.ctx.ShowMediaPanel(b)
+ b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
}
}
@@ -345,18 +401,18 @@ func (s *AddCreativeCtx) showCreativeConfirmation(b *bot.Bot) {
escapedName := ui.EscapeHTML(*s.Name)
confirmText := fmt.Sprintf(msgConfirmCreativeFormat, escapedName)
- buttons := s.BuildEditorButtons("✓ Сохранить", "confirm_create", "Отмена", "cancel")
+ buttons := filterCreateButtons(s.BuildEditorButtons("✓ Сохранить", "confirm_create", "Отмена", "cancel"))
s.ShowControlPanel(b, confirmText, buttons)
}
func (s *AddCreativeCtx) createCreative(b *bot.Bot) {
var err error
- var mediaData []byte
- if s.MediaFileID != "" {
- mediaData, err = b.DownloadFileBytes(s.MediaFileID)
+ var mediaItems []backend.CreativeMediaInput
+ if len(s.MediaItems) > 0 {
+ mediaItems, err = s.BuildMediaInputs(b)
if err != nil {
log.Error().Err(err).Msg("Failed to download creative media")
- buttons := s.BuildEditorButtons("✓ Попробовать снова", "confirm_create", "Отмена", "cancel")
+ buttons := filterCreateButtons(s.BuildEditorButtons("✓ Попробовать снова", "confirm_create", "Отмена", "cancel"))
s.ShowControlPanel(b, msgDownloadMediaError, buttons)
return
}
@@ -372,14 +428,16 @@ func (s *AddCreativeCtx) createCreative(b *bot.Bot) {
log.Info().Str("text", *s.Text).Msg("Creative text")
- creative, err := b.Backend.CreateCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, backend.CreateCreativeInput{
- Name: *s.Name,
- Text: *s.Text,
- MediaType: s.MediaType,
- MediaFileID: s.MediaFileID,
- MediaData: mediaData,
- Buttons: buttons,
- })
+ input := backend.CreateCreativeInput{
+ Name: *s.Name,
+ Text: *s.Text,
+ Buttons: buttons,
+ }
+ if len(mediaItems) > 0 {
+ input.MediaItems = mediaItems
+ }
+
+ creative, err := b.Backend.CreateCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, input)
if err != nil {
log.Error().Err(err).Msg("Failed to create creative")
@@ -395,7 +453,7 @@ func (s *AddCreativeCtx) createCreative(b *bot.Bot) {
userMsg = msgCreateError
}
- buttons := s.BuildEditorButtons("✓ Попробовать снова", "confirm_create", "Отмена", "cancel")
+ buttons := filterCreateButtons(s.BuildEditorButtons("✓ Попробовать снова", "confirm_create", "Отмена", "cancel"))
s.ShowControlPanel(b, userMsg, buttons)
return
}
diff --git a/tg_bot/screens/creative_details.go b/tg_bot/screens/creative_details.go
index 75364ad..d11d2ba 100644
--- a/tg_bot/screens/creative_details.go
+++ b/tg_bot/screens/creative_details.go
@@ -3,6 +3,7 @@ package screens
import (
"context"
"fmt"
+ "sort"
"strings"
"github.com/NicoNex/echotron/v3"
@@ -42,8 +43,19 @@ func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
} else {
log.Info().Msg("Creative text is empty from backend")
}
- s.MediaType = creative.MediaType
- s.MediaFileID = creative.MediaFileID
+ s.MediaItems = nil
+ if len(creative.MediaItems) > 0 {
+ mediaItems := append([]backend.CreativeMediaItem(nil), creative.MediaItems...)
+ sort.Slice(mediaItems, func(i, j int) bool {
+ return mediaItems[i].Position < mediaItems[j].Position
+ })
+ for _, item := range mediaItems {
+ s.MediaItems = append(s.MediaItems, MediaItem{
+ MediaType: item.MediaType,
+ MediaFileID: item.MediaFileID,
+ })
+ }
+ }
s.Buttons = nil
for _, btn := range creative.Buttons {
s.Buttons = append(s.Buttons, InlineButton{Text: btn.Text, URL: btn.URL})
@@ -92,8 +104,7 @@ func (s *CreativeDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
s.ShowMediaPanel(b)
case data == "delete_media":
- s.MediaType = ""
- s.MediaFileID = ""
+ s.MediaItems = nil
s.MediaChanged = true
s.UpdateCreativePreview(b)
s.showManagementPanelWithDelete(b)
@@ -166,10 +177,17 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
if !s.SetMediaFromMessage(u.Message) {
return
}
- s.DeleteUserMessage(b, u.Message.ID)
+ if u.Message.MediaGroupID != "" {
+ groupID := u.Message.MediaGroupID
+ s.scheduleMediaGroupAction(groupID, func() {
+ s.UpdateCreativePreview(b)
+ s.ShowMediaPanel(b)
+ })
+ b.MarkHandled()
+ return
+ }
s.UpdateCreativePreview(b)
- s.ClearInputMode()
- s.showManagementPanelWithDelete(b)
+ s.ShowMediaPanel(b)
return
case inputModeButtonText:
if u.Message.Text == "" {
@@ -220,7 +238,6 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
}
text := ui.FormatMessageHTML(u.Message)
s.Text = &text
-
s.DeleteUserMessage(b, u.Message.ID)
s.UpdateCreativePreview(b)
@@ -233,7 +250,6 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
if u.Message.Text != "" {
text := ui.FormatMessageHTML(u.Message)
s.Text = &text
-
s.DeleteUserMessage(b, u.Message.ID)
s.UpdateCreativePreview(b)
@@ -279,13 +295,8 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
Text: s.Text,
}
if s.MediaChanged {
- mediaType := s.MediaType
- mediaFileID := s.MediaFileID
- input.MediaType = &mediaType
- input.MediaFileID = &mediaFileID
-
- if s.MediaFileID != "" {
- mediaData, err := b.DownloadFileBytes(s.MediaFileID)
+ if len(s.MediaItems) > 0 {
+ mediaItems, err := s.BuildMediaInputs(b)
if err != nil {
log.Error().Err(err).Msg("Failed to download creative media")
// Показываем ошибку через панель управления
@@ -295,10 +306,10 @@ func (s *CreativeDetails) updateCreative(b *bot.Bot) {
})
return
}
- input.MediaData = &mediaData
+ input.MediaItems = &mediaItems
} else {
- emptyData := []byte{}
- input.MediaData = &emptyData
+ emptyItems := []backend.CreativeMediaInput{}
+ input.MediaItems = &emptyItems
}
}
diff --git a/tg_bot/screens/creative_editor.go b/tg_bot/screens/creative_editor.go
index 8d68378..e367dcf 100644
--- a/tg_bot/screens/creative_editor.go
+++ b/tg_bot/screens/creative_editor.go
@@ -4,8 +4,11 @@ import (
"fmt"
"regexp"
"strings"
+ "sync"
+ "time"
"github.com/NicoNex/echotron/v3"
+ "github.com/TelegramExchange/tgex-backend/tg_bot/backend"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
"github.com/rs/zerolog/log"
@@ -17,6 +20,11 @@ type InlineButton struct {
URL string
}
+type MediaItem struct {
+ MediaType string // "photo", "video", "animation"
+ MediaFileID string // File ID из Telegram
+}
+
// CreativeEditorFields содержит общие поля и методы для создания/редактирования креатива
type CreativeEditorFields struct {
// Данные креатива
@@ -25,8 +33,7 @@ type CreativeEditorFields struct {
Buttons []InlineButton
// Медиа
- MediaType string // "photo", "video", "animation"
- MediaFileID string // File ID из Telegram
+ MediaItems []MediaItem
MediaChanged bool
// Режим ввода
@@ -39,15 +46,20 @@ type CreativeEditorFields struct {
CreativeMessageID *int
// ID сообщения с панелью управления
ControlPanelMessageID *int
+ // ID сообщений дополнительного медиа (после первого)
+ ExtraMediaMessageIDs []int
+
+ pendingMediaGroupMu sync.Mutex
+ pendingMediaGroupID string
+ pendingMediaGroupTimer *time.Timer
+ pendingMediaGroupGen int64
// Предыдущее состояние для определения изменения типа сообщения
- previousHasMedia bool
- previousMediaType string
- previousMediaFileID string
+ previousMediaItems []MediaItem
}
const inviteLinkPlaceholder = "{{invite_link}}"
-const inviteLinkPreviewURL = "https://t.me/+nlOUmIsLGAlmZTMy" //ссылка приглашения для предпросмотра
+const inviteLinkPreviewURL = "https://t.me/joinchat/nlOUmIsLGAlmZTMy" //ссылка приглашения для предпросмотра
const buttonURLPlaceholder = "https://example.com"
var inviteLinkPattern = regexp.MustCompile(`https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+`)
@@ -85,6 +97,77 @@ func buildCreativeKeyboard(buttons [][]echotron.InlineKeyboardButton) echotron.I
return keyboard
}
+func copyMediaItems(items []MediaItem) []MediaItem {
+ if len(items) == 0 {
+ return nil
+ }
+ clone := make([]MediaItem, len(items))
+ copy(clone, items)
+ return clone
+}
+
+func mediaItemsEqual(a, b []MediaItem) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i].MediaType != b[i].MediaType || a[i].MediaFileID != b[i].MediaFileID {
+ return false
+ }
+ }
+ return true
+}
+
+func (e *CreativeEditorFields) primaryMedia() *MediaItem {
+ if len(e.MediaItems) == 0 {
+ return nil
+ }
+ return &e.MediaItems[0]
+}
+
+func (e *CreativeEditorFields) hasMultipleMedia() bool {
+ return len(e.MediaItems) > 1
+}
+
+func (e *CreativeEditorFields) clearExtraMediaMessages(b *bot.Bot) {
+ for _, msgID := range e.ExtraMediaMessageIDs {
+ b.DeleteMessage(b.ChatID, msgID)
+ }
+ e.ExtraMediaMessageIDs = nil
+}
+
+const mediaGroupDebounce = 2 * time.Second
+
+func (e *CreativeEditorFields) scheduleMediaGroupAction(groupID string, action func()) {
+ e.pendingMediaGroupMu.Lock()
+ defer e.pendingMediaGroupMu.Unlock()
+
+ if groupID == "" {
+ action()
+ return
+ }
+
+ if e.pendingMediaGroupTimer != nil {
+ e.pendingMediaGroupTimer.Stop()
+ }
+ e.pendingMediaGroupID = groupID
+ e.pendingMediaGroupGen++
+ gen := e.pendingMediaGroupGen
+ e.pendingMediaGroupTimer = time.AfterFunc(mediaGroupDebounce, func() {
+ e.pendingMediaGroupMu.Lock()
+ currentGroup := e.pendingMediaGroupID
+ currentGen := e.pendingMediaGroupGen
+ if currentGen == gen && currentGroup == groupID {
+ e.pendingMediaGroupID = ""
+ e.pendingMediaGroupTimer = nil
+ }
+ e.pendingMediaGroupMu.Unlock()
+ if currentGen == gen && currentGroup == groupID {
+ action()
+ }
+ })
+}
+
func isTelegramParseError(err error) bool {
if err == nil {
return false
@@ -188,14 +271,66 @@ func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
}
// Если есть медиа, отправляем с медиа
- if e.MediaFileID != "" {
+ if len(e.MediaItems) > 1 {
+ e.clearExtraMediaMessages(b)
+ group := make([]echotron.GroupableInputMedia, 0, len(e.MediaItems))
+ for i, item := range e.MediaItems {
+ caption := ""
+ parseMode := echotron.ParseMode("")
+ if i == 0 {
+ caption = creativeText
+ parseMode = echotron.HTML
+ }
+ switch item.MediaType {
+ case "photo":
+ group = append(group, echotron.InputMediaPhoto{
+ Type: echotron.MediaTypePhoto,
+ Media: echotron.NewInputFileID(item.MediaFileID),
+ Caption: caption,
+ ParseMode: parseMode,
+ })
+ case "video":
+ group = append(group, echotron.InputMediaVideo{
+ Type: echotron.MediaTypeVideo,
+ Media: echotron.NewInputFileID(item.MediaFileID),
+ Caption: caption,
+ ParseMode: parseMode,
+ })
+ case "animation":
+ group = append(group, echotron.InputMediaVideo{
+ Type: echotron.MediaTypeVideo,
+ Media: echotron.NewInputFileID(item.MediaFileID),
+ Caption: caption,
+ ParseMode: parseMode,
+ })
+ default:
+ log.Warn().Str("media_type", item.MediaType).Msg("Unsupported media type for media group")
+ }
+ }
+
+ if len(group) > 0 {
+ res, err := b.SendMediaGroup(b.ChatID, group, nil)
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to send media group")
+ if id, err := sendText(); err == nil {
+ msgID = id
+ } else if isTelegramParseError(err) {
+ if id, err := sendFallback(); err == nil {
+ msgID = id
+ }
+ }
+ } else if len(res.Result) > 0 {
+ msgID = res.Result[0].ID
+ }
+ }
+ } else if media := e.primaryMedia(); media != nil {
var res echotron.APIResponseMessage
var err error
- switch e.MediaType {
+ switch media.MediaType {
case "photo":
res, err = b.SendPhoto(
- echotron.NewInputFileID(e.MediaFileID),
+ echotron.NewInputFileID(media.MediaFileID),
b.ChatID,
&echotron.PhotoOptions{
Caption: creativeText,
@@ -205,7 +340,7 @@ func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
)
case "video":
res, err = b.SendVideo(
- echotron.NewInputFileID(e.MediaFileID),
+ echotron.NewInputFileID(media.MediaFileID),
b.ChatID,
&echotron.VideoOptions{
Caption: creativeText,
@@ -215,7 +350,7 @@ func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
)
case "animation":
res, err = b.SendAnimation(
- echotron.NewInputFileID(e.MediaFileID),
+ echotron.NewInputFileID(media.MediaFileID),
b.ChatID,
&echotron.AnimationOptions{
Caption: creativeText,
@@ -226,7 +361,7 @@ func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
}
if err != nil {
- log.Error().Err(err).Str("media_type", e.MediaType).Msg("Failed to send media")
+ log.Error().Err(err).Str("media_type", media.MediaType).Msg("Failed to send media")
// Fallback to text message - используем прямой SendMessage
if id, err := sendText(); err == nil {
msgID = id
@@ -238,6 +373,7 @@ func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
} else if res.Result != nil {
msgID = res.Result.ID
}
+
} else {
// Отправляем обычное текстовое сообщение напрямую (не через SendNew)
// чтобы не затронуть LastMessageID и не затронуть cleanup
@@ -262,9 +398,7 @@ func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
e.CreativeMessageID = &msgID
- e.previousHasMedia = e.MediaFileID != ""
- e.previousMediaType = e.MediaType
- e.previousMediaFileID = e.MediaFileID
+ e.previousMediaItems = copyMediaItems(e.MediaItems)
}
// UpdateCreativePreview обновляет превью креатива
@@ -276,15 +410,18 @@ func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
return
}
- currentHasMedia := e.MediaFileID != ""
+ currentHasMedia := len(e.MediaItems) > 0
+ previousHasMedia := len(e.previousMediaItems) > 0
+ mediaItemsChanged := !mediaItemsEqual(e.MediaItems, e.previousMediaItems)
// Если тип сообщения изменился (текст ↔ медиа), нужно пересоздать оба сообщения
- if currentHasMedia != e.previousHasMedia {
+ if currentHasMedia != previousHasMedia {
// Удаляем оба сообщения
if e.CreativeMessageID != nil {
b.DeleteMessage(b.ChatID, *e.CreativeMessageID)
e.CreativeMessageID = nil
}
+ e.clearExtraMediaMessages(b)
if e.ControlPanelMessageID != nil {
b.DeleteMessage(b.ChatID, *e.ControlPanelMessageID)
e.ControlPanelMessageID = nil
@@ -295,6 +432,16 @@ func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
return
}
+ if mediaItemsChanged && (len(e.MediaItems) > 1 || len(e.previousMediaItems) > 1) {
+ if e.CreativeMessageID != nil {
+ b.DeleteMessage(b.ChatID, *e.CreativeMessageID)
+ e.CreativeMessageID = nil
+ }
+ e.clearExtraMediaMessages(b)
+ e.SendCreativePreview(b)
+ return
+ }
+
// Тип не изменился - просто редактируем на месте
creativeText := e.GetCreativeText()
creativeButtons := e.GetCreativeButtons()
@@ -308,10 +455,14 @@ func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
if len(runes) > 200 {
textPreview = string(runes[:200]) + "..."
}
+ mediaType := ""
+ if media := e.primaryMedia(); media != nil {
+ mediaType = media.MediaType
+ }
log.Error().
Err(err).
Int("creative_message_id", *e.CreativeMessageID).
- Str("media_type", e.MediaType).
+ Str("media_type", mediaType).
Int("text_len", len([]rune(creativeText))).
Str("text_preview", textPreview).
Msg(msg)
@@ -319,38 +470,42 @@ func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
// Если есть медиа - редактируем caption
updated := false
- if e.MediaFileID != "" {
- mediaChanged := e.MediaFileID != e.previousMediaFileID || e.MediaType != e.previousMediaType
+ if media := e.primaryMedia(); media != nil {
+ var prevMedia MediaItem
+ if len(e.previousMediaItems) > 0 {
+ prevMedia = e.previousMediaItems[0]
+ }
+ mediaChanged := media.MediaFileID != prevMedia.MediaFileID || media.MediaType != prevMedia.MediaType
if mediaChanged {
- var media echotron.InputMedia
- switch e.MediaType {
+ var inputMedia echotron.InputMedia
+ switch media.MediaType {
case "photo":
- media = echotron.InputMediaPhoto{
+ inputMedia = echotron.InputMediaPhoto{
Type: echotron.MediaTypePhoto,
- Media: echotron.NewInputFileID(e.MediaFileID),
+ Media: echotron.NewInputFileID(media.MediaFileID),
Caption: creativeText,
ParseMode: echotron.HTML,
}
case "video":
- media = echotron.InputMediaVideo{
+ inputMedia = echotron.InputMediaVideo{
Type: echotron.MediaTypeVideo,
- Media: echotron.NewInputFileID(e.MediaFileID),
+ Media: echotron.NewInputFileID(media.MediaFileID),
Caption: creativeText,
ParseMode: echotron.HTML,
}
case "animation":
- media = echotron.InputMediaAnimation{
+ inputMedia = echotron.InputMediaAnimation{
Type: echotron.MediaTypeAnimation,
- Media: echotron.NewInputFileID(e.MediaFileID),
+ Media: echotron.NewInputFileID(media.MediaFileID),
Caption: creativeText,
ParseMode: echotron.HTML,
}
}
- if media != nil {
+ if inputMedia != nil {
_, err := b.EditMessageMedia(
msgID,
- media,
+ inputMedia,
&echotron.MessageMediaOptions{
ReplyMarkup: keyboard,
},
@@ -361,7 +516,7 @@ func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
updated = true
}
} else {
- log.Warn().Str("media_type", e.MediaType).Msg("Unsupported media type for edit")
+ log.Warn().Str("media_type", media.MediaType).Msg("Unsupported media type for edit")
}
} else {
_, err := b.EditMessageCaption(
@@ -399,9 +554,11 @@ func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
}
if updated {
- e.previousHasMedia = currentHasMedia
- e.previousMediaType = e.MediaType
- e.previousMediaFileID = e.MediaFileID
+ if currentHasMedia {
+ e.previousMediaItems = copyMediaItems(e.MediaItems)
+ } else {
+ e.previousMediaItems = nil
+ }
}
}
@@ -413,21 +570,23 @@ func (e *CreativeEditorFields) DeleteUserMessage(b *bot.Bot, messageID int) {
}
func (e *CreativeEditorFields) SetMediaFromMessage(message *echotron.Message) bool {
+ var item MediaItem
switch {
case message.Photo != nil && len(message.Photo) > 0:
photo := message.Photo[len(message.Photo)-1]
- e.MediaType = "photo"
- e.MediaFileID = photo.FileID
+ item.MediaType = "photo"
+ item.MediaFileID = photo.FileID
case message.Video != nil:
- e.MediaType = "video"
- e.MediaFileID = message.Video.FileID
+ item.MediaType = "video"
+ item.MediaFileID = message.Video.FileID
case message.Animation != nil:
- e.MediaType = "animation"
- e.MediaFileID = message.Animation.FileID
+ item.MediaType = "animation"
+ item.MediaFileID = message.Animation.FileID
default:
return false
}
+ e.MediaItems = append(e.MediaItems, item)
e.MediaChanged = true
return true
}
@@ -496,13 +655,21 @@ func (e *CreativeEditorFields) BuildEditorButtons(confirmText, confirmCallback,
Button("✎ Текст", "edit_text"),
Button("+ Медиа", "add_media"),
))
- buttons = append(buttons, Row(Button("+ Добавить кнопку", "add_button")))
+ addButtonLabel := "+ Добавить кнопку"
+ addButtonCallback := "add_button"
+ if e.hasMultipleMedia() {
+ addButtonLabel = "Кнопки недоступны для >1 медиа"
+ addButtonCallback = "empty"
+ }
+ buttons = append(buttons, Row(Button(addButtonLabel, addButtonCallback)))
// Кнопка удаления медиа (если медиа добавлено)
- if e.MediaFileID != "" {
- buttons = append(buttons, Row(
- Button("⌫ Убрать медиа", "delete_media"),
- ))
+ if len(e.MediaItems) > 0 {
+ label := "⌫ Убрать медиа"
+ if len(e.MediaItems) > 1 {
+ label = fmt.Sprintf("⌫ Убрать медиа (%d)", len(e.MediaItems))
+ }
+ buttons = append(buttons, Row(Button(label, "delete_media")))
}
// Кнопки удаления (если есть кнопки)
@@ -566,3 +733,25 @@ func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons
b.Edit(text, keyboard)
}
}
+
+func (e *CreativeEditorFields) BuildMediaInputs(b *bot.Bot) ([]backend.CreativeMediaInput, error) {
+ if len(e.MediaItems) == 0 {
+ return nil, nil
+ }
+ mediaItems := make([]backend.CreativeMediaInput, 0, len(e.MediaItems))
+ for _, item := range e.MediaItems {
+ if item.MediaFileID == "" {
+ continue
+ }
+ mediaData, err := b.DownloadFileBytes(item.MediaFileID)
+ if err != nil {
+ return nil, err
+ }
+ mediaItems = append(mediaItems, backend.CreativeMediaInput{
+ MediaType: item.MediaType,
+ MediaFileID: item.MediaFileID,
+ MediaData: mediaData,
+ })
+ }
+ return mediaItems, nil
+}
diff --git a/tg_bot/screens/creative_editor_ui.go b/tg_bot/screens/creative_editor_ui.go
index 1f53a13..a6a0736 100644
--- a/tg_bot/screens/creative_editor_ui.go
+++ b/tg_bot/screens/creative_editor_ui.go
@@ -35,7 +35,8 @@ const msgAddMediaPrompt = `
Отправьте фото, видео или GIF
-Это медиа будет отображаться в креативе вместе с текстом
+Можно добавить несколько медиа — отправляйте по одному.
+Нажмите «Отмена», когда закончите.
`
const msgButtonURLPrompt = `