395 lines
12 KiB
Go
395 lines
12 KiB
Go
package screens
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"example.com/m/bot"
|
||
"github.com/NicoNex/echotron/v3"
|
||
"github.com/rs/zerolog/log"
|
||
)
|
||
|
||
// InlineButton представляет кнопку с URL
|
||
type InlineButton struct {
|
||
Text string
|
||
URL string
|
||
}
|
||
|
||
// CreativeEditorFields содержит общие поля и методы для создания/редактирования креатива
|
||
type CreativeEditorFields struct {
|
||
// Данные креатива
|
||
Name *string
|
||
Text *string
|
||
Buttons []InlineButton
|
||
|
||
// Медиа
|
||
MediaType string // "photo", "video", "animation"
|
||
MediaFileID string // File ID из Telegram
|
||
|
||
// Состояние для добавления кнопки
|
||
WaitingForButtonText bool
|
||
WaitingForButtonURL bool
|
||
CurrentButtonText *string
|
||
WaitingForMedia bool
|
||
|
||
// ID сообщения с креативом (которое постоянно обновляется)
|
||
CreativeMessageID *int
|
||
// ID сообщения с панелью управления
|
||
ControlPanelMessageID *int
|
||
|
||
// Предыдущее состояние для определения изменения типа сообщения
|
||
previousHasMedia bool
|
||
}
|
||
|
||
// GetCreativeText возвращает текст для превью креатива
|
||
func (e *CreativeEditorFields) GetCreativeText() string {
|
||
if e.Text != nil && *e.Text != "" {
|
||
return *e.Text
|
||
}
|
||
return `<i>💭 Я твой креатив
|
||
|
||
У меня пока нет текста, добавь его ниже.
|
||
|
||
</i>`
|
||
}
|
||
|
||
// GetCreativeButtons возвращает кнопки для превью креатива
|
||
func (e *CreativeEditorFields) GetCreativeButtons() [][]echotron.InlineKeyboardButton {
|
||
var buttons [][]echotron.InlineKeyboardButton
|
||
|
||
for _, btn := range e.Buttons {
|
||
buttons = append(buttons, []echotron.InlineKeyboardButton{
|
||
{
|
||
Text: btn.Text,
|
||
URL: btn.URL,
|
||
},
|
||
})
|
||
}
|
||
|
||
return buttons
|
||
}
|
||
|
||
// SendCreativePreview отправляет превью креатива
|
||
func (e *CreativeEditorFields) SendCreativePreview(b *bot.Bot) {
|
||
creativeText := e.GetCreativeText()
|
||
creativeButtons := e.GetCreativeButtons()
|
||
|
||
// Создаем клавиатуру с пустым массивом (не nil!)
|
||
keyboard := echotron.InlineKeyboardMarkup{
|
||
InlineKeyboard: creativeButtons,
|
||
}
|
||
if len(creativeButtons) == 0 {
|
||
keyboard.InlineKeyboard = [][]echotron.InlineKeyboardButton{}
|
||
}
|
||
|
||
var msgID int
|
||
|
||
// Если есть медиа, отправляем с медиа
|
||
if e.MediaFileID != "" {
|
||
var res echotron.APIResponseMessage
|
||
var err error
|
||
|
||
switch e.MediaType {
|
||
case "photo":
|
||
res, err = b.SendPhoto(
|
||
echotron.NewInputFileID(e.MediaFileID),
|
||
b.ChatID,
|
||
&echotron.PhotoOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
case "video":
|
||
res, err = b.SendVideo(
|
||
echotron.NewInputFileID(e.MediaFileID),
|
||
b.ChatID,
|
||
&echotron.VideoOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
case "animation":
|
||
res, err = b.SendAnimation(
|
||
echotron.NewInputFileID(e.MediaFileID),
|
||
b.ChatID,
|
||
&echotron.AnimationOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
}
|
||
|
||
if err != nil {
|
||
log.Error().Err(err).Str("media_type", e.MediaType).Msg("Failed to send media")
|
||
// Fallback to text message - используем прямой SendMessage
|
||
res, err := b.SendMessage(creativeText, b.ChatID, &echotron.MessageOptions{
|
||
ReplyMarkup: keyboard,
|
||
ParseMode: echotron.HTML,
|
||
})
|
||
if err == nil && res.Result != nil {
|
||
msgID = res.Result.ID
|
||
}
|
||
} else if res.Result != nil {
|
||
msgID = res.Result.ID
|
||
}
|
||
} else {
|
||
// Отправляем обычное текстовое сообщение напрямую (не через SendNew)
|
||
// чтобы не обновлять LastMessageID и не затронуть cleanup
|
||
res, err := b.SendMessage(creativeText, b.ChatID, &echotron.MessageOptions{
|
||
ReplyMarkup: keyboard,
|
||
ParseMode: echotron.HTML,
|
||
})
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("Failed to send creative preview")
|
||
return
|
||
}
|
||
if res.Result != nil {
|
||
msgID = res.Result.ID
|
||
}
|
||
}
|
||
|
||
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
|
||
e.CreativeMessageID = &msgID
|
||
e.previousHasMedia = e.MediaFileID != ""
|
||
log.Info().Int("creative_msg_id", *e.CreativeMessageID).Bool("has_media", e.MediaFileID != "").Msg("Creative preview sent")
|
||
}
|
||
|
||
// UpdateCreativePreview обновляет превью креатива
|
||
func (e *CreativeEditorFields) UpdateCreativePreview(b *bot.Bot) {
|
||
if e.CreativeMessageID == nil {
|
||
log.Warn().Msg("UpdateCreativePreview: CreativeMessageID is nil")
|
||
// Если превью еще не создано, создаем его
|
||
e.SendCreativePreview(b)
|
||
return
|
||
}
|
||
|
||
currentHasMedia := e.MediaFileID != ""
|
||
|
||
log.Info().
|
||
Int("creative_msg_id", *e.CreativeMessageID).
|
||
Bool("has_media", currentHasMedia).
|
||
Bool("previous_has_media", e.previousHasMedia).
|
||
Str("media_type", e.MediaType).
|
||
Msg("Updating creative preview")
|
||
|
||
// Если тип сообщения изменился (текст ↔ медиа), нужно пересоздать оба сообщения
|
||
if currentHasMedia != e.previousHasMedia {
|
||
log.Info().Msg("Message type changed, recreating both messages")
|
||
|
||
// Удаляем оба сообщения
|
||
if e.CreativeMessageID != nil {
|
||
b.DeleteMessage(b.ChatID, *e.CreativeMessageID)
|
||
e.CreativeMessageID = nil
|
||
}
|
||
if e.ControlPanelMessageID != nil {
|
||
b.DeleteMessage(b.ChatID, *e.ControlPanelMessageID)
|
||
e.ControlPanelMessageID = nil
|
||
}
|
||
|
||
// Создаем превью заново (панель будет создана через ShowManagementPanel после этого)
|
||
e.SendCreativePreview(b)
|
||
return
|
||
}
|
||
|
||
// Тип не изменился - просто редактируем на месте
|
||
creativeText := e.GetCreativeText()
|
||
creativeButtons := e.GetCreativeButtons()
|
||
|
||
keyboard := echotron.InlineKeyboardMarkup{
|
||
InlineKeyboard: creativeButtons,
|
||
}
|
||
if len(creativeButtons) == 0 {
|
||
keyboard.InlineKeyboard = [][]echotron.InlineKeyboardButton{}
|
||
}
|
||
|
||
msgID := echotron.NewMessageID(b.ChatID, *e.CreativeMessageID)
|
||
|
||
// Если есть медиа - редактируем caption
|
||
if e.MediaFileID != "" {
|
||
_, err := b.EditMessageCaption(
|
||
msgID,
|
||
&echotron.MessageCaptionOptions{
|
||
Caption: creativeText,
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("Failed to edit media caption")
|
||
} else {
|
||
e.previousHasMedia = currentHasMedia
|
||
}
|
||
} else {
|
||
// Текстовое сообщение - редактируем текст
|
||
_, err := b.EditMessageText(
|
||
creativeText,
|
||
msgID,
|
||
&echotron.MessageTextOptions{
|
||
ParseMode: echotron.HTML,
|
||
ReplyMarkup: keyboard,
|
||
},
|
||
)
|
||
if err != nil {
|
||
log.Error().Err(err).Msg("Failed to edit message text")
|
||
} else {
|
||
e.previousHasMedia = currentHasMedia
|
||
}
|
||
}
|
||
}
|
||
|
||
// ShowManagementPanel показывает панель управления креативом
|
||
// confirmButtonText - текст кнопки подтверждения (например "✓ Сохранить")
|
||
// confirmCallback - callback для кнопки подтверждения
|
||
// cancelCallback - callback для кнопки отмены
|
||
// extraButtons - дополнительные кнопки которые будут добавлены перед финальными кнопками
|
||
func (e *CreativeEditorFields) ShowManagementPanel(b *bot.Bot, confirmButtonText, confirmCallback, cancelCallback string, extraButtons ...[]echotron.InlineKeyboardButton) {
|
||
text := `<b>👆Превью креатива выше</b> — так он будет выглядеть при отправке.
|
||
|
||
Выберите, что изменить:`
|
||
|
||
var buttons [][]echotron.InlineKeyboardButton
|
||
|
||
// Блок редактирования контента
|
||
buttons = append(buttons, bot.Row(
|
||
bot.Button("✎ Текст", "edit_text"),
|
||
bot.Button("⧉ Медиа", "add_media"),
|
||
))
|
||
// Добавление кнопки сразу под основным блоком
|
||
buttons = append(buttons, bot.Row(bot.Button("+ Добавить кнопку", "add_button")))
|
||
|
||
// Кнопка удаления медиа (если медиа добавлено)
|
||
if e.MediaFileID != "" {
|
||
buttons = append(buttons, bot.Row(
|
||
bot.Button("⌫ Убрать медиа", "delete_media"),
|
||
))
|
||
}
|
||
|
||
// Кнопки удаления (если есть кнопки)
|
||
if len(e.Buttons) > 0 {
|
||
var row []echotron.InlineKeyboardButton
|
||
for i, btn := range e.Buttons {
|
||
row = append(row, bot.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)
|
||
}
|
||
|
||
// Финальные кнопки
|
||
if e.Text != nil {
|
||
buttons = append(buttons, bot.Row(
|
||
bot.Button("← Назад", cancelCallback),
|
||
bot.Button(confirmButtonText, confirmCallback),
|
||
))
|
||
} else {
|
||
buttons = append(buttons, bot.Row(
|
||
bot.Button("← Назад", cancelCallback),
|
||
))
|
||
}
|
||
|
||
e.ShowControlPanel(b, text, buttons)
|
||
}
|
||
|
||
// ShowControlPanel редактирует панель управления с произвольным текстом и кнопками
|
||
func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons [][]echotron.InlineKeyboardButton) {
|
||
if e.ControlPanelMessageID == nil {
|
||
// Отправляем новую панель
|
||
keyboard := bot.Keyboard(buttons...)
|
||
b.SendNew(text, keyboard)
|
||
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
|
||
controlPanelMsgID := b.LastMessageID
|
||
e.ControlPanelMessageID = &controlPanelMsgID
|
||
log.Info().Int("control_panel_msg_id", *e.ControlPanelMessageID).Msg("Control panel sent")
|
||
} else {
|
||
// Редактируем существующую панель
|
||
log.Info().Int("control_panel_msg_id", *e.ControlPanelMessageID).Msg("Editing control panel")
|
||
oldLastMessageID := b.LastMessageID
|
||
b.LastMessageID = *e.ControlPanelMessageID
|
||
|
||
keyboard := bot.Keyboard(buttons...)
|
||
b.Edit(text, keyboard)
|
||
|
||
b.LastMessageID = oldLastMessageID
|
||
log.Info().Msg("Control panel updated")
|
||
}
|
||
}
|
||
|
||
// GetFormattedText извлекает форматированный текст из сообщения с entities
|
||
func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) string {
|
||
if message.Text == "" {
|
||
return ""
|
||
}
|
||
|
||
// Если нет entities - возвращаем текст как есть
|
||
if len(message.Entities) == 0 {
|
||
return message.Text
|
||
}
|
||
|
||
// Преобразуем текст в руны для правильной работы с UTF-8
|
||
runes := []rune(message.Text)
|
||
result := ""
|
||
lastPos := 0
|
||
|
||
// Обрабатываем entities
|
||
for _, entity := range message.Entities {
|
||
// Добавляем текст до entity
|
||
if entity.Offset > lastPos {
|
||
result += string(runes[lastPos:entity.Offset])
|
||
}
|
||
|
||
// Получаем текст entity
|
||
entityText := string(runes[entity.Offset : entity.Offset+entity.Length])
|
||
|
||
// Оборачиваем в соответствующие HTML-теги
|
||
switch entity.Type {
|
||
case "bold":
|
||
result += "<b>" + entityText + "</b>"
|
||
case "italic":
|
||
result += "<i>" + entityText + "</i>"
|
||
case "underline":
|
||
result += "<u>" + entityText + "</u>"
|
||
case "strikethrough":
|
||
result += "<s>" + entityText + "</s>"
|
||
case "code":
|
||
result += "<code>" + entityText + "</code>"
|
||
case "pre":
|
||
result += "<pre>" + entityText + "</pre>"
|
||
case "text_link":
|
||
if entity.URL != "" {
|
||
result += fmt.Sprintf("<a href=\"%s\">%s</a>", entity.URL, entityText)
|
||
} else {
|
||
result += entityText
|
||
}
|
||
case "url":
|
||
result += entityText
|
||
default:
|
||
result += entityText
|
||
}
|
||
|
||
lastPos = entity.Offset + entity.Length
|
||
}
|
||
|
||
// Добавляем оставшийся текст
|
||
if lastPos < len(runes) {
|
||
result += string(runes[lastPos:])
|
||
}
|
||
|
||
return result
|
||
}
|