бот несколько медиа
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
// === Обработка входящего события ===
|
||||
|
||||
@@ -19,6 +19,7 @@ type AddCreativeCtx struct {
|
||||
|
||||
ProjectID string
|
||||
BackState bot.State
|
||||
WaitMessageSent bool
|
||||
}
|
||||
|
||||
type AddCreativeStart struct{ Ctx *AddCreativeCtx }
|
||||
@@ -40,9 +41,9 @@ const msgWaitCreative = `
|
||||
const msgConfirmCreativeFormat = `
|
||||
<b>➕ Добавляем этот креатив?</b>
|
||||
|
||||
<b>Название креатива:</b> %s
|
||||
<b>Название:</b> %s
|
||||
|
||||
<i>Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.</i>
|
||||
<i>Чтобы изменить название, отправь мне сообщение 👇:</i>
|
||||
|
||||
`
|
||||
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.ClearInputMode()
|
||||
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
|
||||
s.ctx.ShowMediaPanel(b)
|
||||
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
|
||||
})
|
||||
b.MarkHandled()
|
||||
return
|
||||
}
|
||||
s.ctx.UpdateCreativePreview(b)
|
||||
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{
|
||||
input := backend.CreateCreativeInput{
|
||||
Name: *s.Name,
|
||||
Text: *s.Text,
|
||||
MediaType: s.MediaType,
|
||||
MediaFileID: s.MediaFileID,
|
||||
MediaData: mediaData,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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.ClearInputMode()
|
||||
s.showManagementPanelWithDelete(b)
|
||||
s.ShowMediaPanel(b)
|
||||
})
|
||||
b.MarkHandled()
|
||||
return
|
||||
}
|
||||
s.UpdateCreativePreview(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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ const msgAddMediaPrompt = `
|
||||
|
||||
Отправьте <b>фото, видео или GIF</b>
|
||||
|
||||
<i>Это медиа будет отображаться в креативе вместе с текстом</i>
|
||||
<i>Можно добавить несколько медиа — отправляйте по одному.</i>
|
||||
<i>Нажмите «Отмена», когда закончите.</i>
|
||||
|
||||
`
|
||||
const msgButtonURLPrompt = `
|
||||
|
||||
Reference in New Issue
Block a user