775 lines
23 KiB
Go
775 lines
23 KiB
Go
package screens
|
||
|
||
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"
|
||
)
|
||
|
||
// InlineButton представляет кнопку с URL
|
||
type InlineButton struct {
|
||
Text string
|
||
URL string
|
||
}
|
||
|
||
type MediaItem struct {
|
||
MediaType string // "photo", "video", "animation"
|
||
MediaFileID string // File ID из Telegram
|
||
}
|
||
|
||
// CreativeEditorFields содержит общие поля и методы для создания/редактирования креатива
|
||
type CreativeEditorFields struct {
|
||
// Данные креатива
|
||
Name *string
|
||
Text *string
|
||
Buttons []InlineButton
|
||
Tag *string // "testing" or "production"
|
||
|
||
// Медиа
|
||
MediaItems []MediaItem
|
||
MediaChanged bool
|
||
|
||
// Режим ввода
|
||
InputMode string
|
||
|
||
// Состояние для добавления кнопки
|
||
PendingButtonType string
|
||
|
||
// ID сообщения с креативом (которое постоянно обновляется)
|
||
CreativeMessageID *int
|
||
// ID сообщения с панелью управления
|
||
ControlPanelMessageID *int
|
||
// ID сообщений дополнительного медиа (после первого)
|
||
ExtraMediaMessageIDs []int
|
||
|
||
pendingMediaGroupMu sync.Mutex
|
||
pendingMediaGroupID string
|
||
pendingMediaGroupTimer *time.Timer
|
||
pendingMediaGroupGen int64
|
||
|
||
// Предыдущее состояние для определения изменения типа сообщения
|
||
previousMediaItems []MediaItem
|
||
}
|
||
|
||
const inviteLinkPlaceholder = "{{invite_link}}"
|
||
const inviteLinkPreviewURL = "https://t.me/joinchat/nlOUmIsLGAlmZTMy" //ссылка приглашения для предпросмотра
|
||
const buttonURLPlaceholder = "https://example.com"
|
||
|
||
var inviteLinkPattern = regexp.MustCompile(`https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+`)
|
||
var inviteLinkTagPattern = regexp.MustCompile(`<a\s+class="tg-link">\s*(.*?)\s*</a>`)
|
||
|
||
const (
|
||
inputModeText = "text"
|
||
inputModeButtonText = "button_text"
|
||
inputModeButtonURL = "button_url"
|
||
inputModeMedia = "media"
|
||
)
|
||
const msgCreativeDefault = `
|
||
<i>💭 Я твой креатив
|
||
|
||
У меня пока нет текста, добавь его ниже.
|
||
|
||
</i>
|
||
`
|
||
const msgCreativeManagement = `
|
||
<b>👆Превью креатива выше</b> — так он будет выглядеть при отправке.
|
||
|
||
Выберите, что изменить:
|
||
|
||
`
|
||
const msgCreativePreviewBroken = "⚠️ Не удалось показать превью: креатив содержит некорректную разметку. Создайте его заново или отредактируйте текст."
|
||
|
||
func buildCreativeKeyboard(buttons [][]echotron.InlineKeyboardButton) echotron.InlineKeyboardMarkup {
|
||
keyboard := echotron.InlineKeyboardMarkup{
|
||
InlineKeyboard: buttons,
|
||
}
|
||
if len(buttons) == 0 {
|
||
keyboard.InlineKeyboard = [][]echotron.InlineKeyboardButton{}
|
||
}
|
||
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
|
||
}
|
||
|
||
func (e *CreativeEditorFields) CleanupMessages(b *bot.Bot) {
|
||
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
|
||
}
|
||
b.LastMessageID = 0
|
||
}
|
||
|
||
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
|
||
}
|
||
msg := err.Error()
|
||
return strings.Contains(msg, "can't parse entities") || strings.Contains(msg, "Unexpected end tag")
|
||
}
|
||
|
||
// GetCreativeText возвращает текст для превью креатива
|
||
func (e *CreativeEditorFields) GetCreativeText() string {
|
||
if e.Text == nil {
|
||
return msgCreativeDefault
|
||
}
|
||
|
||
text := strings.TrimSpace(ui.SanitizeHTML(*e.Text))
|
||
if text == "" {
|
||
return msgCreativeDefault
|
||
}
|
||
|
||
// Заменяем плейсхолдеры tg-link на кликабельную preview-ссылку.
|
||
text = inviteLinkTagPattern.ReplaceAllStringFunc(text, func(match string) string {
|
||
sub := inviteLinkTagPattern.FindStringSubmatch(match)
|
||
inner := ""
|
||
if len(sub) > 1 {
|
||
inner = strings.TrimSpace(sub[1])
|
||
}
|
||
if inner == "" {
|
||
inner = inviteLinkPreviewURL
|
||
}
|
||
return fmt.Sprintf(`<a href="%s">%s</a>`, inviteLinkPreviewURL, inner)
|
||
})
|
||
|
||
// Заменяем пригласительные ссылки на preview URL только для отображения.
|
||
text = inviteLinkPattern.ReplaceAllString(text, inviteLinkPreviewURL)
|
||
|
||
// Также заменяем плейсхолдер {{invite_link}} на preview URL (как в кнопках).
|
||
text = strings.ReplaceAll(text, inviteLinkPlaceholder, inviteLinkPreviewURL)
|
||
|
||
return text
|
||
}
|
||
|
||
// GetCreativeButtons возвращает кнопки для превью креатива
|
||
func (e *CreativeEditorFields) GetCreativeButtons() [][]echotron.InlineKeyboardButton {
|
||
var buttons [][]echotron.InlineKeyboardButton
|
||
|
||
for _, btn := range e.Buttons {
|
||
url := btn.URL
|
||
if url == inviteLinkPlaceholder {
|
||
url = inviteLinkPreviewURL
|
||
}
|
||
buttons = append(buttons, []echotron.InlineKeyboardButton{
|
||
{
|
||
Text: btn.Text,
|
||
URL: url,
|
||
},
|
||
})
|
||
}
|
||
|
||
return buttons
|
||
}
|
||
|
||
// SendCreativePreview отправляет превью креатива
|
||
func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
|
||
creativeText := e.GetCreativeText()
|
||
creativeButtons := e.GetCreativeButtons()
|
||
|
||
// Создаем клавиатуру с пустым массивом (не nil!)
|
||
keyboard := buildCreativeKeyboard(creativeButtons)
|
||
|
||
var msgID int
|
||
sendText := func() (int, error) {
|
||
res, err := b.SendMessage(creativeText, b.ChatID, &echotron.MessageOptions{
|
||
ReplyMarkup: keyboard,
|
||
ParseMode: echotron.HTML,
|
||
LinkPreviewOptions: echotron.LinkPreviewOptions{
|
||
IsDisabled: true,
|
||
},
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
if res.Result == nil {
|
||
return 0, fmt.Errorf("send message: empty result")
|
||
}
|
||
return res.Result.ID, nil
|
||
}
|
||
sendFallback := func() (int, error) {
|
||
res, err := b.SendMessage(msgCreativePreviewBroken, b.ChatID, &echotron.MessageOptions{
|
||
ReplyMarkup: keyboard,
|
||
LinkPreviewOptions: echotron.LinkPreviewOptions{
|
||
IsDisabled: true,
|
||
},
|
||
})
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
if res.Result == nil {
|
||
return 0, fmt.Errorf("send message: empty result")
|
||
}
|
||
return res.Result.ID, nil
|
||
}
|
||
|
||
// Если есть медиа, отправляем с медиа
|
||
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 media.MediaType {
|
||
case "photo":
|
||
res, err = b.SendPhoto(
|
||
echotron.NewInputFileID(media.MediaFileID),
|
||
b.ChatID,
|
||
&echotron.PhotoOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
case "video":
|
||
res, err = b.SendVideo(
|
||
echotron.NewInputFileID(media.MediaFileID),
|
||
b.ChatID,
|
||
&echotron.VideoOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
case "animation":
|
||
res, err = b.SendAnimation(
|
||
echotron.NewInputFileID(media.MediaFileID),
|
||
b.ChatID,
|
||
&echotron.AnimationOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
}
|
||
|
||
if err != nil {
|
||
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
|
||
} else if isTelegramParseError(err) {
|
||
if id, err := sendFallback(); err == nil {
|
||
msgID = id
|
||
}
|
||
}
|
||
} else if res.Result != nil {
|
||
msgID = res.Result.ID
|
||
}
|
||
|
||
} else {
|
||
// Отправляем обычное текстовое сообщение напрямую (не через SendNew)
|
||
// чтобы не затронуть LastMessageID и не затронуть cleanup
|
||
id, err := sendText()
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("Failed to send creative preview")
|
||
if isTelegramParseError(err) {
|
||
id, err = sendFallback()
|
||
if err == nil {
|
||
msgID = id
|
||
} else {
|
||
log.Error().Err(err).Msg("Failed to send fallback creative preview")
|
||
return
|
||
}
|
||
} else {
|
||
return
|
||
}
|
||
} else {
|
||
msgID = id
|
||
}
|
||
}
|
||
|
||
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
|
||
e.CreativeMessageID = &msgID
|
||
e.previousMediaItems = copyMediaItems(e.MediaItems)
|
||
|
||
// Устанавливаем LastMessageID чтобы ShowControlPanel мог корректно работать
|
||
// ВАЖНО: Это позволяет избежать конфликтов при навигации между экранами
|
||
b.LastMessageID = msgID
|
||
}
|
||
|
||
// UpdateCreativePreview обновляет превью креатива
|
||
func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
|
||
if e.CreativeMessageID == nil {
|
||
log.Warn().Msg("UpdateCreativePreview: CreativeMessageID is nil")
|
||
// Если превью еще не создано, создаем его
|
||
e.SendCreativePreview(b)
|
||
return
|
||
}
|
||
|
||
currentHasMedia := len(e.MediaItems) > 0
|
||
previousHasMedia := len(e.previousMediaItems) > 0
|
||
mediaItemsChanged := !mediaItemsEqual(e.MediaItems, e.previousMediaItems)
|
||
|
||
// Если тип сообщения изменился (текст ↔ медиа), нужно пересоздать оба сообщения
|
||
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
|
||
}
|
||
|
||
// Создаем превью заново (панель будет создана через ShowManagementPanel после этого)
|
||
e.SendCreativePreview(b)
|
||
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()
|
||
|
||
keyboard := buildCreativeKeyboard(creativeButtons)
|
||
|
||
msgID := echotron.NewMessageID(b.ChatID, *e.CreativeMessageID)
|
||
logEditError := func(msg string, err error) {
|
||
textPreview := creativeText
|
||
runes := []rune(textPreview)
|
||
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", mediaType).
|
||
Int("text_len", len([]rune(creativeText))).
|
||
Str("text_preview", textPreview).
|
||
Msg(msg)
|
||
}
|
||
|
||
// Если есть медиа - редактируем caption
|
||
updated := false
|
||
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 inputMedia echotron.InputMedia
|
||
switch media.MediaType {
|
||
case "photo":
|
||
inputMedia = echotron.InputMediaPhoto{
|
||
Type: echotron.MediaTypePhoto,
|
||
Media: echotron.NewInputFileID(media.MediaFileID),
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
}
|
||
case "video":
|
||
inputMedia = echotron.InputMediaVideo{
|
||
Type: echotron.MediaTypeVideo,
|
||
Media: echotron.NewInputFileID(media.MediaFileID),
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
}
|
||
case "animation":
|
||
inputMedia = echotron.InputMediaAnimation{
|
||
Type: echotron.MediaTypeAnimation,
|
||
Media: echotron.NewInputFileID(media.MediaFileID),
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
}
|
||
}
|
||
|
||
if inputMedia != nil {
|
||
_, err := b.EditMessageMedia(
|
||
msgID,
|
||
inputMedia,
|
||
&echotron.MessageMediaOptions{
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
if err != nil {
|
||
logEditError("Failed to edit message media", err)
|
||
} else {
|
||
updated = true
|
||
}
|
||
} else {
|
||
log.Warn().Str("media_type", media.MediaType).Msg("Unsupported media type for edit")
|
||
}
|
||
} else {
|
||
_, err := b.EditMessageCaption(
|
||
msgID,
|
||
&echotron.MessageCaptionOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
if err != nil {
|
||
logEditError("Failed to edit media caption", err)
|
||
} else {
|
||
updated = true
|
||
}
|
||
}
|
||
} else {
|
||
// Текстовое сообщение - редактируем текст
|
||
_, err := b.EditMessageText(
|
||
creativeText,
|
||
msgID,
|
||
&echotron.MessageTextOptions{
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
LinkPreviewOptions: echotron.LinkPreviewOptions{
|
||
IsDisabled: true,
|
||
},
|
||
},
|
||
)
|
||
if err != nil {
|
||
logEditError("Failed to edit message text", err)
|
||
} else {
|
||
updated = true
|
||
}
|
||
}
|
||
|
||
if updated {
|
||
if currentHasMedia {
|
||
e.previousMediaItems = copyMediaItems(e.MediaItems)
|
||
} else {
|
||
e.previousMediaItems = nil
|
||
}
|
||
}
|
||
}
|
||
|
||
func (e *CreativeEditorFields) DeleteUserMessage(b *bot.Bot, messageID int) {
|
||
_, err := b.DeleteMessage(b.ChatID, messageID)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("Failed to delete user message")
|
||
}
|
||
}
|
||
|
||
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]
|
||
item.MediaType = "photo"
|
||
item.MediaFileID = photo.FileID
|
||
case message.Video != nil:
|
||
item.MediaType = "video"
|
||
item.MediaFileID = message.Video.FileID
|
||
case message.Animation != nil:
|
||
item.MediaType = "animation"
|
||
item.MediaFileID = message.Animation.FileID
|
||
default:
|
||
return false
|
||
}
|
||
|
||
e.MediaItems = append(e.MediaItems, item)
|
||
e.MediaChanged = true
|
||
return true
|
||
}
|
||
|
||
func (e *CreativeEditorFields) AddInviteButton(text string) {
|
||
e.Buttons = append(e.Buttons, InlineButton{
|
||
Text: text,
|
||
URL: inviteLinkPlaceholder,
|
||
})
|
||
}
|
||
|
||
func (e *CreativeEditorFields) AddCustomButtonPlaceholder(text string) {
|
||
e.Buttons = append(e.Buttons, InlineButton{
|
||
Text: text,
|
||
URL: buttonURLPlaceholder,
|
||
})
|
||
}
|
||
|
||
func (e *CreativeEditorFields) UpdateLastButtonURL(url string) {
|
||
if len(e.Buttons) > 0 {
|
||
e.Buttons[len(e.Buttons)-1].URL = url
|
||
}
|
||
}
|
||
|
||
func (e *CreativeEditorFields) ClearInputMode() {
|
||
e.InputMode = ""
|
||
e.PendingButtonType = ""
|
||
}
|
||
|
||
func (e *CreativeEditorFields) CancelPendingButton(b *bot.Bot) {
|
||
if e.InputMode == inputModeButtonURL && len(e.Buttons) > 0 {
|
||
lastButton := e.Buttons[len(e.Buttons)-1]
|
||
if lastButton.URL == buttonURLPlaceholder || lastButton.URL == inviteLinkPlaceholder {
|
||
e.Buttons = e.Buttons[:len(e.Buttons)-1]
|
||
e.UpdateCreativePreview(b)
|
||
}
|
||
}
|
||
|
||
e.ClearInputMode()
|
||
}
|
||
|
||
func (e *CreativeEditorFields) IsValidButtonURL(url string, allowHTTP bool) bool {
|
||
if allowHTTP {
|
||
return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
|
||
}
|
||
return strings.HasPrefix(url, "https://")
|
||
}
|
||
|
||
// ShowManagementPanel показывает панель управления креативом
|
||
// confirmButtonText - текст кнопки подтверждения (например "✓ Сохранить")
|
||
// confirmCallback - callback для кнопки подтверждения
|
||
// cancelCallback - callback для кнопки отмены
|
||
// extraButtons - дополнительные кнопки которые будут добавлены перед финальными кнопками
|
||
func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText, confirmCallback, cancelCallback string, extraButtons ...[]echotron.InlineKeyboardButton) {
|
||
text := msgCreativeManagement
|
||
|
||
buttons := e.BuildEditorButtons(confirmButtonText, confirmCallback, "Назад", cancelCallback, extraButtons...)
|
||
e.ShowControlPanel(b, text, buttons)
|
||
}
|
||
|
||
func (e *CreativeEditorFields) BuildEditorButtons(confirmText, confirmCallback, cancelText, cancelCallback string, extraButtons ...[]echotron.InlineKeyboardButton) [][]echotron.InlineKeyboardButton {
|
||
var buttons [][]echotron.InlineKeyboardButton
|
||
|
||
// Блок редактирования контента
|
||
buttons = append(buttons, Row(
|
||
Button("✎ Текст", "edit_text"),
|
||
Button("+ Медиа", "add_media"),
|
||
))
|
||
addButtonLabel := "+ Добавить кнопку"
|
||
addButtonCallback := "add_button"
|
||
if e.hasMultipleMedia() {
|
||
addButtonLabel = "Кнопки недоступны для >1 медиа"
|
||
addButtonCallback = "empty"
|
||
}
|
||
buttons = append(buttons, Row(Button(addButtonLabel, addButtonCallback)))
|
||
|
||
// Кнопка удаления медиа (если медиа добавлено)
|
||
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")))
|
||
}
|
||
|
||
// Кнопки удаления (если есть кнопки)
|
||
if len(e.Buttons) > 0 {
|
||
var row []echotron.InlineKeyboardButton
|
||
for i, btn := range e.Buttons {
|
||
row = append(row, Button(
|
||
fmt.Sprintf(`⌫ Убрать «%s»`, btn.Text),
|
||
fmt.Sprintf("delete_button:%d", i),
|
||
))
|
||
|
||
if len(row) == 2 {
|
||
buttons = append(buttons, row)
|
||
row = nil
|
||
}
|
||
}
|
||
|
||
if len(row) > 0 {
|
||
buttons = append(buttons, row)
|
||
}
|
||
}
|
||
|
||
// Дополнительные кнопки (например "Удалить креатив")
|
||
for _, extraRow := range extraButtons {
|
||
buttons = append(buttons, extraRow)
|
||
}
|
||
|
||
// Финальные кнопки
|
||
cancelLabel := "← " + cancelText
|
||
if e.Text != nil {
|
||
buttons = append(buttons, Row(
|
||
Button(cancelLabel, cancelCallback),
|
||
Button(confirmText, confirmCallback),
|
||
))
|
||
} else {
|
||
buttons = append(buttons, Row(
|
||
Button(cancelLabel, cancelCallback),
|
||
))
|
||
}
|
||
|
||
return buttons
|
||
}
|
||
|
||
// ShowControlPanel редактирует панель управления с произвольным текстом и кнопками
|
||
func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons [][]echotron.InlineKeyboardButton) {
|
||
if e.ControlPanelMessageID == nil {
|
||
// Отправляем новую панель
|
||
keyboard := Keyboard(buttons...)
|
||
b.SendNew(text, keyboard)
|
||
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
|
||
controlPanelMsgID := b.LastMessageID
|
||
e.ControlPanelMessageID = &controlPanelMsgID
|
||
} else {
|
||
// Редактируем существующую панель
|
||
// Временно заменяем LastMessageID на ID панели управления
|
||
oldLastMessageID := b.LastMessageID
|
||
b.LastMessageID = *e.ControlPanelMessageID
|
||
defer func() { b.LastMessageID = oldLastMessageID }()
|
||
|
||
keyboard := Keyboard(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
|
||
}
|