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