490 lines
13 KiB
Go
490 lines
13 KiB
Go
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 = `
|
||
<b>+ Добавить креатив</b>
|
||
|
||
📄 <b>Пришлите креатив который хотите добавить</b>
|
||
|
||
• Обрабатываем любое сообщение Telegram
|
||
• Отправляйте сразу оформленный пост
|
||
• На следующем шаге можно добавить кнопки или изменить контент
|
||
|
||
`
|
||
const msgConfirmCreativeFormat = `
|
||
<b>➕ Добавляем этот креатив?</b>
|
||
|
||
<b>Название:</b> %s
|
||
|
||
<i>Чтобы изменить название, отправь мне сообщение 👇:</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 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)
|
||
}
|
||
}
|