package screens
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/NicoNex/echotron/v3"
"github.com/TelegramExchange/tgex-backend/tg_bot/backend"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
"github.com/rs/zerolog/log"
)
type AddCreativeCtx struct {
CreativeEditorFields
ProjectID string
WorkspaceID string
BackState bot.State
}
type AddCreativeStart struct{ ctx *AddCreativeCtx }
type AddCreativeEdit struct{ ctx *AddCreativeCtx }
type AddCreativeInput struct{ ctx *AddCreativeCtx }
const msgWaitCreative = `
+ Добавить креатив
📄 Пришлите креатив который хотите добавить
• Обрабатываем любое сообщение Telegram
• Отправляйте сразу оформленный пост
• На следующем шаге можно добавить кнопки или изменить контент
`
const msgConfirmCreativeFormat = `
➕ Добавляем этот креатив?
Название креатива: %s
Если нужно изменить название креатива, просто введите его здесь. Название автоматически сохранится. Лимит 200 символов.
`
const msgDownloadMediaError = `
❌ Не удалось загрузить медиа
Попробуйте удалить медиа и добавить заново.
`
const msgInviteLinkRequired = `
❌ Ошибка валидации
Текст должен содержать ОДНУ инвайт-ссылку вашего канала!
Формат ссылки:
https://t.me/+xxx
Пример правильного текста:
Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn
Нажмите "✎ Текст" ниже чтобы исправить.
`
const msgInviteLinkTooMany = `
❌ Слишком много ссылок
Текст должен содержать ТОЛЬКО ОДНУ инвайт-ссылку.
У вас их несколько. Удалите лишние.
Нажмите "✎ Текст" ниже чтобы исправить.
`
const msgCreateError = `
❌ Ошибка создания
Не удалось создать креатив.
Попробуйте ещё раз или вернитесь назад.
`
const msgCreativeCreated = `
✅ Креатив успешно создан!
📄 Название: %s
`
const msgCreativeCreatedButtons = `
🔘 Кнопок добавлено: %d
`
func (s *AddCreativeStart) Enter(b *bot.Bot, mode bot.RenderMode) {
kb := Keyboard(Row(Button("« Отмена", "cancel")))
b.Render(msgWaitCreative, kb, mode)
}
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
}
data := u.CallbackQuery.Data
switch {
case data == "cancel":
if s.ctx.BackState != nil {
b.SetState(s.ctx.BackState, bot.EditMessage)
}
case data == "edit_text":
s.ctx.InputMode = inputModeText
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
case data == "add_button":
s.ctx.InputMode = inputModeButtonText
s.ctx.PendingButtonType = "invite"
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
case data == "add_media":
s.ctx.InputMode = inputModeMedia
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
case data == "delete_media":
s.ctx.MediaType = ""
s.ctx.MediaFileID = ""
s.ctx.MediaChanged = true
s.ctx.UpdateCreativePreview(b)
s.Enter(b, bot.EditMessage)
case data == "confirm_create":
s.ctx.createCreative(b)
case strings.HasPrefix(data, "delete_button:"):
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 *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
}
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.ctx.DeleteUserMessage(b, u.Message.ID)
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
}
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)
if !s.ctx.IsValidButtonURL(url, true) {
s.ctx.ShowInvalidButtonURLPanel(b)
return
}
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.ctx.DeleteUserMessage(b, u.Message.ID)
s.ctx.UpdateCreativePreview(b)
s.ctx.ClearInputMode()
b.SetState(&AddCreativeEdit{ctx: s.ctx}, bot.EditMessage)
}
}
func (s *AddCreativeInput) Handle(_ *bot.Bot, _ *echotron.Update) {}
func (s *AddCreativeInput) Exit() {}
func (s *AddCreativeCtx) extractCreativeFromMessage(message *echotron.Message) {
if message.Text != "" || message.Caption != "" {
text := ui.FormatMessageHTML(message)
if text != "" {
s.Text = &text
}
}
s.SetMediaFromMessage(message)
if message.ReplyMarkup != nil && len(message.ReplyMarkup.InlineKeyboard) > 0 {
for _, row := range message.ReplyMarkup.InlineKeyboard {
for _, button := range row {
if button.URL != "" {
s.Buttons = append(s.Buttons, InlineButton{
Text: button.Text,
URL: button.URL,
})
}
}
}
}
}
func (s *AddCreativeCtx) generateCreativeName() string {
if s.Text != nil && *s.Text != "" {
re := regexp.MustCompile(`<[^>]*>`)
cleanText := re.ReplaceAllString(*s.Text, "")
runes := []rune(strings.TrimSpace(cleanText))
if len(runes) > 10 {
return string(runes[:10]) + "..."
}
if len(runes) > 0 {
return string(runes)
}
}
return "Новый креатив"
}
func (s *AddCreativeCtx) showCreativeConfirmation(b *bot.Bot) {
if s.CreativeMessageID == nil {
s.SendCreativePreview(b)
} else {
s.UpdateCreativePreview(b)
}
escapedName := ui.EscapeHTML(*s.Name)
confirmText := fmt.Sprintf(msgConfirmCreativeFormat, escapedName)
buttons := s.BuildEditorButtons("✓ Сохранить", "confirm_create", "Отмена", "cancel")
s.ShowControlPanel(b, confirmText, buttons)
}
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("✓ Попробовать снова", "confirm_create", "Отмена", "cancel")
s.ShowControlPanel(b, msgDownloadMediaError, buttons)
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("text", *s.Text).Msg("Creative text")
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 = msgInviteLinkRequired
} else if strings.Contains(errMsg, "Creative text must contain only one invite link") {
userMsg = msgInviteLinkTooMany
} else {
userMsg = msgCreateError
}
buttons := s.BuildEditorButtons("✓ Попробовать снова", "confirm_create", "Отмена", "cancel")
s.ShowControlPanel(b, userMsg, buttons)
return
}
successText := fmt.Sprintf(msgCreativeCreated, creative.Name)
if len(s.Buttons) > 0 {
successText += fmt.Sprintf(msgCreativeCreatedButtons, len(s.Buttons))
}
b.SendNew(successText, Keyboard())
if s.BackState != nil {
b.SetState(s.BackState, bot.NewMessage)
}
}