экраны закупов и креативов
This commit is contained in:
@@ -12,38 +12,15 @@ import (
|
||||
)
|
||||
|
||||
type AddCreative struct {
|
||||
CreativeEditorFields // Встраивание общих полей
|
||||
|
||||
ProjectID string
|
||||
WorkspaceID string
|
||||
BackState bot.State
|
||||
|
||||
// Промежуточные данные для многошагового ввода
|
||||
Name *string
|
||||
Text *string
|
||||
Buttons []InlineButton
|
||||
|
||||
// Медиа
|
||||
MediaType string // "photo", "video", "animation"
|
||||
MediaFileID string // File ID из Telegram
|
||||
|
||||
// Состояние для добавления кнопки
|
||||
WaitingForButtonText bool
|
||||
WaitingForButtonURL bool
|
||||
CurrentButtonText *string
|
||||
WaitingForMedia bool
|
||||
|
||||
// ID сообщения с креативом (которое постоянно обновляется)
|
||||
CreativeMessageID *int
|
||||
// ID сообщения с панелью управления
|
||||
ControlPanelMessageID *int
|
||||
}
|
||||
|
||||
type InlineButton struct {
|
||||
Text string
|
||||
URL string
|
||||
}
|
||||
|
||||
func (s *AddCreative) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||
text := `<b>➕ Добавить креатив</b>
|
||||
text := `<b>+ Добавить креатив</b>
|
||||
|
||||
Введите <b>название</b> креатива:
|
||||
|
||||
@@ -75,9 +52,9 @@ func (s *AddCreative) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||
|
||||
case data == "edit_text":
|
||||
// Редактируем панель управления - просим текст
|
||||
s.showControlPanel(b, `<b>✏️ Введите текст креатива:</b>
|
||||
s.ShowControlPanel(b, `<b>✎ Введите текст креатива:</b>
|
||||
|
||||
⚠️ <b>Важно:</b> Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала
|
||||
⚠ <b>Важно:</b> Текст должен содержать <b>ОДНУ</b> инвайт-ссылку вашего канала
|
||||
|
||||
<b>Формат ссылки:</b>
|
||||
<code>https://t.me/+xxx</code>
|
||||
@@ -92,7 +69,7 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
||||
// Начинаем процесс добавления кнопки
|
||||
s.WaitingForButtonText = true
|
||||
|
||||
s.showControlPanel(b, `<b>➕ Добавить кнопку</b>
|
||||
s.ShowControlPanel(b, `<b>+ Добавить кнопку</b>
|
||||
|
||||
Введите <b>текст кнопки:</b>
|
||||
|
||||
@@ -106,7 +83,7 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
||||
// Начинаем процесс добавления медиа
|
||||
s.WaitingForMedia = true
|
||||
|
||||
s.showControlPanel(b, `<b>📷 Добавить медиа</b>
|
||||
s.ShowControlPanel(b, `<b>⧉ Добавить медиа</b>
|
||||
|
||||
Отправьте <b>фото, видео или GIF</b>
|
||||
|
||||
@@ -118,8 +95,8 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
||||
// Удаляем медиа
|
||||
s.MediaType = ""
|
||||
s.MediaFileID = ""
|
||||
s.updateCreativePreview(b)
|
||||
s.showManagementPanel(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
|
||||
case data == "cancel_add_button":
|
||||
// Отменяем добавление кнопки
|
||||
@@ -128,18 +105,18 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
||||
lastButton := s.Buttons[len(s.Buttons)-1]
|
||||
if lastButton.URL == "https://example.com" {
|
||||
s.Buttons = s.Buttons[:len(s.Buttons)-1]
|
||||
s.updateCreativePreview(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
}
|
||||
}
|
||||
s.WaitingForButtonText = false
|
||||
s.WaitingForButtonURL = false
|
||||
s.CurrentButtonText = nil
|
||||
s.showManagementPanel(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
|
||||
case data == "cancel_edit", data == "cancel_media":
|
||||
// Отменяем редактирование текста или добавление медиа
|
||||
s.WaitingForMedia = false
|
||||
s.showManagementPanel(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
|
||||
case data == "confirm_create":
|
||||
// Создаём креатив
|
||||
@@ -153,8 +130,8 @@ https://t.me/+AbCdEfGhIjKlMn</i>`, [][]echotron.InlineKeyboardButton{
|
||||
if _, err := fmt.Sscanf(parts[1], "%d", &index); err == nil {
|
||||
if index >= 0 && index < len(s.Buttons) {
|
||||
s.Buttons = append(s.Buttons[:index], s.Buttons[index+1:]...)
|
||||
s.updateCreativePreview(b)
|
||||
s.showManagementPanel(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,8 +187,8 @@ func (s *AddCreative) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
||||
}
|
||||
|
||||
s.WaitingForMedia = false
|
||||
s.updateCreativePreview(b)
|
||||
s.showManagementPanel(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -241,10 +218,10 @@ func (s *AddCreative) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
||||
})
|
||||
|
||||
// Обновляем превью креатива, чтобы показать новую кнопку
|
||||
s.updateCreativePreview(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
|
||||
// Редактируем панель управления - просим URL
|
||||
s.showControlPanel(b, `<b>💡 Введите URL для кнопки:</b>
|
||||
s.ShowControlPanel(b, `<b>✧ Введите URL для кнопки:</b>
|
||||
|
||||
URL должен начинаться с <code>https://</code>
|
||||
|
||||
@@ -262,7 +239,7 @@ URL должен начинаться с <code>https://</code>
|
||||
|
||||
// Базовая валидация URL
|
||||
if !strings.HasPrefix(url, "https://") {
|
||||
s.showControlPanel(b, `<b>❌ Неверный формат URL</b>
|
||||
s.ShowControlPanel(b, `<b>❌ Неверный формат URL</b>
|
||||
|
||||
URL должен начинаться с <code>https://</code>
|
||||
|
||||
@@ -280,317 +257,40 @@ URL должен начинаться с <code>https://</code>
|
||||
s.CurrentButtonText = nil
|
||||
|
||||
// Обновляем креатив и возвращаем панель управления
|
||||
s.updateCreativePreview(b)
|
||||
s.showManagementPanel(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
return
|
||||
}
|
||||
|
||||
// Шаг 2: Получаем текст
|
||||
if s.Text == nil {
|
||||
text := s.getFormattedText(u.Message)
|
||||
text := s.GetFormattedText(u.Message)
|
||||
s.Text = &text
|
||||
|
||||
// Обновляем креатив и возвращаем панель управления
|
||||
s.updateCreativePreview(b)
|
||||
s.showManagementPanel(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
return
|
||||
}
|
||||
|
||||
// Если текст уже задан - это повторное редактирование
|
||||
text := s.getFormattedText(u.Message)
|
||||
text := s.GetFormattedText(u.Message)
|
||||
s.Text = &text
|
||||
|
||||
// Обновляем креатив и возвращаем панель управления
|
||||
s.updateCreativePreview(b)
|
||||
s.showManagementPanel(b)
|
||||
s.UpdateCreativePreview(b)
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
}
|
||||
|
||||
func (s *AddCreative) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
// getFormattedText конвертирует текст сообщения с entities в HTML
|
||||
func (s *AddCreative) getFormattedText(message *echotron.Message) string {
|
||||
if message.Text == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Если нет entities - возвращаем текст как есть
|
||||
if len(message.Entities) == 0 {
|
||||
return message.Text
|
||||
}
|
||||
|
||||
// Преобразуем текст в руны для правильной работы с UTF-8
|
||||
runes := []rune(message.Text)
|
||||
result := ""
|
||||
lastPos := 0
|
||||
|
||||
// Обрабатываем entities
|
||||
for _, entity := range message.Entities {
|
||||
// Добавляем текст до entity
|
||||
if entity.Offset > lastPos {
|
||||
result += string(runes[lastPos:entity.Offset])
|
||||
}
|
||||
|
||||
// Получаем текст entity
|
||||
entityText := string(runes[entity.Offset : entity.Offset+entity.Length])
|
||||
|
||||
// Оборачиваем в соответствующие HTML-теги
|
||||
switch entity.Type {
|
||||
case "bold":
|
||||
result += "<b>" + entityText + "</b>"
|
||||
case "italic":
|
||||
result += "<i>" + entityText + "</i>"
|
||||
case "underline":
|
||||
result += "<u>" + entityText + "</u>"
|
||||
case "strikethrough":
|
||||
result += "<s>" + entityText + "</s>"
|
||||
case "code":
|
||||
result += "<code>" + entityText + "</code>"
|
||||
case "pre":
|
||||
result += "<pre>" + entityText + "</pre>"
|
||||
case "text_link":
|
||||
if entity.URL != "" {
|
||||
result += fmt.Sprintf("<a href=\"%s\">%s</a>", entity.URL, entityText)
|
||||
} else {
|
||||
result += entityText
|
||||
}
|
||||
case "url":
|
||||
result += entityText
|
||||
default:
|
||||
result += entityText
|
||||
}
|
||||
|
||||
lastPos = entity.Offset + entity.Length
|
||||
}
|
||||
|
||||
// Добавляем оставшийся текст
|
||||
if lastPos < len(runes) {
|
||||
result += string(runes[lastPos:])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// sendCreativeAndPanel отправляет превью креатива и панель управления
|
||||
func (s *AddCreative) sendCreativeAndPanel(b *bot.Bot) {
|
||||
// 1. Отправляем превью креатива
|
||||
s.sendCreativePreview(b)
|
||||
s.SendCreativePreview(b)
|
||||
|
||||
// 2. Отправляем панель управления
|
||||
s.showManagementPanel(b)
|
||||
}
|
||||
|
||||
// sendCreativePreview отправляет новое сообщение с превью креатива (с медиа или без)
|
||||
func (s *AddCreative) sendCreativePreview(b *bot.Bot) {
|
||||
creativeText := s.getCreativeText()
|
||||
creativeButtons := s.getCreativeButtons()
|
||||
|
||||
// Создаем клавиатуру с пустым массивом (не nil!)
|
||||
keyboard := echotron.InlineKeyboardMarkup{
|
||||
InlineKeyboard: creativeButtons,
|
||||
}
|
||||
if len(creativeButtons) == 0 {
|
||||
keyboard.InlineKeyboard = [][]echotron.InlineKeyboardButton{}
|
||||
}
|
||||
|
||||
var msgID int
|
||||
|
||||
// Если есть медиа, отправляем с медиа
|
||||
if s.MediaFileID != "" {
|
||||
var res echotron.APIResponseMessage
|
||||
var err error
|
||||
|
||||
switch s.MediaType {
|
||||
case "photo":
|
||||
res, err = b.SendPhoto(
|
||||
echotron.NewInputFileID(s.MediaFileID),
|
||||
b.ChatID,
|
||||
&echotron.PhotoOptions{
|
||||
Caption: creativeText,
|
||||
ParseMode: echotron.HTML,
|
||||
ReplyMarkup: keyboard,
|
||||
},
|
||||
)
|
||||
case "video":
|
||||
res, err = b.SendVideo(
|
||||
echotron.NewInputFileID(s.MediaFileID),
|
||||
b.ChatID,
|
||||
&echotron.VideoOptions{
|
||||
Caption: creativeText,
|
||||
ParseMode: echotron.HTML,
|
||||
ReplyMarkup: keyboard,
|
||||
},
|
||||
)
|
||||
case "animation":
|
||||
res, err = b.SendAnimation(
|
||||
echotron.NewInputFileID(s.MediaFileID),
|
||||
b.ChatID,
|
||||
&echotron.AnimationOptions{
|
||||
Caption: creativeText,
|
||||
ParseMode: echotron.HTML,
|
||||
ReplyMarkup: keyboard,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("media_type", s.MediaType).Msg("Failed to send media")
|
||||
// Fallback to text message
|
||||
b.SendNew(creativeText, keyboard)
|
||||
msgID = b.LastMessageID
|
||||
} else if res.Result != nil {
|
||||
msgID = res.Result.ID
|
||||
}
|
||||
} else {
|
||||
// Отправляем обычное текстовое сообщение
|
||||
b.SendNew(creativeText, keyboard)
|
||||
msgID = b.LastMessageID
|
||||
}
|
||||
|
||||
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
|
||||
s.CreativeMessageID = &msgID
|
||||
log.Info().Int("creative_msg_id", *s.CreativeMessageID).Bool("has_media", s.MediaFileID != "").Msg("Creative preview sent")
|
||||
}
|
||||
|
||||
// updateCreativePreview обновляет превью креатива
|
||||
func (s *AddCreative) updateCreativePreview(b *bot.Bot) {
|
||||
if s.CreativeMessageID == nil {
|
||||
log.Warn().Msg("updateCreativePreview: CreativeMessageID is nil")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("creative_msg_id", *s.CreativeMessageID).
|
||||
Bool("has_media", s.MediaFileID != "").
|
||||
Str("media_type", s.MediaType).
|
||||
Msg("Updating creative preview")
|
||||
|
||||
// Удаляем оба сообщения (креатив и панель)
|
||||
if s.CreativeMessageID != nil {
|
||||
_, err := b.DeleteMessage(b.ChatID, *s.CreativeMessageID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Int("msg_id", *s.CreativeMessageID).Msg("Failed to delete old creative preview")
|
||||
}
|
||||
}
|
||||
|
||||
if s.ControlPanelMessageID != nil {
|
||||
_, err := b.DeleteMessage(b.ChatID, *s.ControlPanelMessageID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Int("msg_id", *s.ControlPanelMessageID).Msg("Failed to delete old control panel")
|
||||
}
|
||||
s.ControlPanelMessageID = nil
|
||||
}
|
||||
|
||||
// Отправляем оба сообщения заново в правильном порядке
|
||||
s.sendCreativePreview(b)
|
||||
s.showManagementPanel(b)
|
||||
|
||||
log.Info().Int("new_creative_msg_id", *s.CreativeMessageID).Msg("Creative preview updated")
|
||||
}
|
||||
|
||||
// getCreativeText возвращает текст для превью креатива
|
||||
func (s *AddCreative) getCreativeText() string {
|
||||
if s.Text != nil {
|
||||
return *s.Text
|
||||
}
|
||||
return `<i> Я твой креатив
|
||||
|
||||
У меня пока нет текста, добавь его ниже.
|
||||
|
||||
</i>`
|
||||
}
|
||||
|
||||
// getCreativeButtons возвращает кнопки для превью креатива
|
||||
func (s *AddCreative) getCreativeButtons() [][]echotron.InlineKeyboardButton {
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
for _, btn := range s.Buttons {
|
||||
buttons = append(buttons, []echotron.InlineKeyboardButton{
|
||||
{
|
||||
Text: btn.Text,
|
||||
URL: btn.URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return buttons
|
||||
}
|
||||
|
||||
// showManagementPanel показывает панель управления креативом
|
||||
func (s *AddCreative) showManagementPanel(b *bot.Bot) {
|
||||
text := `<b>☝️ Выше отображается <b>превью вашего креатива</b> — так он будет выглядеть при отправке</b>
|
||||
|
||||
Используйте кнопки ниже для редактирования:`
|
||||
|
||||
var buttons [][]echotron.InlineKeyboardButton
|
||||
|
||||
// Кнопки редактирования
|
||||
row1 := []echotron.InlineKeyboardButton{}
|
||||
if s.Text != nil {
|
||||
row1 = append(row1, bot.Button("✏️ Текст", "edit_text"))
|
||||
} else {
|
||||
row1 = append(row1, bot.Button("➕ Текст", "edit_text"))
|
||||
}
|
||||
|
||||
if s.MediaFileID != "" {
|
||||
row1 = append(row1, bot.Button("✏️ Медиа", "add_media"))
|
||||
} else {
|
||||
row1 = append(row1, bot.Button("➕ Медиа", "add_media"))
|
||||
}
|
||||
|
||||
row1 = append(row1, bot.Button("➕ Кнопка", "add_button"))
|
||||
buttons = append(buttons, row1)
|
||||
|
||||
// Кнопка удаления медиа (если медиа добавлено)
|
||||
if s.MediaFileID != "" {
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("🗑 Удалить медиа", "delete_media"),
|
||||
))
|
||||
}
|
||||
|
||||
// Кнопки удаления (если есть кнопки)
|
||||
for i, btn := range s.Buttons {
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button(fmt.Sprintf("🗑 %s", btn.Text), fmt.Sprintf("delete_button:%d", i)),
|
||||
))
|
||||
}
|
||||
|
||||
// Финальные кнопки
|
||||
if s.Text != nil {
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("✅ Подтвердить", "confirm_create"),
|
||||
bot.Button("« Отмена", "cancel"),
|
||||
))
|
||||
} else {
|
||||
buttons = append(buttons, bot.Row(
|
||||
bot.Button("« Отмена", "cancel"),
|
||||
))
|
||||
}
|
||||
|
||||
s.showControlPanel(b, text, buttons)
|
||||
}
|
||||
|
||||
// showControlPanel редактирует панель управления с произвольным текстом и кнопками
|
||||
func (s *AddCreative) showControlPanel(b *bot.Bot, text string, buttons [][]echotron.InlineKeyboardButton) {
|
||||
if s.ControlPanelMessageID == nil {
|
||||
// Отправляем новую панель
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.SendNew(text, keyboard)
|
||||
// ВАЖНО: Сохраняем копию значения, а не указатель на переменную!
|
||||
controlPanelMsgID := b.LastMessageID
|
||||
s.ControlPanelMessageID = &controlPanelMsgID
|
||||
log.Info().Int("control_panel_msg_id", *s.ControlPanelMessageID).Msg("Control panel sent")
|
||||
} else {
|
||||
// Редактируем существующую панель
|
||||
log.Info().Int("control_panel_msg_id", *s.ControlPanelMessageID).Msg("Editing control panel")
|
||||
oldLastMessageID := b.LastMessageID
|
||||
b.LastMessageID = *s.ControlPanelMessageID
|
||||
|
||||
keyboard := bot.Keyboard(buttons...)
|
||||
b.Edit(text, keyboard)
|
||||
|
||||
b.LastMessageID = oldLastMessageID
|
||||
log.Info().Msg("Control panel updated")
|
||||
}
|
||||
s.ShowManagementPanel(b, "✓ Сохранить", "confirm_create", "cancel")
|
||||
}
|
||||
|
||||
func (s *AddCreative) createCreative(b *bot.Bot) {
|
||||
|
||||
Reference in New Issue
Block a user