Files
tgex-backend/telegram_bot/screens/select_channels_for_purchase.go
2026-01-03 12:41:33 +03:00

348 lines
8.9 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package screens
import (
"context"
"fmt"
"regexp"
"strings"
"example.com/m/adapter/backend"
"example.com/m/bot"
"github.com/NicoNex/echotron/v3"
"github.com/rs/zerolog/log"
)
type PurchaseChannelInput struct {
Username string
PlannedCost *float64
Comment *string
}
type SelectChannelsForPurchase struct {
WorkspaceID string
ProjectID string
ProjectTitle string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
CurrentPage int
InvalidUsernames []string
BackState bot.State
}
var channelUsernameRe = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{4,31}$`)
func (s *SelectChannelsForPurchase) Enter(b *bot.Bot, mode bot.RenderMode) {
s.renderChannelSelection(b, mode)
}
func (s *SelectChannelsForPurchase) renderChannelSelection(b *bot.Bot, mode bot.RenderMode) {
text := "<i>… Создание Каналы</i>\n\n"
text += "<b>Создание закупа</b>\n\n"
text += "<b>Шаг 2/2:</b> Добавление каналов\n\n"
var buttons [][]echotron.InlineKeyboardButton
if len(s.Channels) == 0 {
text += `Добавьте каналы для размещения рекламы
Отправьте username канала (без @)
Например: <code>channel_name</code>
После добавления всех каналов нажмите <b>Далее</b>`
} else {
text += fmt.Sprintf("Добавлено каналов: <b>%d</b>\n\n", len(s.Channels))
for i, ch := range s.Channels {
channelText := fmt.Sprintf(" %d. @%s", i+1, ch.Username)
if ch.PlannedCost != nil {
channelText += fmt.Sprintf(" — %.0f₽", *ch.PlannedCost)
}
text += channelText + "\n"
}
text += "\n"
const channelsPerPage = 6
start := s.CurrentPage * channelsPerPage
if start >= len(s.Channels) {
s.CurrentPage = 0
start = 0
}
end := start + channelsPerPage
if end > len(s.Channels) {
end = len(s.Channels)
}
var slots []echotron.InlineKeyboardButton
for i := start; i < end; i++ {
ch := s.Channels[i]
buttonText := fmt.Sprintf("@%s", ch.Username)
if ch.PlannedCost != nil {
buttonText = fmt.Sprintf("@%s — %.0f₽", ch.Username, *ch.PlannedCost)
}
slots = append(slots, bot.Button(buttonText, fmt.Sprintf("remove_channel:%d", i)))
}
if len(s.Channels) > channelsPerPage {
for len(slots) < channelsPerPage {
slots = append(slots, bot.Button(" ", "empty"))
}
}
for i := 0; i < len(slots); i += 2 {
row := []echotron.InlineKeyboardButton{slots[i]}
if i+1 < len(slots) {
row = append(row, slots[i+1])
} else {
row = append(row, bot.Button(" ", "empty"))
}
buttons = append(buttons, row)
}
pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage
if pages > 1 {
var navRow []echotron.InlineKeyboardButton
if s.CurrentPage > 0 {
navRow = append(navRow, bot.Button("←", "prev_page"))
} else {
navRow = append(navRow, bot.Button("·", "empty"))
}
navRow = append(navRow, bot.Button(fmt.Sprintf("• %d/%d •", s.CurrentPage+1, pages), "page_info"))
if s.CurrentPage+1 < pages {
navRow = append(navRow, bot.Button("→", "next_page"))
} else {
navRow = append(navRow, bot.Button("·", "empty"))
}
buttons = append(buttons, navRow)
}
text += "\nДобавьте еще каналы или создайте закуп"
// Кнопка создания закупа (доступна только если есть каналы)
}
if len(s.InvalidUsernames) > 0 {
text += fmt.Sprintf("\n\n⚠ Пропущены: %s", strings.Join(s.InvalidUsernames, ", "))
s.InvalidUsernames = nil
}
// Кнопки навигации
if len(s.Channels) > 0 {
buttons = append(buttons, bot.Row(
bot.Button("← Назад", "back"),
bot.Button("→ Далее", "next"),
))
} else {
buttons = append(buttons, bot.Row(
bot.Button("← Назад", "back"),
))
}
keyboard := bot.Keyboard(buttons...)
b.Render(text, keyboard, mode)
}
func (s *SelectChannelsForPurchase) ensureJWT(b *bot.Bot) (string, error) {
if b.Session.JWT != "" {
return b.Session.JWT, nil
}
jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID)
if err != nil {
return "", err
}
b.Session.JWT = jwt
return jwt, nil
}
func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
if u.CallbackQuery == nil || u.CallbackQuery.Data == "" {
return
}
data := u.CallbackQuery.Data
switch data {
case "back":
// Возвращаемся к выбору креатива
b.SetState(&AddPurchase{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
ActivePicker: "",
BackState: s.BackState,
}, bot.EditMessage)
case "next":
if len(s.Channels) == 0 {
b.Edit("❌ Добавьте хотя бы один канал", bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_to_select")),
))
return
}
b.SetState(&PurchaseOptionalDetails{
WorkspaceID: s.WorkspaceID,
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
BackState: s.BackState,
}, bot.EditMessage)
case "back_to_select":
s.Enter(b, bot.EditMessage)
case "prev_page":
if s.CurrentPage > 0 {
s.CurrentPage--
}
s.Enter(b, bot.EditMessage)
case "next_page":
s.CurrentPage++
s.Enter(b, bot.EditMessage)
default:
if len(data) > 15 && data[:15] == "remove_channel:" {
var index int
if _, err := fmt.Sscanf(data[15:], "%d", &index); err == nil {
if index >= 0 && index < len(s.Channels) {
s.Channels = append(s.Channels[:index], s.Channels[index+1:]...)
if s.CurrentPage > 0 {
channelsPerPage := 6
pages := (len(s.Channels) + channelsPerPage - 1) / channelsPerPage
if s.CurrentPage >= pages {
s.CurrentPage = pages - 1
}
}
s.Enter(b, bot.EditMessage)
return
}
}
}
s.Enter(b, bot.NewMessage)
}
}
func (s *SelectChannelsForPurchase) createPurchase(b *bot.Bot, jwt string) {
// Преобразуем каналы в формат API
var apiChannels []backend.CreatePurchaseChannelInput
for _, ch := range s.Channels {
apiChannels = append(apiChannels, backend.CreatePurchaseChannelInput{
Username: ch.Username,
PlannedCost: ch.PlannedCost,
Comment: ch.Comment,
})
}
input := backend.CreatePurchaseInput{
CreativeID: s.CreativeID,
Channels: apiChannels,
}
purchase, err := b.Backend.CreatePurchase(
context.Background(),
jwt,
s.WorkspaceID,
s.ProjectID,
input,
)
if err != nil {
log.Error().Err(err).Msg("Failed to create purchase")
b.Edit("❌ Не удалось создать закуп\n\nВозможные причины:\n• Один из каналов не найден\n• Ошибка сервера", bot.Keyboard(
bot.Row(bot.Button("← Назад", "back_to_select")),
))
return
}
// Показываем успешное создание
successText := fmt.Sprintf(`<i>… Создание Готово</i>
<b>Закуп создан</b>
▪ <b>ID:</b> %s
▪ <b>Креатив:</b> %s
▪ <b>Каналов:</b> %d
Закуп создан и готов к работе`, purchase.ID, purchase.CreativeID, len(purchase.Channels))
b.Edit(successText, bot.Keyboard(
bot.Row(bot.Button("● Готово", "done")),
))
}
func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update) {
if u.Message == nil || u.Message.Text == "" {
return
}
raw := strings.TrimSpace(u.Message.Text)
if raw == "" {
return
}
tokens := strings.FieldsFunc(raw, func(r rune) bool {
return r == ' ' || r == '\n' || r == '\t' || r == ',' || r == ';'
})
added := 0
var invalid []string
for _, token := range tokens {
username := strings.TrimSpace(token)
username = strings.TrimPrefix(username, "@")
username = strings.Trim(username, "@")
username = strings.Trim(username, ",;")
if username == "" {
continue
}
if !channelUsernameRe.MatchString(username) {
invalid = append(invalid, username)
continue
}
exists := false
for _, ch := range s.Channels {
if ch.Username == username {
exists = true
break
}
}
if exists {
continue
}
s.Channels = append(s.Channels, PurchaseChannelInput{
Username: username,
})
added++
}
if added == 0 {
if len(invalid) == 0 {
return
}
}
s.InvalidUsernames = invalid
// Обновляем экран
s.Enter(b, bot.NewMessage)
}
func (s *SelectChannelsForPurchase) Handle(b *bot.Bot, u *echotron.Update) {
// Обрабатываем callback после успешного создания
if u.CallbackQuery != nil && u.CallbackQuery.Data == "done" {
// Возвращаемся к списку закупов
if s.BackState != nil {
b.SetState(s.BackState, bot.EditMessage)
}
}
}