This commit is contained in:
Artem Tsyrulnikov
2026-01-15 22:02:16 +03:00
parent 6ec26c96e3
commit 049024ebe8
72 changed files with 2417 additions and 2054 deletions

View File

@@ -4,25 +4,30 @@ import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"example.com/m/adapter/backend"
"example.com/m/bot"
"example.com/m/ui"
"github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log"
)
type AddCreative struct {
CreativeEditorFields // Встраивание общих полей
type AddCreativeCtx struct {
CreativeEditorFields
ProjectID string
WorkspaceID string
BackState bot.State
WaitingForCreative bool // Ждем пересылки сообщения с креативом
CreativeMessageSent bool // Креатив уже получен и показан
WaitingForTextEdit bool // Ждем редактирования текста креатива
ProjectID string
WorkspaceID string
BackState bot.State
}
type AddCreativeStart struct{ ctx *AddCreativeCtx }
type AddCreativeEdit struct{ ctx *AddCreativeCtx }
type AddCreativeInput struct{ ctx *AddCreativeCtx }
const msgWaitCreative = `
<b> Добавить креатив</b>
@@ -31,15 +36,102 @@ const msgWaitCreative = `
• Обрабатываем любое сообщение Telegram
• Отправляйте сразу оформленный пост
На следующем шаге можно добавить кнопки или изменить контент
`
const msgConfirmCreativeFormat = `
<b> Добавляем этот креатив?</b>
<b>Название креатива:</b> %s
<i>Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.</i>
`
const msgDownloadMediaError = `
<b>❌ Не удалось загрузить медиа</b>
Попробуйте удалить медиа и добавить заново.
`
const msgInviteLinkRequired = `
<b>❌ Ошибка валидации</b>
Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала!
<b>Формат ссылки:</b>
<code>https://t.me/+xxx</code>
<b>Пример правильного текста:</b>
<i>Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn</i>
<b>Нажмите "✎ Текст" ниже чтобы исправить.</b>
`
const msgInviteLinkTooMany = `
<b>❌ Слишком много ссылок</b>
Текст должен содержать <b>ТОЛЬКО ОДНУ</b> инвайт-ссылку.
У вас их несколько. Удалите лишние.
<b>Нажмите "✎ Текст" ниже чтобы исправить.</b>
`
const msgCreateError = `
<b>❌ Ошибка создания</b>
Не удалось создать креатив.
Попробуйте ещё раз или вернитесь назад.
`
const msgCreativeCreated = `
<b>✅ Креатив успешно создан!</b>
📄 <b>Название:</b> %s
`
const msgCreativeCreatedButtons = `
🔘 <b>Кнопок добавлено:</b> %d
`
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
func (s *AddCreativeStart) Enter(b *bot.Bot, mode bot.RenderMode) {
kb := Keyboard(Row(Button("« Отмена", "cancel")))
s.WaitingForCreative = true
b.Render(msgWaitCreative, kb, mode)
}
func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
func (s *AddCreativeStart) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
if u.CallbackQuery.Data == "cancel" && s.ctx.BackState != nil {
b.SetState(s.ctx.BackState, bot.EditMessage)
}
}
func (s *AddCreativeStart) HandleMessage(b *bot.Bot, u *echotron.Update) {
if u.Message == nil {
return
}
s.ctx.extractCreativeFromMessage(u.Message)
if s.ctx.Name == nil {
name := s.ctx.generateCreativeName()
s.ctx.Name = &name
}
s.ctx.DeleteUserMessage(b, u.Message.ID)
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.NewMessage)
}
func (s *AddCreativeStart) Handle(_ *bot.Bot, _ *echotron.Update) {}
func (s *AddCreativeStart) Exit() {}
func (s *AddCreativeEdit) Enter(b *bot.Bot, _ bot.RenderMode) {
s.ctx.showCreativeConfirmation(b)
}
func (s *AddCreativeEdit) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
@@ -48,413 +140,175 @@ func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
switch {
case data == "cancel":
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
if s.ctx.BackState != nil {
b.SetState(s.ctx.BackState, bot.EditMessage)
}
case data == "edit_text":
// Начинаем редактирование текста
s.WaitingForTextEdit = true
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{
Row(Button("« Отмена", "cancel_edit")),
})
s.ctx.InputMode = inputModeText
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
case data == "add_button":
// Начинаем процесс добавления кнопки
s.WaitingForButtonText = true
s.PendingButtonType = "invite"
s.ShowControlPanel(b, `<b> Добавить кнопку</b>
Введите <b>текст кнопки:</b>
<i>Например:</i>
• Перейти на сайт
• Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
Row(
Button("✓ Ссылка на канал", "button_type_invite"),
Button("Своя ссылка", "button_type_custom"),
),
Row(Button("« Отмена", "cancel_add_button")),
})
s.ctx.InputMode = inputModeButtonText
s.ctx.PendingButtonType = "invite"
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
case data == "add_media":
// Начинаем процесс добавления медиа
s.WaitingForMedia = true
s.ShowControlPanel(b, `<b> Добавить медиа</b>
Отправьте <b>фото, видео или GIF</b>
<i>Это медиа будет отображаться в креативе вместе с текстом</i>`, [][]echotron.InlineKeyboardButton{
Row(Button("« Отмена", "cancel_media")),
})
s.ctx.InputMode = inputModeMedia
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
case data == "delete_media":
// Удаляем медиа
s.MediaType = ""
s.MediaFileID = ""
s.MediaChanged = true
s.UpdateCreativePreview(b)
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
case data == "cancel_add_button":
// Отменяем добавление кнопки
// Если мы уже создали кнопку с placeholder - удаляем её
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
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
case data == "button_type_invite":
s.PendingButtonType = "invite"
s.ShowControlPanel(b, `<b> Добавить кнопку</b>
Введите <b>текст кнопки:</b>
<i>Например:</i>
• Перейти на сайт
• Написать нам
<i>Ссылка на канал подставится автоматически при создании закупа.</i>`, [][]echotron.InlineKeyboardButton{
Row(
Button("✓ Ссылка на канал", "button_type_invite"),
Button("Своя ссылка", "button_type_custom"),
),
Row(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{
Row(
Button("Ссылка на канал", "button_type_invite"),
Button("✓ Своя ссылка", "button_type_custom"),
),
Row(Button("« Отмена", "cancel_add_button")),
})
case data == "cancel_edit":
// Отменяем редактирование текста
s.WaitingForTextEdit = false
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
case data == "cancel_media":
// Отменяем добавление медиа
s.WaitingForMedia = false
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
s.ctx.MediaType = ""
s.ctx.MediaFileID = ""
s.ctx.MediaChanged = true
s.ctx.UpdateCreativePreview(b)
s.Enter(b, bot.EditMessage)
case data == "confirm_create":
// Создаём креатив
s.createCreative(b)
s.ctx.createCreative(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)
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
}
}
indexStr := strings.TrimPrefix(data, "delete_button:")
index, err := strconv.Atoi(indexStr)
if err != nil || index < 0 || index >= len(s.ctx.Buttons) {
return
}
s.ctx.Buttons = append(s.ctx.Buttons[:index], s.ctx.Buttons[index+1:]...)
s.ctx.UpdateCreativePreview(b)
s.Enter(b, bot.EditMessage)
default:
s.Enter(b, bot.NewMessage)
}
}
func (s *AddCreative) HandleMessage(b *bot.Bot, u *echotron.Update) {
func (s *AddCreativeEdit) HandleMessage(b *bot.Bot, u *echotron.Update) {
if u.Message == nil || u.Message.Text == "" {
return
}
newName := u.Message.Text
runes := []rune(newName)
if len(runes) > 200 {
newName = string(runes[:200])
}
s.ctx.Name = &newName
s.ctx.DeleteUserMessage(b, u.Message.ID)
s.Enter(b, bot.EditMessage)
}
func (s *AddCreativeEdit) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *AddCreativeEdit) Exit() {}
func (s *AddCreativeInput) Enter(b *bot.Bot, _ bot.RenderMode) {
switch s.ctx.InputMode {
case inputModeText:
s.ctx.ShowTextEditPanel(b)
case inputModeButtonText:
s.ctx.ShowAddButtonPanel(b)
case inputModeButtonURL:
s.ctx.ShowButtonURLPanel(b)
case inputModeMedia:
s.ctx.ShowMediaPanel(b)
}
}
func (s *AddCreativeInput) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
switch u.CallbackQuery.Data {
case "button_type_invite":
s.ctx.PendingButtonType = "invite"
s.ctx.ShowAddButtonPanel(b)
case "button_type_custom":
s.ctx.PendingButtonType = "custom"
s.ctx.ShowAddButtonPanel(b)
case "cancel_add_button":
s.ctx.CancelPendingButton(b)
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
case "cancel_edit", "cancel_media":
s.ctx.ClearInputMode()
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
}
}
func (s *AddCreativeInput) HandleMessage(b *bot.Bot, u *echotron.Update) {
if u.Message == nil {
return
}
// Шаг 1: Ждем пересылки креатива (любое сообщение)
if s.WaitingForCreative {
// Извлекаем данные из сообщения
s.extractCreativeFromMessage(u.Message)
// Генерируем название из текста или дефолтное
if s.Name == nil {
name := s.generateCreativeName()
s.Name = &name
}
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
s.WaitingForCreative = false
s.CreativeMessageSent = true
// Показываем превью + панель с предложением добавить
s.showCreativeConfirmation(b)
return
}
// Обработка медиа (когда явно просим добавить медиа)
if s.WaitingForMedia {
handled := false
if u.Message.Photo != nil && len(u.Message.Photo) > 0 {
photo := u.Message.Photo[len(u.Message.Photo)-1]
s.MediaType = "photo"
s.MediaFileID = photo.FileID
s.MediaChanged = true
handled = true
} else if u.Message.Video != nil {
s.MediaType = "video"
s.MediaFileID = u.Message.Video.FileID
s.MediaChanged = true
handled = true
} else if u.Message.Animation != nil {
s.MediaType = "animation"
s.MediaFileID = u.Message.Animation.FileID
s.MediaChanged = true
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.WaitingForMedia = false
s.UpdateCreativePreview(b)
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
return
}
}
// Обработка добавления кнопки - текст кнопки
if s.WaitingForButtonText {
switch s.ctx.InputMode {
case inputModeText:
if u.Message.Text == "" {
return
}
text := ui.FormatMessageHTML(u.Message)
s.ctx.Text = &text
s.ctx.DeleteUserMessage(b, u.Message.ID)
s.ctx.ClearInputMode()
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
case inputModeButtonText:
if u.Message.Text == "" {
return
}
buttonText := u.Message.Text
s.CurrentButtonText = &buttonText
s.WaitingForButtonText = false
s.ctx.DeleteUserMessage(b, u.Message.ID)
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
if s.ctx.PendingButtonType == "custom" {
s.ctx.AddCustomButtonPlaceholder(buttonText)
s.ctx.UpdateCreativePreview(b)
s.ctx.InputMode = inputModeButtonURL
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
return
}
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>
URL должен начинаться с <code>https://</code>
<i>Например:</i>
<code>https://example.com</code>
<code>https://t.me/yourchannel</code>`, [][]echotron.InlineKeyboardButton{
Row(Button("« Отмена", "cancel_add_button")),
})
} else {
s.Buttons = append(s.Buttons, InlineButton{
Text: buttonText,
URL: inviteLinkPlaceholder,
})
s.UpdateCreativePreview(b)
s.PendingButtonType = ""
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
}
return
}
// Обработка добавления кнопки - URL
if s.WaitingForButtonURL {
s.ctx.AddInviteButton(buttonText)
s.ctx.UpdateCreativePreview(b)
s.ctx.PendingButtonType = ""
s.ctx.ClearInputMode()
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
case inputModeButtonURL:
if u.Message.Text == "" {
return
}
url := strings.TrimSpace(u.Message.Text)
s.ctx.DeleteUserMessage(b, u.Message.ID)
url := u.Message.Text
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
if !strings.HasPrefix(url, "https://") {
s.ShowControlPanel(b, `<b>❌ Неверный формат URL</b>
URL должен начинаться с <code>https://</code>
Попробуйте ещё раз.`, [][]echotron.InlineKeyboardButton{
Row(Button("« Отмена", "cancel_add_button")),
})
if !s.ctx.IsValidButtonURL(url, true) {
s.ctx.ShowInvalidButtonURLPanel(b)
return
}
if len(s.Buttons) > 0 {
s.Buttons[len(s.Buttons)-1].URL = url
s.ctx.UpdateLastButtonURL(url)
s.ctx.UpdateCreativePreview(b)
s.ctx.PendingButtonType = ""
s.ctx.ClearInputMode()
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
case inputModeMedia:
if !s.ctx.SetMediaFromMessage(u.Message) {
return
}
s.WaitingForButtonURL = false
s.CurrentButtonText = nil
s.PendingButtonType = ""
s.UpdateCreativePreview(b)
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
return
}
// Если ждем редактирования текста креатива
if s.WaitingForTextEdit && 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.WaitingForTextEdit = false
s.UpdateCreativePreview(b)
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
return
}
// Если креатив уже показан и пользователь вводит текст - это изменение названия
if s.CreativeMessageSent && u.Message.Text != "" {
newName := u.Message.Text
// Ограничение 200 символов
runes := []rune(newName)
if len(runes) > 200 {
newName = string(runes[:200])
}
s.Name = &newName
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
// Обновляем панель с новым названием
s.showCreativeConfirmation(b)
return
s.ctx.DeleteUserMessage(b, u.Message.ID)
s.ctx.UpdateCreativePreview(b)
s.ctx.ClearInputMode()
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
}
}
func (s *AddCreative) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *AddCreativeInput) Handle(_ *bot.Bot, _ *echotron.Update) {}
// extractCreativeFromMessage извлекает данные креатива из сообщения
func (s *AddCreative) extractCreativeFromMessage(message *echotron.Message) {
// Извлекаем текст с форматированием (GetFormattedText теперь поддерживает и text и caption)
func (s *AddCreativeInput) Exit() {}
func (s *AddCreativeCtx) extractCreativeFromMessage(message *echotron.Message) {
if message.Text != "" || message.Caption != "" {
text := s.GetFormattedText(message)
text := ui.FormatMessageHTML(message)
if text != "" {
s.Text = &text
}
}
// Извлекаем медиа
if message.Photo != nil && len(message.Photo) > 0 {
photo := message.Photo[len(message.Photo)-1]
s.MediaType = "photo"
s.MediaFileID = photo.FileID
s.MediaChanged = true
} else if message.Video != nil {
s.MediaType = "video"
s.MediaFileID = message.Video.FileID
s.MediaChanged = true
} else if message.Animation != nil {
s.MediaType = "animation"
s.MediaFileID = message.Animation.FileID
s.MediaChanged = true
}
s.SetMediaFromMessage(message)
// Извлекаем кнопки из ReplyMarkup
if message.ReplyMarkup != nil && len(message.ReplyMarkup.InlineKeyboard) > 0 {
for _, row := range message.ReplyMarkup.InlineKeyboard {
for _, button := range row {
// Берем только кнопки с URL
if button.URL != "" {
s.Buttons = append(s.Buttons, InlineButton{
Text: button.Text,
@@ -466,10 +320,8 @@ func (s *AddCreative) extractCreativeFromMessage(message *echotron.Message) {
}
}
// generateCreativeName генерирует название из текста или дефолтное
func (s *AddCreative) generateCreativeName() string {
func (s *AddCreativeCtx) generateCreativeName() string {
if s.Text != nil && *s.Text != "" {
// Удаляем ВСЕ HTML теги с помощью regex
re := regexp.MustCompile(`<[^>]*>`)
cleanText := re.ReplaceAllString(*s.Text, "")
@@ -485,119 +337,28 @@ func (s *AddCreative) generateCreativeName() string {
return "Новый креатив"
}
// escapeHTML экранирует HTML-символы для безопасного вывода
func escapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
return s
}
// buildEditorButtons создает кнопки для панели редактирования креатива
func (s *AddCreative) buildEditorButtons(confirmText, cancelCallback string) [][]echotron.InlineKeyboardButton {
var buttons [][]echotron.InlineKeyboardButton
// Кнопки редактирования
buttons = append(buttons, Row(
Button("✎ Текст", "edit_text"),
Button(" Медиа", "add_media"),
))
buttons = append(buttons, Row(Button(" Добавить кнопку", "add_button")))
// Кнопка удаления медиа (если медиа добавлено)
if s.MediaFileID != "" {
buttons = append(buttons, Row(
Button("⌫ Убрать медиа", "delete_media"),
))
}
// Кнопки удаления добавленных кнопок
if len(s.Buttons) > 0 {
var row []echotron.InlineKeyboardButton
for i, btn := range s.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)
}
}
// Финальные кнопки
if s.Text != nil {
buttons = append(buttons, Row(
Button("← "+cancelCallback, "cancel"),
Button(confirmText, "confirm_create"),
))
} else {
buttons = append(buttons, Row(
Button("← "+cancelCallback, "cancel"),
))
}
return buttons
}
// showCreativeConfirmation показывает превью креатива + панель с предложением добавить
func (s *AddCreative) showCreativeConfirmation(b *bot.Bot) {
// 1. Отправляем/обновляем превью креатива
func (s *AddCreativeCtx) showCreativeConfirmation(b *bot.Bot) {
if s.CreativeMessageID == nil {
s.SendCreativePreview(b)
} else {
s.UpdateCreativePreview(b)
}
// 2. Формируем текст панели управления
// Экранируем HTML-символы в названии для безопасного вывода
escapedName := escapeHTML(*s.Name)
confirmText := fmt.Sprintf(`<b> Добавляем этот креатив?</b>
<b>Название креатива:</b> %s
<i>Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.</i>`, escapedName)
// 3. Показываем панель с кнопками редактирования + сохранения
buttons := s.buildEditorButtons("✓ Сохранить", "Отмена")
escapedName := ui.EscapeHTML(*s.Name)
confirmText := fmt.Sprintf(msgConfirmCreativeFormat, escapedName)
buttons := s.BuildEditorButtons("✓ Сохранить", "confirm_create", "Отмена", "cancel")
s.ShowControlPanel(b, confirmText, buttons)
}
// sendCreativeAndPanel отправляет превью креатива и панель управления
func (s *AddCreative) sendCreativeAndPanel(b *bot.Bot) {
// 1. Отправляем превью креатива
s.SendCreativePreview(b)
// 2. Отправляем панель управления
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
func (s *AddCreative) createCreative(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 login", Keyboard())
return
}
func (s *AddCreativeCtx) createCreative(b *bot.Bot) {
var err error
var mediaData []byte
if s.MediaFileID != "" {
mediaData, err = b.DownloadFileBytes(s.MediaFileID)
if err != nil {
log.Error().Err(err).Msg("Failed to download creative media")
// Показываем ошибку через панель управления
buttons := s.buildEditorButtons("✓ Попробовать снова", "Отмена")
s.ShowControlPanel(b, "<b>❌ Не удалось загрузить медиа</b>\n\nПопробуйте удалить медиа и добавить заново.", buttons)
buttons := s.BuildEditorButtons("✓ Попробовать снова", "confirm_create", "Отмена", "cancel")
s.ShowControlPanel(b, msgDownloadMediaError, buttons)
return
}
}
@@ -610,84 +371,41 @@ func (s *AddCreative) createCreative(b *bot.Bot) {
})
}
// Логируем что отправляем
log.Info().
Str("name", *s.Name).
Str("text", *s.Text).
Int("buttons_count", len(buttons)).
Interface("buttons", buttons).
Str("media_type", s.MediaType).
Msg("Creating creative with data")
// Создаём креатив через API
creative, err := b.Backend.CreateCreative(
context.Background(),
jwt,
s.WorkspaceID,
s.ProjectID,
backend.CreateCreativeInput{
Name: *s.Name,
Text: *s.Text,
MediaType: s.MediaType,
MediaFileID: s.MediaFileID,
MediaData: mediaData,
Buttons: buttons,
},
)
creative, err := b.Backend.CreateCreative(context.Background(), b.Session.JWT, s.WorkspaceID, s.ProjectID, backend.CreateCreativeInput{
Name: *s.Name,
Text: *s.Text,
MediaType: s.MediaType,
MediaFileID: s.MediaFileID,
MediaData: mediaData,
Buttons: buttons,
})
if err != nil {
log.Error().Err(err).Msg("Failed to create creative")
// Проверяем, это ошибка валидации инвайт-ссылки
errMsg := err.Error()
var userMsg string
if strings.Contains(errMsg, "Creative text must contain one invite link") {
userMsg = `<b>❌ Ошибка валидации</b>
Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала!
<b>Формат ссылки:</b>
<code>https://t.me/+xxx</code>
<b>Пример правильного текста:</b>
<i>Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn</i>
<b>Нажмите "✎ Текст" ниже чтобы исправить.</b>`
userMsg = msgInviteLinkRequired
} else if strings.Contains(errMsg, "Creative text must contain only one invite link") {
userMsg = `<b>❌ Слишком много ссылок</b>
Текст должен содержать <b>ТОЛЬКО ОДНУ</b> инвайт-ссылку.
У вас их несколько. Удалите лишние.
<b>Нажмите "✎ Текст" ниже чтобы исправить.</b>`
userMsg = msgInviteLinkTooMany
} else {
userMsg = `<b>❌ Ошибка создания</b>
Не удалось создать креатив.
Попробуйте ещё раз или вернитесь назад.`
userMsg = msgCreateError
}
// Показываем ошибку через панель управления, чтобы не ломать порядок сообщений
buttons := s.buildEditorButtons("✓ Попробовать снова", "Отмена")
buttons := s.BuildEditorButtons("✓ Попробовать снова", "confirm_create", "Отмена", "cancel")
s.ShowControlPanel(b, userMsg, buttons)
return
}
// Успех! Показываем сообщение и возвращаемся
successText := `<b>✅ Креатив успешно создан!</b>
📄 <b>Название:</b> ` + creative.Name
successText := fmt.Sprintf(msgCreativeCreated, creative.Name)
if len(s.Buttons) > 0 {
successText += fmt.Sprintf(`
🔘 <b>Кнопок добавлено:</b> %d`, len(s.Buttons))
successText += fmt.Sprintf(msgCreativeCreatedButtons, len(s.Buttons))
}
b.SendNew(successText, Keyboard())
// Возвращаемся на экран креативов
if s.BackState != nil {
b.SetState(s.BackState, bot.NewMessage)
}