Files
tgex-backend/telegram_bot/screens/creative_details.go
Artem Tsyrulnikov eb54b01835 фиксы
2026-01-08 21:50:04 +03:00

515 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package screens
import (
"context"
"fmt"
"strings"
"example.com/m/adapter/backend"
"example.com/m/bot"
"github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log"
)
type CreativeDetails struct {
CreativeEditorFields // Встраивание общих полей
CreativeID string
ProjectID string
WorkspaceID string
BackState bot.State
// Флаг подтверждения удаления
confirmingDelete bool
}
func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
// JWT уже создан в Bot.Update(), просто берем из сессии
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", bot.Keyboard())
return
}
// Загружаем креатив
creative, err := b.Backend.GetCreative(context.Background(), jwt, s.WorkspaceID, s.CreativeID)
if err != nil {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to get creative")
b.SendNew("<b>❌ Не удалось загрузить креатив</b>", bot.Keyboard(
bot.Row(bot.Button("← Назад", "back")),
))
return
}
// Заполняем поля из загруженного креатива
s.Name = &creative.Name
if creative.Text != "" {
s.Text = &creative.Text
log.Info().Str("creative_text", creative.Text).Msg("Set creative text from backend")
} else {
log.Info().Msg("Creative text is empty from backend")
}
s.MediaType = creative.MediaType
s.MediaFileID = creative.MediaFileID
s.Buttons = nil
for _, btn := range creative.Buttons {
s.Buttons = append(s.Buttons, InlineButton{Text: btn.Text, URL: btn.URL})
}
s.MediaChanged = false
log.Info().
Str("creative_id", s.CreativeID).
Str("name", creative.Name).
Bool("has_text", creative.Text != "").
Bool("s_text_is_nil", s.Text == nil).
Msg("Sending creative preview and panel")
// Отправляем превью и панель управления
s.SendCreativePreview(b)
s.showManagementPanelWithDelete(b)
}
func (s *CreativeDetails) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
// Обрабатываем общие callbacks (edit_text, add_button, add_media, delete_button, delete_media)
switch {
case data == "cancel", data == "back":
// Отмена - возвращаемся назад
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
}
case data == "edit_text":
s.ShowControlPanel(b, `<b>✎ Введите новый текст креатива:</b>
⚠ <b>Важно:</b> Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала
<b>Формат ссылки:</b>
<code>https://t.me/+xxx</code>
<b>Пример:</b>
<i>Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена редактирования", "cancel_edit")),
})
case data == "add_button":
s.WaitingForButtonText = true
s.PendingButtonType = "invite"
s.ShowControlPanel(b, `<b> Добавить кнопку</b>
Введите <b>текст кнопки:</b>
<i>Например:</i>
• Перейти на сайт
• Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(
bot.Button("✓ Ссылка на канал", "button_type_invite"),
bot.Button("Своя ссылка", "button_type_custom"),
),
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
case data == "add_media":
s.WaitingForMedia = true
s.ShowControlPanel(b, `<b> Добавить медиа</b>
Отправьте <b>фото, видео или GIF</b>
<i>Это медиа будет отображаться в креативе вместе с текстом</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_media")),
})
case data == "delete_media":
s.MediaType = ""
s.MediaFileID = ""
s.MediaChanged = true
s.UpdateCreativePreview(b)
s.showManagementPanelWithDelete(b)
case data == "cancel_add_button":
// Отменяем добавление кнопки
if s.WaitingForButtonURL && len(s.Buttons) > 0 {
lastButton := s.Buttons[len(s.Buttons)-1]
if lastButton.URL == "https://example.com" || lastButton.URL == inviteLinkPlaceholder {
s.Buttons = s.Buttons[:len(s.Buttons)-1]
s.UpdateCreativePreview(b)
}
}
s.WaitingForButtonText = false
s.WaitingForButtonURL = false
s.PendingButtonType = ""
s.CurrentButtonText = nil
s.showManagementPanelWithDelete(b)
case data == "button_type_invite":
s.PendingButtonType = "invite"
s.ShowControlPanel(b, `<b> Добавить кнопку</b>
Введите <b>текст кнопки:</b>
<i>Например:</i>
• Перейти на сайт
• Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(
bot.Button("✓ Ссылка на канал", "button_type_invite"),
bot.Button("Своя ссылка", "button_type_custom"),
),
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
case data == "button_type_custom":
s.PendingButtonType = "custom"
s.ShowControlPanel(b, `<b> Добавить кнопку</b>
Введите <b>текст кнопки:</b>
<i>Например:</i>
• Перейти на сайт
• Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
bot.Row(
bot.Button("Ссылка на канал", "button_type_invite"),
bot.Button("✓ Своя ссылка", "button_type_custom"),
),
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
case data == "cancel_edit", data == "cancel_media":
s.WaitingForMedia = false
s.showManagementPanelWithDelete(b)
case data == "confirm_save":
s.updateCreative(b)
case data == "delete_creative":
if !s.confirmingDelete {
// Первое нажатие - просим подтверждение
s.confirmingDelete = true
s.showManagementPanelWithDelete(b)
} else {
// Второе нажатие - удаляем
s.deleteCreative(b)
}
case data == "confirm_delete":
// Это только для безопасности, главная логика в delete_creative
s.deleteCreative(b)
case strings.HasPrefix(data, "delete_button:"):
parts := strings.Split(data, ":")
if len(parts) == 2 {
var index int
if _, err := fmt.Sscanf(parts[1], "%d", &index); err == nil {
if index >= 0 && index < len(s.Buttons) {
s.Buttons = append(s.Buttons[:index], s.Buttons[index+1:]...)
s.UpdateCreativePreview(b)
s.showManagementPanelWithDelete(b)
}
}
}
default:
// Сбрасываем флаг подтверждения удаления при любом другом действии
if s.confirmingDelete {
s.confirmingDelete = false
s.showManagementPanelWithDelete(b)
}
}
}
func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
if u.Message == nil {
return
}
// Обрабатываем добавление медиа
if s.WaitingForMedia {
handled := false
if u.Message.Photo != nil && len(u.Message.Photo) > 0 {
photos := u.Message.Photo
largestPhoto := photos[len(photos)-1]
s.MediaType = "photo"
s.MediaFileID = largestPhoto.FileID
s.MediaChanged = true
s.WaitingForMedia = false
handled = true
} else if u.Message.Video != nil {
s.MediaType = "video"
s.MediaFileID = u.Message.Video.FileID
s.MediaChanged = true
s.WaitingForMedia = false
handled = true
} else if u.Message.Animation != nil {
s.MediaType = "animation"
s.MediaFileID = u.Message.Animation.FileID
s.MediaChanged = true
s.WaitingForMedia = false
handled = true
}
if handled {
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
s.UpdateCreativePreview(b)
s.showManagementPanelWithDelete(b)
return
}
}
// Обрабатываем ввод текста кнопки
if s.WaitingForButtonText && u.Message.Text != "" {
buttonText := u.Message.Text
s.CurrentButtonText = &buttonText
s.WaitingForButtonText = false
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
// Создаём кнопку с placeholder URL
if s.PendingButtonType == "custom" {
s.WaitingForButtonURL = true
s.Buttons = append(s.Buttons, InlineButton{
Text: buttonText,
URL: "https://example.com",
})
s.UpdateCreativePreview(b)
s.ShowControlPanel(b, `<b>✧ Введите URL для кнопки:</b>
<i>Например:</i>
<code>https://example.com</code>`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
} else {
s.Buttons = append(s.Buttons, InlineButton{
Text: buttonText,
URL: inviteLinkPlaceholder,
})
s.UpdateCreativePreview(b)
s.PendingButtonType = ""
s.showManagementPanelWithDelete(b)
}
return
}
// Обрабатываем ввод URL кнопки
if s.WaitingForButtonURL && u.Message.Text != "" {
url := strings.TrimSpace(u.Message.Text)
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
// Простая валидация URL
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
s.ShowControlPanel(b, `<b>❌ Неверный формат URL</b>
URL должен начинаться с <code>http://</code> или <code>https://</code>
Попробуйте ещё раз:`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
return
}
// Обновляем URL последней кнопки
if len(s.Buttons) > 0 {
s.Buttons[len(s.Buttons)-1].URL = url
}
s.WaitingForButtonURL = false
s.CurrentButtonText = nil
s.PendingButtonType = ""
s.UpdateCreativePreview(b)
s.showManagementPanelWithDelete(b)
return
}
// Обрабатываем редактирование текста креатива
if !s.WaitingForButtonText && !s.WaitingForButtonURL && !s.WaitingForMedia && u.Message.Text != "" {
text := s.GetFormattedText(u.Message)
s.Text = &text
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
s.UpdateCreativePreview(b)
s.showManagementPanelWithDelete(b)
return
}
// Обрабатываем имя креатива (если нет других состояний)
if u.Message.Text != "" {
text := s.GetFormattedText(u.Message)
s.Text = &text
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
s.UpdateCreativePreview(b)
s.showManagementPanelWithDelete(b)
}
}
func (s *CreativeDetails) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *CreativeDetails) showManagementPanelWithDelete(b *bot.Bot) {
var confirmText, confirmCallback string
if s.confirmingDelete {
confirmText = "✖ Подтвердить удаление"
confirmCallback = "confirm_delete"
} else {
confirmText = "✓ Сохранить"
confirmCallback = "confirm_save"
}
// Добавляем кнопку удаления креатива
deleteButton := bot.Row(bot.Button("⌦ Удалить креатив", "delete_creative"))
s.ShowManagementPanel(b, confirmText, confirmCallback, "cancel", deleteButton)
}
func (s *CreativeDetails) updateCreative(b *bot.Bot) {
// JWT уже создан в Bot.Update(), просто берем из сессии
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", bot.Keyboard())
return
}
var err error
// Подготавливаем данные для обновления
input := backend.UpdateCreativeInput{
Name: s.Name,
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 err != nil {
log.Error().Err(err).Msg("Failed to download creative media")
// Показываем ошибку через панель управления
s.ShowControlPanel(b, "<b>❌ Не удалось загрузить медиа</b>\n\nПопробуйте удалить медиа и добавить заново, или нажмите \"Сохранить\" без изменения медиа.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")),
bot.Row(bot.Button("✓ Сохранить без медиа", "confirm_save")),
})
return
}
input.MediaData = &mediaData
} else {
emptyData := []byte{}
input.MediaData = &emptyData
}
}
buttons := make([]backend.CreativeButton, 0, len(s.Buttons))
for _, button := range s.Buttons {
buttons = append(buttons, backend.CreativeButton{
Text: button.Text,
URL: button.URL,
})
}
input.Buttons = &buttons
// Отправляем запрос на обновление
_, err = b.Backend.UpdateCreative(
context.Background(),
jwt,
s.WorkspaceID,
s.CreativeID,
input,
)
if err != nil {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to update creative")
// Показываем ошибку через панель управления
s.ShowControlPanel(b, "<b>❌ Не удалось сохранить изменения</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")),
bot.Row(bot.Button("✓ Попробовать снова", "confirm_save")),
})
return
}
log.Info().Str("creative_id", s.CreativeID).Msg("Creative updated successfully")
// Возвращаемся назад
if s.BackState != nil {
b.SendNew("✅ Креатив успешно обновлен!", bot.Keyboard())
b.SetState(s.BackState, bot.EditMessage)
}
}
func (s *CreativeDetails) deleteCreative(b *bot.Bot) {
// JWT уже создан в Bot.Update(), просто берем из сессии
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start", bot.Keyboard())
return
}
// Отправляем запрос на удаление
err := b.Backend.DeleteCreative(
context.Background(),
jwt,
s.WorkspaceID,
s.CreativeID,
)
if err != nil {
log.Error().Err(err).Str("creative_id", s.CreativeID).Msg("Failed to delete creative")
// Показываем ошибку через панель управления
s.confirmingDelete = false // Сбрасываем флаг подтверждения
s.ShowControlPanel(b, "<b>❌ Не удалось удалить креатив</b>\n\nПроверьте подключение и попробуйте снова.", [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("← Назад", "back")),
bot.Row(bot.Button("⌦ Попробовать снова", "delete_creative")),
})
return
}
log.Info().Str("creative_id", s.CreativeID).Msg("Creative deleted successfully")
// Возвращаемся назад
if s.BackState != nil {
b.SendNew("✅ Креатив успешно удален!", bot.Keyboard())
b.SetState(s.BackState, bot.EditMessage)
}
}