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 AddCreative struct {
CreativeEditorFields // Встраивание общих полей
ProjectID string
WorkspaceID string
BackState bot.State
WaitingForCreative bool // Ждем пересылки сообщения с креативом
CreativeMessageSent bool // Креатив уже получен и показан
WaitingForTextEdit bool // Ждем редактирования текста креатива
}
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
text := `… › Креативы › Добавление
+ Добавить креатив
📄 Пришлите креатив который хотите добавить
• Обрабатываем любое сообщение Telegram
• Отправляйте сразу оформленный пост
• На следующем шаге можно добавить кнопки или изменить контент`
buttons := [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel")),
}
keyboard := bot.Keyboard(buttons...)
s.WaitingForCreative = true
b.Render(text, keyboard, mode)
}
func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
switch {
case data == "cancel":
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
}
case data == "edit_text":
// Начинаем редактирование текста
s.WaitingForTextEdit = true
s.ShowControlPanel(b, `✎ Введите текст креатива:
⚠ Важно: Текст должен содержать ОДНУ инвайт-ссылку вашего канала
Формат ссылки:
https://t.me/+xxx
Пример:
Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_edit")),
})
case data == "add_button":
// Начинаем процесс добавления кнопки
s.WaitingForButtonText = true
s.ShowControlPanel(b, `+ Добавить кнопку
Введите текст кнопки:
Например:
• Перейти на сайт
• Написать нам`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
case data == "add_media":
// Начинаем процесс добавления медиа
s.WaitingForMedia = true
s.ShowControlPanel(b, `+ Добавить медиа
Отправьте фото, видео или GIF
Это медиа будет отображаться в креативе вместе с текстом`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_media")),
})
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" {
s.Buttons = s.Buttons[:len(s.Buttons)-1]
s.UpdateCreativePreview(b)
}
}
s.WaitingForButtonText = false
s.WaitingForButtonURL = false
s.CurrentButtonText = nil
if s.CreativeMessageSent {
s.showCreativeConfirmation(b)
} else {
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
}
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")
}
case data == "confirm_create":
// Создаём креатив
s.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")
}
}
}
}
default:
s.Enter(b, bot.NewMessage)
}
}
func (s *AddCreative) 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 {
if u.Message.Text == "" {
return
}
buttonText := u.Message.Text
s.CurrentButtonText = &buttonText
s.WaitingForButtonText = false
s.WaitingForButtonURL = true
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
s.Buttons = append(s.Buttons, InlineButton{
Text: buttonText,
URL: "https://example.com",
})
s.UpdateCreativePreview(b)
s.ShowControlPanel(b, `✧ Введите URL для кнопки:
URL должен начинаться с https://
Например:
https://example.com
https://t.me/yourchannel`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
return
}
// Обработка добавления кнопки - URL
if s.WaitingForButtonURL {
if u.Message.Text == "" {
return
}
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, `❌ Неверный формат URL
URL должен начинаться с https://
Попробуйте ещё раз.`, [][]echotron.InlineKeyboardButton{
bot.Row(bot.Button("« Отмена", "cancel_add_button")),
})
return
}
if len(s.Buttons) > 0 {
s.Buttons[len(s.Buttons)-1].URL = url
}
s.WaitingForButtonURL = false
s.CurrentButtonText = nil
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
}
}
func (s *AddCreative) Handle(_ *bot.Bot, _ *echotron.Update) { return }
// extractCreativeFromMessage извлекает данные креатива из сообщения
func (s *AddCreative) extractCreativeFromMessage(message *echotron.Message) {
// Извлекаем текст с форматированием (GetFormattedText теперь поддерживает и text и caption)
if message.Text != "" || message.Caption != "" {
text := s.GetFormattedText(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
}
// Извлекаем кнопки из 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,
URL: button.URL,
})
}
}
}
}
}
// generateCreativeName генерирует название из текста или дефолтное
func (s *AddCreative) generateCreativeName() string {
// Если есть текст, берем первые слова
if s.Text != nil && *s.Text != "" {
// Удаляем HTML теги для генерации названия
cleanText := strings.ReplaceAll(*s.Text, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
cleanText = strings.ReplaceAll(cleanText, "", "")
// Берем первые 50 символов
runes := []rune(strings.TrimSpace(cleanText))
if len(runes) > 50 {
return string(runes[:50]) + "..."
}
if len(runes) > 0 {
return string(runes)
}
}
// Дефолтное название
return "Новый креатив"
}
// showCreativeConfirmation показывает превью креатива + панель с предложением добавить
func (s *AddCreative) showCreativeConfirmation(b *bot.Bot) {
// 1. Отправляем/обновляем превью креатива
if s.CreativeMessageID == nil {
s.SendCreativePreview(b)
} else {
s.UpdateCreativePreview(b)
}
// 2. Формируем текст панели управления
confirmText := fmt.Sprintf(`➕ Добавляем этот креатив?
Название креатива: %s
Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.`, *s.Name)
// 3. Показываем панель с кнопками редактирования + сохранения
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 s.MediaFileID != "" {
buttons = append(buttons, bot.Row(
bot.Button("⌫ Убрать медиа", "delete_media"),
))
}
// Кнопки удаления добавленных кнопок
if len(s.Buttons) > 0 {
var row []echotron.InlineKeyboardButton
for i, btn := range s.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)
}
}
// Финальные кнопки
if s.Text != nil {
buttons = append(buttons, bot.Row(
bot.Button("← Отмена", "cancel"),
bot.Button("✓ Сохранить", "confirm_create"),
))
} else {
buttons = append(buttons, bot.Row(
bot.Button("← Отмена", "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
jwt, err := s.ensureJWT(b)
if err != nil {
log.Error().Err(err).Msg("Failed to get JWT")
b.SendNew("❌ Ошибка авторизации\n\nПопробуйте /start login", bot.Keyboard())
return
}
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")
b.SendNew("❌ Не удалось загрузить медиа", bot.Keyboard(
bot.Row(bot.Button("← Назад", "cancel")),
))
return
}
}
buttons := make([]backend.CreativeButton, 0, len(s.Buttons))
for _, button := range s.Buttons {
buttons = append(buttons, backend.CreativeButton{
Text: button.Text,
URL: button.URL,
})
}
// Логируем что отправляем
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,
},
)
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 = `❌ Ошибка валидации
Текст должен содержать ОДНУ инвайт-ссылку вашего канала!
Формат ссылки:
https://t.me/+xxx
Пример правильного текста:
Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn`
} else if strings.Contains(errMsg, "Creative text must contain only one invite link") {
userMsg = `❌ Слишком много ссылок
Текст должен содержать ТОЛЬКО ОДНУ инвайт-ссылку.
У вас их несколько. Удалите лишние.`
} else {
userMsg = `❌ Ошибка создания
Не удалось создать креатив.
Попробуйте ещё раз или вернитесь назад.`
}
b.SendNew(userMsg, bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_from_preview")),
))
return
}
// Успех! Показываем сообщение и возвращаемся
successText := `✅ Креатив успешно создан!
📄 Название: ` + creative.Name
if len(s.Buttons) > 0 {
successText += fmt.Sprintf(`
🔘 Кнопок добавлено: %d`, len(s.Buttons))
}
b.SendNew(successText, bot.Keyboard())
// Возвращаемся на экран креативов
if s.BackState != nil {
b.SetState(s.BackState, bot.NewMessage)
}
}
func (s *AddCreative) ensureJWT(b *bot.Bot) (string, error) {
// Проверяем, есть ли JWT в сессии
if b.Session.JWT != "" {
return b.Session.JWT, nil
}
// Если нет, запрашиваем новый
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
if err != nil {
return "", err
}
// Сохраняем в сессию
b.Session.JWT = jwt
return jwt, nil
}