refactor
This commit is contained in:
@@ -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, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user