создание креатива постом
This commit is contained in:
@@ -20,12 +20,6 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
|
||||||
- DB__HOST=postgres
|
|
||||||
- DB__PORT=5432
|
|
||||||
- DB__USER=user
|
|
||||||
- DB__PASSWORD=password
|
|
||||||
- DB__DATABASE=tgex
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -47,20 +47,32 @@ MAX_CREATIVE_MEDIA_BYTES = 20 * 1024 * 1024
|
|||||||
|
|
||||||
_INVITE_LINK_HTML = re.compile(r'<a\s+href=["\']https?://t\.me/\+[a-zA-Z0-9_-]+["\']>([^<]*)</a>', re.IGNORECASE)
|
_INVITE_LINK_HTML = re.compile(r'<a\s+href=["\']https?://t\.me/\+[a-zA-Z0-9_-]+["\']>([^<]*)</a>', re.IGNORECASE)
|
||||||
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/\+[a-zA-Z0-9_-]+', re.IGNORECASE)
|
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/\+[a-zA-Z0-9_-]+', re.IGNORECASE)
|
||||||
|
_INVITE_LINK_REPLACED = re.compile(r'<a\s+class=["\']tg-link["\']>([^<]*)</a>', re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
def replace_invite_link_with_tag(text: str) -> str:
|
def replace_invite_link_with_tag(text: str) -> str:
|
||||||
"""Replace Telegram invite link (t.me/+xxx) with tracking tag."""
|
"""Replace Telegram invite link (t.me/+xxx) with tracking tag."""
|
||||||
|
|
||||||
|
# Проверяем, есть ли уже замененная ссылка
|
||||||
|
replaced_count = len(_INVITE_LINK_REPLACED.findall(text))
|
||||||
|
|
||||||
html_count = len(_INVITE_LINK_HTML.findall(text))
|
html_count = len(_INVITE_LINK_HTML.findall(text))
|
||||||
plain_count = len(_INVITE_LINK_PLAIN.findall(text))
|
|
||||||
total = html_count + plain_count
|
# Сначала удаляем HTML-ссылки из текста, чтобы не считать их дважды
|
||||||
|
text_without_html_links = _INVITE_LINK_HTML.sub('', text)
|
||||||
|
plain_count = len(_INVITE_LINK_PLAIN.findall(text_without_html_links))
|
||||||
|
|
||||||
|
total = replaced_count + html_count + plain_count
|
||||||
|
|
||||||
if total == 0:
|
if total == 0:
|
||||||
raise CreativeInviteLinkNotFound()
|
raise CreativeInviteLinkNotFound()
|
||||||
if total > 1:
|
if total > 1:
|
||||||
raise CreativeMultipleInviteLinks()
|
raise CreativeMultipleInviteLinks()
|
||||||
|
|
||||||
|
# Если ссылка уже заменена, возвращаем текст как есть
|
||||||
|
if replaced_count == 1:
|
||||||
|
return text
|
||||||
|
|
||||||
if html_count == 1:
|
if html_count == 1:
|
||||||
return _INVITE_LINK_HTML.sub(r'<a class="tg-link">\1</a>', text)
|
return _INVITE_LINK_HTML.sub(r'<a class="tg-link">\1</a>', text)
|
||||||
return _INVITE_LINK_PLAIN.sub('<a class="tg-link"></a>', text)
|
return _INVITE_LINK_PLAIN.sub('<a class="tg-link"></a>', text)
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
botToken = "8134938541:AAFcZhIGfYynQ9axyuA35R1nd7sPQZodrrQ"
|
botToken = "7718883894:AAHUqtjqHtSs2Rp6Tb4PokqU6syfKmcFvOo"
|
||||||
botUsername = "tgex_bot" // TODO: заменить на реальный username бота
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ import (
|
|||||||
type AddCreative struct {
|
type AddCreative struct {
|
||||||
CreativeEditorFields // Встраивание общих полей
|
CreativeEditorFields // Встраивание общих полей
|
||||||
|
|
||||||
ProjectID string
|
ProjectID string
|
||||||
WorkspaceID string
|
WorkspaceID string
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
|
WaitingForCreative bool // Ждем пересылки сообщения с креативом
|
||||||
|
CreativeMessageSent bool // Креатив уже получен и показан
|
||||||
|
WaitingForTextEdit bool // Ждем редактирования текста креатива
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
|
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
@@ -24,18 +27,18 @@ func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
|
|
||||||
<b>+ Добавить креатив</b>
|
<b>+ Добавить креатив</b>
|
||||||
|
|
||||||
Введите <b>название</b> креатива:
|
📄 <b>Пришлите креатив который хотите добавить</b>
|
||||||
|
|
||||||
<i>Например:</i>
|
• Обрабатываем любое сообщение Telegram
|
||||||
‣ Летняя акция
|
• Отправляйте сразу оформленный пост
|
||||||
‣ Баннер 1920x1080
|
• На следующем шаге можно добавить кнопки или изменить контент`
|
||||||
‣ Видео промо`
|
|
||||||
|
|
||||||
buttons := [][]echotron.InlineKeyboardButton{
|
buttons := [][]echotron.InlineKeyboardButton{
|
||||||
bot.Row(bot.Button("« Отмена", "cancel")),
|
bot.Row(bot.Button("« Отмена", "cancel")),
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboard := bot.Keyboard(buttons...)
|
keyboard := bot.Keyboard(buttons...)
|
||||||
|
s.WaitingForCreative = true
|
||||||
b.Render(text, keyboard, mode)
|
b.Render(text, keyboard, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +56,8 @@ func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case data == "edit_text":
|
case data == "edit_text":
|
||||||
// Редактируем панель управления - просим текст
|
// Начинаем редактирование текста
|
||||||
|
s.WaitingForTextEdit = true
|
||||||
s.ShowControlPanel(b, `<b>✎ Введите текст креатива:</b>
|
s.ShowControlPanel(b, `<b>✎ Введите текст креатива:</b>
|
||||||
|
|
||||||
⚠ <b>Важно:</b> Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала
|
⚠ <b>Важно:</b> Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала
|
||||||
@@ -99,7 +103,12 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
|||||||
s.MediaFileID = ""
|
s.MediaFileID = ""
|
||||||
s.MediaChanged = true
|
s.MediaChanged = true
|
||||||
s.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
|
||||||
|
if s.CreativeMessageSent {
|
||||||
|
s.showCreativeConfirmation(b)
|
||||||
|
} else {
|
||||||
|
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||||
|
}
|
||||||
|
|
||||||
case data == "cancel_add_button":
|
case data == "cancel_add_button":
|
||||||
// Отменяем добавление кнопки
|
// Отменяем добавление кнопки
|
||||||
@@ -114,12 +123,30 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
|||||||
s.WaitingForButtonText = false
|
s.WaitingForButtonText = false
|
||||||
s.WaitingForButtonURL = false
|
s.WaitingForButtonURL = false
|
||||||
s.CurrentButtonText = nil
|
s.CurrentButtonText = nil
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
|
||||||
|
|
||||||
case data == "cancel_edit", data == "cancel_media":
|
if s.CreativeMessageSent {
|
||||||
// Отменяем редактирование текста или добавление медиа
|
s.showCreativeConfirmation(b)
|
||||||
|
} else {
|
||||||
|
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
s.WaitingForMedia = false
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
if s.CreativeMessageSent {
|
||||||
|
s.showCreativeConfirmation(b)
|
||||||
|
} else {
|
||||||
|
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||||
|
}
|
||||||
|
|
||||||
case data == "confirm_create":
|
case data == "confirm_create":
|
||||||
// Создаём креатив
|
// Создаём креатив
|
||||||
@@ -134,7 +161,12 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
|||||||
if index >= 0 && index < len(s.Buttons) {
|
if index >= 0 && index < len(s.Buttons) {
|
||||||
s.Buttons = append(s.Buttons[:index], s.Buttons[index+1:]...)
|
s.Buttons = append(s.Buttons[:index], s.Buttons[index+1:]...)
|
||||||
s.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
|
||||||
|
if s.CreativeMessageSent {
|
||||||
|
s.showCreativeConfirmation(b)
|
||||||
|
} else {
|
||||||
|
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,25 +181,36 @@ func (s *AddCreative) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 1: Получаем название
|
// Шаг 1: Ждем пересылки креатива (любое сообщение)
|
||||||
if s.Name == nil {
|
if s.WaitingForCreative {
|
||||||
if u.Message.Text == "" {
|
// Извлекаем данные из сообщения
|
||||||
return
|
s.extractCreativeFromMessage(u.Message)
|
||||||
}
|
|
||||||
name := u.Message.Text
|
|
||||||
s.Name = &name
|
|
||||||
|
|
||||||
// Отправляем креатив + панель управления
|
// Генерируем название из текста или дефолтное
|
||||||
s.sendCreativeAndPanel(b)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка медиа
|
// Обработка медиа (когда явно просим добавить медиа)
|
||||||
if s.WaitingForMedia {
|
if s.WaitingForMedia {
|
||||||
handled := false
|
handled := false
|
||||||
|
|
||||||
if u.Message.Photo != nil && len(u.Message.Photo) > 0 {
|
if u.Message.Photo != nil && len(u.Message.Photo) > 0 {
|
||||||
// Берем самое большое фото
|
|
||||||
photo := u.Message.Photo[len(u.Message.Photo)-1]
|
photo := u.Message.Photo[len(u.Message.Photo)-1]
|
||||||
s.MediaType = "photo"
|
s.MediaType = "photo"
|
||||||
s.MediaFileID = photo.FileID
|
s.MediaFileID = photo.FileID
|
||||||
@@ -186,7 +229,6 @@ func (s *AddCreative) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if handled {
|
if handled {
|
||||||
// Удаляем сообщение пользователя
|
|
||||||
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
|
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to delete user message")
|
log.Error().Err(err).Msg("Failed to delete user message")
|
||||||
@@ -194,39 +236,40 @@ func (s *AddCreative) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
|
|
||||||
s.WaitingForMedia = false
|
s.WaitingForMedia = false
|
||||||
s.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
|
||||||
|
if s.CreativeMessageSent {
|
||||||
|
s.showCreativeConfirmation(b)
|
||||||
|
} else {
|
||||||
|
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если не медиа и не текст - игнорируем
|
|
||||||
if u.Message.Text == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Удаляем сообщение пользователя
|
|
||||||
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to delete user message")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработка добавления кнопки - текст кнопки
|
// Обработка добавления кнопки - текст кнопки
|
||||||
if s.WaitingForButtonText {
|
if s.WaitingForButtonText {
|
||||||
|
if u.Message.Text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
buttonText := u.Message.Text
|
buttonText := u.Message.Text
|
||||||
s.CurrentButtonText = &buttonText
|
s.CurrentButtonText = &buttonText
|
||||||
s.WaitingForButtonText = false
|
s.WaitingForButtonText = false
|
||||||
s.WaitingForButtonURL = true
|
s.WaitingForButtonURL = true
|
||||||
|
|
||||||
// Сразу создаём кнопку с placeholder URL (используем валидный URL)
|
// Удаляем сообщение пользователя
|
||||||
|
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to delete user message")
|
||||||
|
}
|
||||||
|
|
||||||
s.Buttons = append(s.Buttons, InlineButton{
|
s.Buttons = append(s.Buttons, InlineButton{
|
||||||
Text: buttonText,
|
Text: buttonText,
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
})
|
})
|
||||||
|
|
||||||
// Обновляем превью креатива, чтобы показать новую кнопку
|
|
||||||
s.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
|
|
||||||
// Редактируем панель управления - просим URL
|
|
||||||
s.ShowControlPanel(b, `<b>✧ Введите URL для кнопки:</b>
|
s.ShowControlPanel(b, `<b>✧ Введите URL для кнопки:</b>
|
||||||
|
|
||||||
URL должен начинаться с <code>https://</code>
|
URL должен начинаться с <code>https://</code>
|
||||||
@@ -241,9 +284,18 @@ URL должен начинаться с <code>https://</code>
|
|||||||
|
|
||||||
// Обработка добавления кнопки - URL
|
// Обработка добавления кнопки - URL
|
||||||
if s.WaitingForButtonURL {
|
if s.WaitingForButtonURL {
|
||||||
|
if u.Message.Text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
url := u.Message.Text
|
url := u.Message.Text
|
||||||
|
|
||||||
// Базовая валидация URL
|
// Удаляем сообщение пользователя
|
||||||
|
_, 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://") {
|
if !strings.HasPrefix(url, "https://") {
|
||||||
s.ShowControlPanel(b, `<b>❌ Неверный формат URL</b>
|
s.ShowControlPanel(b, `<b>❌ Неверный формат URL</b>
|
||||||
|
|
||||||
@@ -255,45 +307,212 @@ URL должен начинаться с <code>https://</code>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем URL последней кнопки (которую создали с placeholder)
|
|
||||||
if len(s.Buttons) > 0 {
|
if len(s.Buttons) > 0 {
|
||||||
s.Buttons[len(s.Buttons)-1].URL = url
|
s.Buttons[len(s.Buttons)-1].URL = url
|
||||||
}
|
}
|
||||||
s.WaitingForButtonURL = false
|
s.WaitingForButtonURL = false
|
||||||
s.CurrentButtonText = nil
|
s.CurrentButtonText = nil
|
||||||
|
|
||||||
// Обновляем креатив и возвращаем панель управления
|
|
||||||
s.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
|
||||||
|
if s.CreativeMessageSent {
|
||||||
|
s.showCreativeConfirmation(b)
|
||||||
|
} else {
|
||||||
|
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Шаг 2: Получаем текст
|
// Если ждем редактирования текста креатива
|
||||||
if s.Text == nil {
|
if s.WaitingForTextEdit && u.Message.Text != "" {
|
||||||
text := s.GetFormattedText(u.Message)
|
text := s.GetFormattedText(u.Message)
|
||||||
s.Text = &text
|
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)
|
s.UpdateCreativePreview(b)
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
|
||||||
|
if s.CreativeMessageSent {
|
||||||
|
s.showCreativeConfirmation(b)
|
||||||
|
} else {
|
||||||
|
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если текст уже задан - это повторное редактирование
|
// Если креатив уже показан и пользователь вводит текст - это изменение названия
|
||||||
text := s.GetFormattedText(u.Message)
|
if s.CreativeMessageSent && u.Message.Text != "" {
|
||||||
s.Text = &text
|
newName := u.Message.Text
|
||||||
|
|
||||||
// Обновляем креатив и возвращаем панель управления
|
// Ограничение 200 символов
|
||||||
s.UpdateCreativePreview(b)
|
runes := []rune(newName)
|
||||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
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 }
|
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 отправляет превью креатива и панель управления
|
// sendCreativeAndPanel отправляет превью креатива и панель управления
|
||||||
func (s *AddCreative) sendCreativeAndPanel(b *bot.Bot) {
|
func (s *AddCreative) sendCreativeAndPanel(b *bot.Bot) {
|
||||||
// Устанавливаем навигацию
|
|
||||||
s.Navigation = "… › Креативы › Добавление"
|
|
||||||
|
|
||||||
// 1. Отправляем превью креатива
|
// 1. Отправляем превью креатива
|
||||||
s.SendCreativePreview(b)
|
s.SendCreativePreview(b)
|
||||||
@@ -331,6 +550,15 @@ 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
|
// Создаём креатив через API
|
||||||
creative, err := b.Backend.CreateCreative(
|
creative, err := b.Backend.CreateCreative(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ func (s *CreativeDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
}
|
}
|
||||||
s.MediaChanged = false
|
s.MediaChanged = false
|
||||||
|
|
||||||
// Устанавливаем навигацию
|
|
||||||
s.Navigation = "… › Креативы › Детали"
|
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("creative_id", s.CreativeID).
|
Str("creative_id", s.CreativeID).
|
||||||
Str("name", creative.Name).
|
Str("name", creative.Name).
|
||||||
@@ -195,6 +192,7 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
|
|
||||||
// Обрабатываем добавление медиа
|
// Обрабатываем добавление медиа
|
||||||
if s.WaitingForMedia {
|
if s.WaitingForMedia {
|
||||||
|
handled := false
|
||||||
if u.Message.Photo != nil && len(u.Message.Photo) > 0 {
|
if u.Message.Photo != nil && len(u.Message.Photo) > 0 {
|
||||||
photos := u.Message.Photo
|
photos := u.Message.Photo
|
||||||
largestPhoto := photos[len(photos)-1]
|
largestPhoto := photos[len(photos)-1]
|
||||||
@@ -202,26 +200,29 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
s.MediaFileID = largestPhoto.FileID
|
s.MediaFileID = largestPhoto.FileID
|
||||||
s.MediaChanged = true
|
s.MediaChanged = true
|
||||||
s.WaitingForMedia = false
|
s.WaitingForMedia = false
|
||||||
|
handled = true
|
||||||
s.UpdateCreativePreview(b)
|
|
||||||
s.showManagementPanelWithDelete(b)
|
|
||||||
return
|
|
||||||
|
|
||||||
} else if u.Message.Video != nil {
|
} else if u.Message.Video != nil {
|
||||||
s.MediaType = "video"
|
s.MediaType = "video"
|
||||||
s.MediaFileID = u.Message.Video.FileID
|
s.MediaFileID = u.Message.Video.FileID
|
||||||
s.MediaChanged = true
|
s.MediaChanged = true
|
||||||
s.WaitingForMedia = false
|
s.WaitingForMedia = false
|
||||||
|
handled = true
|
||||||
s.UpdateCreativePreview(b)
|
|
||||||
s.showManagementPanelWithDelete(b)
|
|
||||||
return
|
|
||||||
|
|
||||||
} else if u.Message.Animation != nil {
|
} else if u.Message.Animation != nil {
|
||||||
s.MediaType = "animation"
|
s.MediaType = "animation"
|
||||||
s.MediaFileID = u.Message.Animation.FileID
|
s.MediaFileID = u.Message.Animation.FileID
|
||||||
s.MediaChanged = true
|
s.MediaChanged = true
|
||||||
s.WaitingForMedia = false
|
s.WaitingForMedia = false
|
||||||
|
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.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
s.showManagementPanelWithDelete(b)
|
s.showManagementPanelWithDelete(b)
|
||||||
@@ -236,6 +237,12 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
s.WaitingForButtonText = false
|
s.WaitingForButtonText = false
|
||||||
s.WaitingForButtonURL = true
|
s.WaitingForButtonURL = true
|
||||||
|
|
||||||
|
// Удаляем сообщение пользователя
|
||||||
|
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to delete user message")
|
||||||
|
}
|
||||||
|
|
||||||
// Создаём кнопку с placeholder URL
|
// Создаём кнопку с placeholder URL
|
||||||
s.Buttons = append(s.Buttons, InlineButton{
|
s.Buttons = append(s.Buttons, InlineButton{
|
||||||
Text: buttonText,
|
Text: buttonText,
|
||||||
@@ -257,6 +264,12 @@ func (s *CreativeDetails) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
if s.WaitingForButtonURL && u.Message.Text != "" {
|
if s.WaitingForButtonURL && u.Message.Text != "" {
|
||||||
url := strings.TrimSpace(u.Message.Text)
|
url := strings.TrimSpace(u.Message.Text)
|
||||||
|
|
||||||
|
// Удаляем сообщение пользователя
|
||||||
|
_, err := b.DeleteMessage(b.ChatID, u.Message.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to delete user message")
|
||||||
|
}
|
||||||
|
|
||||||
// Простая валидация URL
|
// Простая валидация URL
|
||||||
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
s.ShowControlPanel(b, `<b>❌ Неверный формат URL</b>
|
s.ShowControlPanel(b, `<b>❌ Неверный формат URL</b>
|
||||||
@@ -287,6 +300,12 @@ URL должен начинаться с <code>http://</code> или <code>https
|
|||||||
text := s.GetFormattedText(u.Message)
|
text := s.GetFormattedText(u.Message)
|
||||||
s.Text = &text
|
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.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
s.showManagementPanelWithDelete(b)
|
s.showManagementPanelWithDelete(b)
|
||||||
return
|
return
|
||||||
@@ -297,6 +316,12 @@ URL должен начинаться с <code>http://</code> или <code>https
|
|||||||
text := s.GetFormattedText(u.Message)
|
text := s.GetFormattedText(u.Message)
|
||||||
s.Text = &text
|
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.UpdateCreativePreview(b)
|
s.UpdateCreativePreview(b)
|
||||||
s.showManagementPanelWithDelete(b)
|
s.showManagementPanelWithDelete(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ type CreativeEditorFields struct {
|
|||||||
MediaFileID string // File ID из Telegram
|
MediaFileID string // File ID из Telegram
|
||||||
MediaChanged bool
|
MediaChanged bool
|
||||||
|
|
||||||
// Навигация (хлебные крошки)
|
|
||||||
Navigation string
|
|
||||||
|
|
||||||
// Состояние для добавления кнопки
|
// Состояние для добавления кнопки
|
||||||
WaitingForButtonText bool
|
WaitingForButtonText bool
|
||||||
WaitingForButtonURL bool
|
WaitingForButtonURL bool
|
||||||
@@ -50,11 +47,6 @@ type CreativeEditorFields struct {
|
|||||||
func (e *CreativeEditorFields) GetCreativeText() string {
|
func (e *CreativeEditorFields) GetCreativeText() string {
|
||||||
var result string
|
var result string
|
||||||
|
|
||||||
// Добавляем навигацию если она есть
|
|
||||||
if e.Navigation != "" {
|
|
||||||
result = fmt.Sprintf("<i>%s</i>\n\n", e.Navigation)
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Text != nil && *e.Text != "" {
|
if e.Text != nil && *e.Text != "" {
|
||||||
// Проверяем что текст не является "визуально пустым" HTML
|
// Проверяем что текст не является "визуально пустым" HTML
|
||||||
// Например: "<a class=\"tg-link\"></a>" - пустая ссылка
|
// Например: "<a class=\"tg-link\"></a>" - пустая ссылка
|
||||||
@@ -364,13 +356,25 @@ func (e *CreativeEditorFields) ShowControlPanel(b *bot.Bot, text string, buttons
|
|||||||
|
|
||||||
// GetFormattedText извлекает форматированный текст из сообщения с entities
|
// GetFormattedText извлекает форматированный текст из сообщения с entities
|
||||||
func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) string {
|
func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) string {
|
||||||
if message.Text == "" {
|
// Определяем откуда брать текст и entities
|
||||||
|
var text string
|
||||||
|
var entities []*echotron.MessageEntity
|
||||||
|
|
||||||
|
if message.Text != "" {
|
||||||
|
text = message.Text
|
||||||
|
entities = message.Entities
|
||||||
|
} else if message.Caption != "" {
|
||||||
|
text = message.Caption
|
||||||
|
entities = message.CaptionEntities
|
||||||
|
}
|
||||||
|
|
||||||
|
if text == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если нет entities - возвращаем текст как есть
|
// Если нет entities - возвращаем текст как есть
|
||||||
if len(message.Entities) == 0 {
|
if len(entities) == 0 {
|
||||||
return message.Text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
type tagSpan struct {
|
type tagSpan struct {
|
||||||
@@ -382,10 +386,10 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Преобразуем текст в руны для правильной работы с UTF-8
|
// Преобразуем текст в руны для правильной работы с UTF-8
|
||||||
runes := []rune(message.Text)
|
runes := []rune(text)
|
||||||
|
|
||||||
var spans []tagSpan
|
var spans []tagSpan
|
||||||
for _, entity := range message.Entities {
|
for _, entity := range entities {
|
||||||
// URL уже присутствует в сыром тексте, чтобы не дублировать - пропускаем entity.
|
// URL уже присутствует в сыром тексте, чтобы не дублировать - пропускаем entity.
|
||||||
if entity.Type == "url" {
|
if entity.Type == "url" {
|
||||||
continue
|
continue
|
||||||
@@ -438,6 +442,7 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Пишем текст один раз, вставляя HTML-теги по позициям
|
// Пишем текст один раз, вставляя HTML-теги по позициям
|
||||||
|
// и экранируя HTML-спецсимволы
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
for i := 0; i <= len(runes); i++ {
|
for i := 0; i <= len(runes); i++ {
|
||||||
if closing, ok := closes[i]; ok {
|
if closing, ok := closes[i]; ok {
|
||||||
@@ -453,7 +458,17 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i < len(runes) {
|
if i < len(runes) {
|
||||||
result.WriteRune(runes[i])
|
// Экранируем HTML-спецсимволы
|
||||||
|
switch runes[i] {
|
||||||
|
case '<':
|
||||||
|
result.WriteString("<")
|
||||||
|
case '>':
|
||||||
|
result.WriteString(">")
|
||||||
|
case '&':
|
||||||
|
result.WriteString("&")
|
||||||
|
default:
|
||||||
|
result.WriteRune(runes[i])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user