создание креатива постом
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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_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:
|
||||
"""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'<a class="tg-link">\1</a>', text)
|
||||
return _INVITE_LINK_PLAIN.sub('<a class="tg-link"></a>', text)
|
||||
|
||||
@@ -13,8 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
botToken = "8134938541:AAFcZhIGfYynQ9axyuA35R1nd7sPQZodrrQ"
|
||||
botUsername = "tgex_bot" // TODO: заменить на реальный username бота
|
||||
botToken = "7718883894:AAHUqtjqHtSs2Rp6Tb4PokqU6syfKmcFvOo"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -17,6 +17,9 @@ type AddCreative struct {
|
||||
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) {
|
||||
|
||||
<b>+ Добавить креатив</b>
|
||||
|
||||
Введите <b>название</b> креатива:
|
||||
📄 <b>Пришлите креатив который хотите добавить</b>
|
||||
|
||||
<i>Например:</i>
|
||||
‣ Летняя акция
|
||||
‣ Баннер 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, `<b>✎ Введите текст креатива:</b>
|
||||
|
||||
⚠ <b>Важно:</b> Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала
|
||||
@@ -99,7 +103,12 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
||||
s.MediaFileID = ""
|
||||
s.MediaChanged = true
|
||||
s.UpdateCreativePreview(b)
|
||||
|
||||
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</i>`, [][]echotron.InlineKeyboardButton{
|
||||
s.WaitingForButtonText = false
|
||||
s.WaitingForButtonURL = false
|
||||
s.CurrentButtonText = nil
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
|
||||
case data == "cancel_edit", data == "cancel_media":
|
||||
// Отменяем редактирование текста или добавление медиа
|
||||
s.WaitingForMedia = false
|
||||
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
|
||||
if s.CreativeMessageSent {
|
||||
s.showCreativeConfirmation(b)
|
||||
} else {
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
}
|
||||
|
||||
case data == "confirm_create":
|
||||
// Создаём креатив
|
||||
@@ -134,10 +161,15 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
||||
if index >= 0 && index < len(s.Buttons) {
|
||||
s.Buttons = append(s.Buttons[:index], s.Buttons[index+1:]...)
|
||||
s.UpdateCreativePreview(b)
|
||||
|
||||
if s.CreativeMessageSent {
|
||||
s.showCreativeConfirmation(b)
|
||||
} else {
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
s.Enter(b, bot.NewMessage)
|
||||
@@ -149,25 +181,36 @@ func (s *AddCreative) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
||||
return
|
||||
}
|
||||
|
||||
// Шаг 1: Получаем название
|
||||
// Шаг 1: Ждем пересылки креатива (любое сообщение)
|
||||
if s.WaitingForCreative {
|
||||
// Извлекаем данные из сообщения
|
||||
s.extractCreativeFromMessage(u.Message)
|
||||
|
||||
// Генерируем название из текста или дефолтное
|
||||
if s.Name == nil {
|
||||
if u.Message.Text == "" {
|
||||
return
|
||||
}
|
||||
name := u.Message.Text
|
||||
name := s.generateCreativeName()
|
||||
s.Name = &name
|
||||
}
|
||||
|
||||
// Отправляем креатив + панель управления
|
||||
s.sendCreativeAndPanel(b)
|
||||
// Удаляем сообщение пользователя
|
||||
_, 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)
|
||||
|
||||
if s.CreativeMessageSent {
|
||||
s.showCreativeConfirmation(b)
|
||||
} else {
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Если не медиа и не текст - игнорируем
|
||||
// Обработка добавления кнопки - текст кнопки
|
||||
if s.WaitingForButtonText {
|
||||
if u.Message.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
buttonText := u.Message.Text
|
||||
s.CurrentButtonText = &buttonText
|
||||
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")
|
||||
}
|
||||
|
||||
// Обработка добавления кнопки - текст кнопки
|
||||
if s.WaitingForButtonText {
|
||||
buttonText := u.Message.Text
|
||||
s.CurrentButtonText = &buttonText
|
||||
s.WaitingForButtonText = false
|
||||
s.WaitingForButtonURL = true
|
||||
|
||||
// Сразу создаём кнопку с placeholder URL (используем валидный URL)
|
||||
s.Buttons = append(s.Buttons, InlineButton{
|
||||
Text: buttonText,
|
||||
URL: "https://example.com",
|
||||
})
|
||||
|
||||
// Обновляем превью креатива, чтобы показать новую кнопку
|
||||
s.UpdateCreativePreview(b)
|
||||
|
||||
// Редактируем панель управления - просим URL
|
||||
s.ShowControlPanel(b, `<b>✧ Введите URL для кнопки:</b>
|
||||
|
||||
URL должен начинаться с <code>https://</code>
|
||||
@@ -241,9 +284,18 @@ URL должен начинаться с <code>https://</code>
|
||||
|
||||
// Обработка добавления кнопки - 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, `<b>❌ Неверный формат URL</b>
|
||||
|
||||
@@ -255,45 +307,212 @@ URL должен начинаться с <code>https://</code>
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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, "<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 отправляет превью креатива и панель управления
|
||||
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(),
|
||||
|
||||
@@ -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, `<b>❌ Неверный формат URL</b>
|
||||
@@ -287,6 +300,12 @@ URL должен начинаться с <code>http://</code> или <code>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 должен начинаться с <code>http://</code> или <code>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)
|
||||
}
|
||||
|
||||
@@ -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("<i>%s</i>\n\n", e.Navigation)
|
||||
}
|
||||
|
||||
if e.Text != nil && *e.Text != "" {
|
||||
// Проверяем что текст не является "визуально пустым" HTML
|
||||
// Например: "<a class=\"tg-link\"></a>" - пустая ссылка
|
||||
@@ -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,9 +458,19 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
|
||||
}
|
||||
}
|
||||
if i < len(runes) {
|
||||
// Экранируем HTML-спецсимволы
|
||||
switch runes[i] {
|
||||
case '<':
|
||||
result.WriteString("<")
|
||||
case '>':
|
||||
result.WriteString(">")
|
||||
case '&':
|
||||
result.WriteString("&")
|
||||
default:
|
||||
result.WriteRune(runes[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user