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
BackState bot.State
WaitMessageSent bool
}
type AddCreativeStart struct{ Ctx *AddCreativeCtx }
type AddCreativeEdit struct{ ctx *AddCreativeCtx }
type AddCreativeInput struct{ ctx *AddCreativeCtx }
const msgWaitCreative = `
+ Добавить креатив
📄 Пришлите креатив который хотите добавить
• Обрабатываем любое сообщение Telegram
• Отправляйте сразу оформленный пост
• На следующем шаге можно добавить кнопки или изменить контент
`
const msgConfirmCreativeFormat = `
➕ Добавляем этот креатив?
Название: %s
Чтобы изменить название, отправь мне сообщение 👇:
`
const msgDownloadMediaError = `
❌ Не удалось загрузить медиа
Попробуйте удалить медиа и добавить заново.
`
const msgInviteLinkRequired = `
❌ Ошибка валидации
Текст должен содержать хотя бы одну инвайт-ссылку вашего канала!
Формат ссылки:
https://t.me/+xxx
Пример правильного текста:
Присоединяйтесь к нашему каналу!
https://t.me/+AbCdEfGhIjKlMn
Нажмите "✎ Текст" ниже чтобы исправить.
`
const msgInviteLinkTooMany = `
❌ Слишком много ссылок
Текст должен содержать ТОЛЬКО ОДНУ инвайт-ссылку.
У вас их несколько. Удалите лишние.
Нажмите "✎ Текст" ниже чтобы исправить.
`
const msgCreateError = `
❌ Ошибка создания
Не удалось создать креатив.
Попробуйте ещё раз или вернитесь назад.
`
const msgCreativeCreated = `
✅ Креатив успешно создан!
📄 Название: %s
`
const msgCreativeCreatedButtons = `
🔘 Кнопок добавлено: %d
`
func filterCreateButtons(rows [][]echotron.InlineKeyboardButton) [][]echotron.InlineKeyboardButton {
filtered := make([][]echotron.InlineKeyboardButton, 0, len(rows))
for _, row := range rows {
skip := false
for _, btn := range row {
switch btn.CallbackData {
case "edit_text", "add_media", "delete_media":
skip = true
}
}
if !skip {
filtered = append(filtered, row)
}
}
return filtered
}
func (s *AddCreativeStart) Enter(b *bot.Bot, mode bot.RenderMode) {
if s.Ctx.WaitMessageSent {
return
}
kb := Keyboard(Row(Button("« Отмена", "cancel")))
b.Render(msgWaitCreative, kb, mode)
s.Ctx.WaitMessageSent = true
}
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
}
if u.Message.MediaGroupID != "" {
groupID := u.Message.MediaGroupID
s.Ctx.scheduleMediaGroupAction(groupID, func() {
b.SetState(&AddCreativeEdit{ctx: s.Ctx}, bot.NewMessage)
})
b.MarkHandled()
return
}
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.MediaItems = nil
s.ctx.MediaChanged = true
s.ctx.UpdateCreativePreview(b)
s.Enter(b, bot.EditMessage)
case data == "edit_tag":
s.ctx.ShowTagPanel(b)
case data == "tag_testing":
tag := "testing"
s.ctx.Tag = &tag
s.Enter(b, bot.EditMessage)
case data == "tag_production":
tag := "production"
s.ctx.Tag = &tag
s.Enter(b, bot.EditMessage)
case data == "cancel_edit":
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 {
return
}
if u.Message.Text == "" {
if s.ctx.SetMediaFromMessage(u.Message) {
if u.Message.MediaGroupID != "" {
groupID := u.Message.MediaGroupID
s.ctx.scheduleMediaGroupAction(groupID, func() {
s.ctx.UpdateCreativePreview(b)
s.Enter(b, bot.EditMessage)
})
b.MarkHandled()
return
}
s.ctx.UpdateCreativePreview(b)
s.Enter(b, bot.EditMessage)
}
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
}
if u.Message.MediaGroupID != "" {
groupID := u.Message.MediaGroupID
s.ctx.scheduleMediaGroupAction(groupID, func() {
s.ctx.UpdateCreativePreview(b)
s.ctx.ShowMediaPanel(b)
b.SetState(&AddCreativeInput{ctx: s.ctx}, bot.EditMessage)
})
b.MarkHandled()
return
}
s.ctx.UpdateCreativePreview(b)
s.ctx.ShowMediaPanel(b)
b.SetState(&AddCreativeInput{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 := filterCreateButtons(s.BuildEditorButtons("✓ Сохранить", "confirm_create", "Отмена", "cancel"))
s.ShowControlPanel(b, confirmText, buttons)
}
func (s *AddCreativeCtx) createCreative(b *bot.Bot) {
var err error
var mediaItems []backend.CreativeMediaInput
if len(s.MediaItems) > 0 {
mediaItems, err = s.BuildMediaInputs(b)
if err != nil {
log.Error().Err(err).Msg("Failed to download creative media")
buttons := filterCreateButtons(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")
tag := "testing"
if s.Tag != nil {
tag = *s.Tag
}
input := backend.CreateCreativeInput{
Name: *s.Name,
Text: *s.Text,
Buttons: buttons,
Tag: &tag,
}
if len(mediaItems) > 0 {
input.MediaItems = mediaItems
}
creative, err := b.Backend.CreateCreative(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, input)
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 := filterCreateButtons(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)
}
}