Files
tgex-backend/telegram_bot/screens/add_creative.go
2026-01-07 12:23:02 +03:00

689 lines
21 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 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 := `<i>… Креативы Добавление</i>
<b> Добавить креатив</b>
📄 <b>Пришлите креатив который хотите добавить</b>
• Обрабатываем любое сообщение 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, `<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)
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{
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":
// Отменяем редактирование текста
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
// Удаляем сообщение пользователя
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
if err != nil {
log.Error().Err(err).Msg("Failed to delete user message")
}
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{
bot.Row(bot.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 {
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, `<b>❌ Неверный формат URL</b>
URL должен начинаться с <code>https://</code>
Попробуйте ещё раз.`, [][]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.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
}
}
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, "<b>", "")
cleanText = strings.ReplaceAll(cleanText, "</b>", "")
cleanText = strings.ReplaceAll(cleanText, "<i>", "")
cleanText = strings.ReplaceAll(cleanText, "</i>", "")
cleanText = strings.ReplaceAll(cleanText, "<u>", "")
cleanText = strings.ReplaceAll(cleanText, "</u>", "")
cleanText = strings.ReplaceAll(cleanText, "<s>", "")
cleanText = strings.ReplaceAll(cleanText, "</s>", "")
cleanText = strings.ReplaceAll(cleanText, "<code>", "")
cleanText = strings.ReplaceAll(cleanText, "</code>", "")
// Берем первые 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(`<b> Добавляем этот креатив?</b>
<b>Название креатива:</b> %s
<i>Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.</i>`, *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 уже создан в Bot.Update(), просто берем из сессии
jwt := b.Session.JWT
if jwt == "" {
log.Error().Msg("JWT is empty in session")
b.SendNew("<b>❌ Ошибка авторизации</b>\n\nПопробуйте /start login", bot.Keyboard())
return
}
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")
b.SendNew("<b>❌ Не удалось загрузить медиа</b>", 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 = `<b>❌ Ошибка валидации</b>
Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала!
<b>Формат ссылки:</b>
<code>https://t.me/+xxx</code>
<b>Пример правильного текста:</b>
<i>Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn</i>`
} else if strings.Contains(errMsg, "Creative text must contain only one invite link") {
userMsg = `<b>❌ Слишком много ссылок</b>
Текст должен содержать <b>ТОЛЬКО ОДНУ</b> инвайт-ссылку.
У вас их несколько. Удалите лишние.`
} else {
userMsg = `<b>❌ Ошибка создания</b>
Не удалось создать креатив.
Попробуйте ещё раз или вернитесь назад.`
}
b.SendNew(userMsg, bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_from_preview")),
))
return
}
// Успех! Показываем сообщение и возвращаемся
successText := `<b>✅ Креатив успешно создан!</b>
📄 <b>Название:</b> ` + creative.Name
if len(s.Buttons) > 0 {
successText += fmt.Sprintf(`
🔘 <b>Кнопок добавлено:</b> %d`, len(s.Buttons))
}
b.SendNew(successText, bot.Keyboard())
// Возвращаемся на экран креативов
if s.BackState != nil {
b.SetState(s.BackState, bot.NewMessage)
}
}