From b212b6c22f12c2cbc02ee1a1819bd4a72976a90d Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Sun, 1 Feb 2026 11:10:22 +0300 Subject: [PATCH] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/telegram/telegram.go | 14 ++ tg_bot/screens/add_purchase.go | 15 ++ .../screens/{purchases.go => placements.go} | 183 +++++++++++------- tg_bot/screens/project_details.go | 105 ++++------ 4 files changed, 181 insertions(+), 136 deletions(-) rename tg_bot/screens/{purchases.go => placements.go} (60%) diff --git a/pkg/telegram/telegram.go b/pkg/telegram/telegram.go index bd56e42..c2702a5 100644 --- a/pkg/telegram/telegram.go +++ b/pkg/telegram/telegram.go @@ -52,6 +52,20 @@ func New(cfg Config) (*Client, error) { return fmt.Errorf("session not authorized") } + // Получаем информацию о текущем пользователе + self, err := tg.Self(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to get user info") + } else { + log.Info(). + Int64("id", self.ID). + Str("username", "@"+self.Username). + Str("first_name", self.FirstName). + Str("last_name", self.LastName). + Str("phone", self.Phone). + Msg("Authorized as user") + } + close(ready) <-ctx.Done() diff --git a/tg_bot/screens/add_purchase.go b/tg_bot/screens/add_purchase.go index b362907..f361ed1 100644 --- a/tg_bot/screens/add_purchase.go +++ b/tg_bot/screens/add_purchase.go @@ -146,6 +146,10 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) { text += `Для создания закупа сначала нужно создать креатив. Вернитесь назад и создайте креатив в разделе Креативы` + + buttons = append(buttons, Row( + Button("🎨 Перейти к креативам", "go_to_creatives"), + )) } else { text += fmt.Sprintf("Креативы%s\n\n", ui.FormatPageInfo(s.CreativePage, page.Pages)) @@ -219,6 +223,17 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) { s.ActivePicker = "" s.Enter(b, bot.EditMessage) + case "go_to_creatives": + b.SetState(&Creatives{ + CurrentPage: 0, + ProjectID: s.ProjectID, + ProjectTitle: s.ProjectTitle, + ProjectTelegramID: s.ProjectTelegramID, + ProjectUsername: s.ProjectUsername, + ProjectStatus: s.ProjectStatus, + BackState: s, + }, bot.EditMessage) + case "prev": if s.ActivePicker == "project_list" { if s.ProjectPage > 0 { diff --git a/tg_bot/screens/purchases.go b/tg_bot/screens/placements.go similarity index 60% rename from tg_bot/screens/purchases.go rename to tg_bot/screens/placements.go index a8cf72d..fd8649d 100644 --- a/tg_bot/screens/purchases.go +++ b/tg_bot/screens/placements.go @@ -6,7 +6,6 @@ import ( "github.com/NicoNex/echotron/v3" "github.com/TelegramExchange/tgex-backend/tg_bot/bot" - "github.com/rs/zerolog/log" ) type Placements struct { @@ -18,72 +17,24 @@ type Placements struct { BackState bot.State } +const msgPlacements = ` +Размещения + +Управление размещениями для проекта. +` + func (s *Placements) Enter(b *bot.Bot, mode bot.RenderMode) { - s.renderPurchases(b, mode) -} + var rows [][]echotron.InlineKeyboardButton -func (s *Placements) renderPurchases(b *bot.Bot, mode bot.RenderMode) { - resp, err := b.Backend.GetPlacements(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID) - if err != nil { - log.Error().Err(err).Msg("Failed to get placements") - b.SendNew("❌ Не удалось загрузить размещения", Keyboard()) - return - } + rows = append(rows, Row(Button("+ Создать размещение", "add_purchase"))) + rows = append(rows, Row(Button("Размещения", "placements_list"))) + rows = append(rows, Row(URLButton("План закупов", "https://example.com"))) + rows = append(rows, Row(Button("← Назад", "back"))) - text := "Размещения\n\n" + keyboard := Keyboard(rows...) + b.Render(msgPlacements, keyboard, mode) - var buttons [][]echotron.InlineKeyboardButton - - if len(resp.Placements) == 0 { - text += `Список размещений пуст. - -Размещение — это план публикации рекламы в канале: - ‣ Выбор креатива - ‣ Канал размещения - ‣ Стоимость и формат - ‣ Статус выполнения - -Создайте первое размещение` - } else { - text += "Управление размещениями для проекта.\n\n" - text += "Выберите размещение для деталей:\n\n" - - for i, placement := range resp.Placements { - statusSymbol := "●" - if placement.Status == "archived" { - statusSymbol = "■" - } - - channelName := "Без названия" - if placement.Channel.Title != nil && *placement.Channel.Title != "" { - channelName = *placement.Channel.Title - } else if placement.Channel.Username != nil && *placement.Channel.Username != "" { - channelName = "@" + *placement.Channel.Username - } - - buttonText := fmt.Sprintf("%s #%d %s", statusSymbol, i+1, channelName) - - buttons = append(buttons, Row( - Button(buttonText, fmt.Sprintf("placement:%s", placement.ID)), - )) - } - } - - // Кнопка создания нового размещения - buttons = append(buttons, Row( - Button("+ Создать размещение", "add_purchase"), - )) - - // Кнопка назад - buttons = append(buttons, Row( - Button("← Назад", "back"), - )) - - keyboard := Keyboard(buttons...) - b.Render(text, keyboard, mode) - - messageID := b.LastMessageID - updateProjectHeaderMedia(b, messageID, text, keyboard, s.ProjectTelegramID, s.ProjectTitle, s.ProjectUsername, s.ProjectStatus) + updateProjectHeaderMedia(b, b.LastMessageID, msgPlacements, keyboard, s.ProjectTelegramID, s.ProjectTitle, s.ProjectUsername, s.ProjectStatus) } func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) { @@ -110,6 +61,106 @@ func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) { BackState: s, }, bot.EditMessage) + case "placements_list": + b.SetState(&PlacementsList{ + ProjectID: s.ProjectID, + ProjectTitle: s.ProjectTitle, + ProjectTelegramID: s.ProjectTelegramID, + ProjectUsername: s.ProjectUsername, + ProjectStatus: s.ProjectStatus, + BackState: s, + }, bot.EditMessage) + + default: + s.Enter(b, bot.NewMessage) + } +} + +func (s *Placements) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return } + +func (s *Placements) Handle(_ *bot.Bot, _ *echotron.Update) { return } + +func (s *Placements) Exit() {} + +// PlacementsList - экран со списком размещений +type PlacementsList struct { + ProjectID string + ProjectTitle string + ProjectTelegramID int64 + ProjectUsername string + ProjectStatus string + BackState bot.State +} + +func (s *PlacementsList) Enter(b *bot.Bot, mode bot.RenderMode) { + s.render(b, mode) +} + +func (s *PlacementsList) render(b *bot.Bot, mode bot.RenderMode) { + resp, err := b.Backend.GetPlacements(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID) + if err != nil { + b.SendNew("❌ Не удалось загрузить размещения", Keyboard()) + return + } + + text := "Список размещений\n\n" + + var buttons [][]echotron.InlineKeyboardButton + + if len(resp.Placements) == 0 { + text += `Список размещений пуст. + +Размещение — это план публикации рекламы в канале: + ‣ Выбор креатива + ‣ Канал размещения + ‣ Стоимость и формат + ‣ Статус выполнения + +Создайте первое размещение` + } else { + for i, placement := range resp.Placements { + statusSymbol := "●" + if placement.Status == "archived" { + statusSymbol = "■" + } + + channelName := "Без названия" + if placement.Channel.Title != nil && *placement.Channel.Title != "" { + channelName = *placement.Channel.Title + } else if placement.Channel.Username != nil && *placement.Channel.Username != "" { + channelName = "@" + *placement.Channel.Username + } + + buttonText := fmt.Sprintf("%s #%d %s", statusSymbol, i+1, channelName) + + buttons = append(buttons, Row( + Button(buttonText, fmt.Sprintf("placement:%s", placement.ID)), + )) + } + } + + // Кнопка назад + buttons = append(buttons, Row( + Button("← Назад", "back"), + )) + + keyboard := Keyboard(buttons...) + b.Render(text, keyboard, mode) +} + +func (s *PlacementsList) HandleCallback(b *bot.Bot, u *echotron.Update) { + if u.CallbackQuery == nil || u.CallbackQuery.Data == "" { + return + } + + data := u.CallbackQuery.Data + + switch data { + case "back": + if s.BackState != nil { + b.SetState(s.BackState, bot.EditMessage) + } + default: // Проверяем, не нажали ли на конкретное размещение if len(data) > 10 && data[:10] == "placement:" { @@ -125,8 +176,8 @@ func (s *Placements) HandleCallback(b *bot.Bot, u *echotron.Update) { } } -func (s *Placements) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return } +func (s *PlacementsList) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return } -func (s *Placements) Handle(_ *bot.Bot, _ *echotron.Update) { return } +func (s *PlacementsList) Handle(_ *bot.Bot, _ *echotron.Update) { return } -func (s *Placements) Exit() {} +func (s *PlacementsList) Exit() {} diff --git a/tg_bot/screens/project_details.go b/tg_bot/screens/project_details.go index 954757d..54f902e 100644 --- a/tg_bot/screens/project_details.go +++ b/tg_bot/screens/project_details.go @@ -12,15 +12,14 @@ import ( ) type ProjectDetails struct { - Project *backend.Project - BackState bot.State - confirmingLinkTypeChange bool // Флаг для двухшагового подтверждения смены типа ссылки + Project *backend.Project + BackState bot.State } func (s *ProjectDetails) Enter(b *bot.Bot, mode bot.RenderMode) { if s.Project == nil { b.SendNew("❌ Проект не найден", Keyboard( - Row(Button("← Назад", "back_to_projects")), + Row(Button("← Назад", "back")), )) return } @@ -40,32 +39,25 @@ func (s *ProjectDetails) formatProjectDetails() string { text := fmt.Sprintf("%s\n\n", p.Title) - // Информация о канале if p.Username != nil && *p.Username != "" { - text += fmt.Sprintf("▸ Telegram: @%s\n", *p.Username) + text += fmt.Sprintf("▸ Tg username: @%s\n", *p.Username) } else { - text += "▸ Telegram: не привязан\n" + text += "▸ Tg username: не привязан\n" } // Статус проекта - var statusSymbol string - var statusText string + var statusSymbol, statusText string switch p.Status { case "active": - statusSymbol = "●" - statusText = "Активный" + statusSymbol, statusText = "●", "Активный" case "inactive": - statusSymbol = "○" - statusText = "Неактивен" + statusSymbol, statusText = "○", "Неактивен" case "archived": - statusSymbol = "■" - statusText = "Архивный" + statusSymbol, statusText = "■", "Архивный" case "paused": - statusSymbol = "◐" - statusText = "Приостановлен" + statusSymbol, statusText = "◐", "Приостановлен" default: - statusSymbol = "○" - statusText = p.Status + statusSymbol, statusText = "○", p.Status } text += fmt.Sprintf("▸ Статус: %s %s\n", statusSymbol, statusText) @@ -82,34 +74,20 @@ func (s *ProjectDetails) buildKeyboard() echotron.InlineKeyboardMarkup { Button("Размещения", fmt.Sprintf("placements:%s", s.Project.ID)), )) - // Второй ряд: Тип вступления по умолчанию для закупов (с подтверждением) - var linkTypeButtonText string - if s.confirmingLinkTypeChange { - // Показываем подтверждение - var newType string - if s.Project.PurchaseInviteTypeDefault == "public" { - newType = "По заявке" - } else { - newType = "Свободное" - } - linkTypeButtonText = fmt.Sprintf("Изменить → %s", newType) + // Второй ряд: Тип вступления по умолчанию для закупов + var currentType string + if s.Project.PurchaseInviteTypeDefault == "public" { + currentType = "открытая" } else { - // Показываем текущий тип вступления - var currentType string - if s.Project.PurchaseInviteTypeDefault == "public" { - currentType = "Свободное" - } else { - currentType = "По заявке" - } - linkTypeButtonText = fmt.Sprintf("Вступление: %s", currentType) + currentType = "с заявками" } buttons = append(buttons, Row( - Button(linkTypeButtonText, fmt.Sprintf("link_type:%s", s.Project.ID)), + Button(fmt.Sprintf("Ссылка: %s", currentType), fmt.Sprintf("link_type:%s", s.Project.ID)), )) // Нижний ряд: Назад и Архивировать buttons = append(buttons, Row( - Button("← Назад", "back_to_projects"), + Button("← Назад", "back"), Button("≡ Архивировать", fmt.Sprintf("archive:%s", s.Project.ID)), )) @@ -124,14 +102,12 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { data := u.CallbackQuery.Data switch { - case data == "back_to_projects", data == "back": - s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения + case data == "back": if s.BackState != nil { s.transitionWithNewMessage(b, s.BackState) } case strings.HasPrefix(data, "creatives:"): - s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения b.SetState(&Creatives{ ProjectID: s.Project.ID, ProjectTitle: s.Project.Title, @@ -142,7 +118,6 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { }, bot.EditMessage) case strings.HasPrefix(data, "placements:"): - s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения b.SetState(&Placements{ ProjectID: s.Project.ID, ProjectTitle: s.Project.Title, @@ -153,38 +128,28 @@ func (s *ProjectDetails) HandleCallback(b *bot.Bot, u *echotron.Update) { }, bot.EditMessage) case data == "back_to_project": - s.confirmingLinkTypeChange = false // Сбрасываем флаг подтверждения s.Enter(b, bot.EditMessage) case strings.HasPrefix(data, "link_type:"): - if !s.confirmingLinkTypeChange { - // Первое нажатие - включаем режим подтверждения - s.confirmingLinkTypeChange = true - s.Enter(b, bot.EditMessage) + var newType string + if s.Project.PurchaseInviteTypeDefault == "public" { + newType = "approval" } else { - // Второе нажатие - меняем тип ссылки - var newType string - if s.Project.PurchaseInviteTypeDefault == "public" { - newType = "approval" - } else { - newType = "public" - } - - updatedProject, err := b.Backend.UpdateProjectInviteLinkType(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.Project.ID, newType) - - if err != nil { - b.SendNew(fmt.Sprintf("❌ Ошибка при изменении типа ссылки: %v", err), Keyboard( - Row(Button("← Назад", "back_to_project")), - )) - return - } - - // Обновляем проект и сбрасываем флаг подтверждения - s.Project = updatedProject - s.confirmingLinkTypeChange = false - s.Enter(b, bot.EditMessage) + newType = "public" } + updatedProject, err := b.Backend.UpdateProjectInviteLinkType(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.Project.ID, newType) + + if err != nil { + b.SendNew(fmt.Sprintf("❌ Ошибка при изменении типа ссылки: %v", err), Keyboard( + Row(Button("← Назад", "back_to_project")), + )) + return + } + + s.Project = updatedProject + s.Enter(b, bot.EditMessage) + default: // Для остальных кнопок показываем заглушку b.Edit(fmt.Sprintf("🚧 Функция в разработке\n\nCallback: %s", data), Keyboard(