diff --git a/src/adapter/telegram_bot.py b/src/adapter/telegram_bot.py index 83c9c5b..44be1a1 100644 --- a/src/adapter/telegram_bot.py +++ b/src/adapter/telegram_bot.py @@ -9,6 +9,7 @@ from aiogram.types import ( InputMediaDocument, InputMediaPhoto, InputMediaVideo, + LinkPreviewOptions, ) from shared.telegram_base import TelegramBase @@ -23,14 +24,14 @@ class TelegramBot(TelegramBase, TelegramBotWriter): text: str, chat_id: int, parse_mode: str | None = None, - disable_preview: bool = False, + disable_preview: bool = True, reply_to_message_id: int | None = None, ) -> int: message = await self.bot.send_message( chat_id=chat_id, text=text, parse_mode=parse_mode, - disable_web_page_preview=disable_preview, + link_preview_options=LinkPreviewOptions(is_disabled=disable_preview), reply_to_message_id=reply_to_message_id, ) return message.message_id @@ -49,7 +50,7 @@ class TelegramBot(TelegramBase, TelegramBotWriter): chat_id: int, buttons: list[list[InlineKeyboardButton]], parse_mode: str | None = None, - disable_preview: bool = False, + disable_preview: bool = True, reply_to_message_id: int | None = None, ) -> int: keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) @@ -58,7 +59,7 @@ class TelegramBot(TelegramBase, TelegramBotWriter): text=text, reply_markup=keyboard, parse_mode=parse_mode, - disable_web_page_preview=disable_preview, + link_preview_options=LinkPreviewOptions(is_disabled=disable_preview), reply_to_message_id=reply_to_message_id, ) return message.message_id @@ -110,6 +111,7 @@ class TelegramBot(TelegramBase, TelegramBotWriter): parse_mode=parse_mode, reply_markup=keyboard, reply_to_message_id=reply_to_message_id, + link_preview_options=LinkPreviewOptions(is_disabled=True), ) return message.message_id @@ -128,9 +130,17 @@ class TelegramBot(TelegramBase, TelegramBotWriter): for index, item in enumerate(media_items): item_caption = caption if index == 0 else None if item.media_type == 'photo': - group.append(InputMediaPhoto(media=item.media_file_id, caption=item_caption, parse_mode=parse_mode)) + group.append(InputMediaPhoto( + media=item.media_file_id, + caption=item_caption, + parse_mode=parse_mode, + )) elif item.media_type == 'video': - group.append(InputMediaVideo(media=item.media_file_id, caption=item_caption, parse_mode=parse_mode)) + group.append(InputMediaVideo( + media=item.media_file_id, + caption=item_caption, + parse_mode=parse_mode, + )) else: raise ValueError(f'Unsupported media type for group: {item.media_type}') diff --git a/src/usecase/purchase/build_placement_creative.py b/src/usecase/purchase/build_placement_creative.py index a8f5aa1..687f6a8 100644 --- a/src/usecase/purchase/build_placement_creative.py +++ b/src/usecase/purchase/build_placement_creative.py @@ -115,7 +115,7 @@ def _build_info_message( # Add empty line before footer lines.append('') - lines.append('Пост ниже') + lines.append('Пост ниже 👇') return '\n'.join(lines) diff --git a/tg_bot/bot/bot.go b/tg_bot/bot/bot.go index 6d6618e..7385598 100644 --- a/tg_bot/bot/bot.go +++ b/tg_bot/bot/bot.go @@ -196,7 +196,11 @@ func (b *Bot) SendNew(text string, keyboard echotron.InlineKeyboardMarkup) { b.cleanupKeyboard(b.LastMessageID) } - res, err := b.SendMessage(text, b.ChatID, &echotron.MessageOptions{ReplyMarkup: keyboard, ParseMode: echotron.HTML}) + res, err := b.SendMessage(text, b.ChatID, &echotron.MessageOptions{ + ReplyMarkup: keyboard, + ParseMode: echotron.HTML, + LinkPreviewOptions: echotron.LinkPreviewOptions{IsDisabled: true}, + }) if err != nil { log.Error().Err(err).Msg("SendMessage failed") return @@ -238,9 +242,17 @@ func (b *Bot) Edit(text string, keyboard echotron.InlineKeyboardMarkup) { msgID := echotron.NewMessageID(b.ChatID, b.LastMessageID) if b.lastRender.isMedia { - _, err = b.EditMessageCaption(msgID, &echotron.MessageCaptionOptions{Caption: text, ParseMode: echotron.HTML, ReplyMarkup: keyboard}) + _, err = b.EditMessageCaption(msgID, &echotron.MessageCaptionOptions{ + Caption: text, + ParseMode: echotron.HTML, + ReplyMarkup: keyboard, + }) } else { - _, err = b.EditMessageText(text, msgID, &echotron.MessageTextOptions{ReplyMarkup: keyboard, ParseMode: echotron.HTML}) + _, err = b.EditMessageText(text, msgID, &echotron.MessageTextOptions{ + ReplyMarkup: keyboard, + ParseMode: echotron.HTML, + LinkPreviewOptions: echotron.LinkPreviewOptions{IsDisabled: true}, + }) } if err == nil { diff --git a/tg_bot/screens/add_purchase.go b/tg_bot/screens/add_purchase.go index d146e59..a0f3720 100644 --- a/tg_bot/screens/add_purchase.go +++ b/tg_bot/screens/add_purchase.go @@ -15,26 +15,28 @@ const addPurchaseCreativesPerPage = 5 const addPurchaseProjectsPerPage = 6 type AddPurchase struct { - ProjectID string - ProjectTitle string - ProjectTelegramID int64 - ProjectUsername string - ProjectStatus string - CreativeID string - CreativeTitle string - ProjectPage int - CreativePage int - ActivePicker string // "" | "project_list" | "creative_list" - BackState bot.State - lastProjects map[string]projectInfo - lastCreatives map[string]creativeInfo + ProjectID string + ProjectTitle string + ProjectTelegramID int64 + ProjectUsername string + ProjectStatus string + ProjectDefaultLinkType string + CreativeID string + CreativeTitle string + ProjectPage int + CreativePage int + ActivePicker string // "" | "project_list" | "creative_list" + BackState bot.State + lastProjects map[string]projectInfo + lastCreatives map[string]creativeInfo } type projectInfo struct { - Title string - TelegramID int64 - Username string - Status string + Title string + TelegramID int64 + Username string + Status string + DefaultInviteType string } type creativeInfo struct { @@ -85,10 +87,11 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) { username = *project.Username } s.lastProjects[project.ID] = projectInfo{ - Title: project.Title, - TelegramID: project.TelegramID, - Username: username, - Status: project.Status, + Title: project.Title, + TelegramID: project.TelegramID, + Username: username, + Status: project.Status, + DefaultInviteType: project.PurchaseInviteTypeDefault, } } @@ -263,17 +266,18 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) { } if s.ProjectID != "" && s.CreativeID != "" { b.SetState(&SelectChannelsForPurchase{ - ProjectID: s.ProjectID, - ProjectTitle: s.ProjectTitle, - ProjectTelegramID: s.ProjectTelegramID, - ProjectUsername: s.ProjectUsername, - ProjectStatus: s.ProjectStatus, - CreativeID: s.CreativeID, - CreativeTitle: s.CreativeTitle, - Channels: []PurchaseChannelInput{}, - Duplicates: []string{}, - ParsingErrors: []ParseError{}, - BackState: s.BackState, + ProjectID: s.ProjectID, + ProjectTitle: s.ProjectTitle, + ProjectTelegramID: s.ProjectTelegramID, + ProjectUsername: s.ProjectUsername, + ProjectStatus: s.ProjectStatus, + ProjectDefaultLinkType: s.ProjectDefaultLinkType, + CreativeID: s.CreativeID, + CreativeTitle: s.CreativeTitle, + Channels: []PurchaseChannelInput{}, + Duplicates: []string{}, + ParsingErrors: []ParseError{}, + BackState: s.BackState, }, bot.EditMessage) } @@ -286,6 +290,7 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) { s.ProjectTelegramID = info.TelegramID s.ProjectUsername = info.Username s.ProjectStatus = info.Status + s.ProjectDefaultLinkType = info.DefaultInviteType } } s.ProjectID = projectID diff --git a/tg_bot/screens/login.go b/tg_bot/screens/login.go index d2060ca..d4e45e8 100644 --- a/tg_bot/screens/login.go +++ b/tg_bot/screens/login.go @@ -33,8 +33,9 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) { kb := Keyboard(Row(URLButton("Войти на сайт", loginURL))) res, err := b.SendMessage(fmt.Sprintf(msgHelloLogin, usernameDisplay), b.ChatID, &echotron.MessageOptions{ - ReplyMarkup: kb, - ParseMode: echotron.HTML, + ReplyMarkup: kb, + ParseMode: echotron.HTML, + LinkPreviewOptions: echotron.LinkPreviewOptions{IsDisabled: true}, }) if err != nil { log.Err(err).Msg("SendMessage error") diff --git a/tg_bot/screens/main_menu.go b/tg_bot/screens/main_menu.go index 011f48e..8ecf105 100644 --- a/tg_bot/screens/main_menu.go +++ b/tg_bot/screens/main_menu.go @@ -14,7 +14,7 @@ type MainMenu struct{} const msgAbout = ` Главное меню -
+За что отвечает каждая кнопка в этом окне: • Рабочее пространство — Одно можно использовать для своих проектов, а другое, например, для проектов, где вы являетесь закупщиком. diff --git a/tg_bot/screens/placements.go b/tg_bot/screens/placements.go index b3b1a94..d60ceac 100644 --- a/tg_bot/screens/placements.go +++ b/tg_bot/screens/placements.go @@ -29,8 +29,7 @@ func (s *Placements) Enter(b *bot.Bot, mode bot.RenderMode) { var rows [][]echotron.InlineKeyboardButton rows = append(rows, Row(Button("+ Создать размещение", "add_purchase"))) - rows = append(rows, Row(Button("Размещения", "placements_list"))) - rows = append(rows, Row(URLButton("План закупов", fmt.Sprintf("https://app.smart-post.ru/dashboard/%s/purchase-plans/%s", b.Session.WorkspaceID, s.ProjectID)))) + rows = append(rows, Row(Button("Список размещений", "placements_list"), URLButton("План закупов", fmt.Sprintf("https://app.smart-post.ru/dashboard/%s/purchase-plans/%s", b.Session.WorkspaceID, s.ProjectID)))) rows = append(rows, Row(Button("← Назад", "back"))) keyboard := Keyboard(rows...) diff --git a/tg_bot/screens/purchase_optional_details.go b/tg_bot/screens/purchase_optional_details.go index a5f2ab8..a6156ed 100644 --- a/tg_bot/screens/purchase_optional_details.go +++ b/tg_bot/screens/purchase_optional_details.go @@ -17,6 +17,7 @@ import ( type PurchaseOptionalDetails struct { ProjectID string ProjectTitle string + ProjectDefaultLinkType string CreativeID string CreativeTitle string Channels []PurchaseChannelInput @@ -254,15 +255,16 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update) case "back": // Сохраняем текущее состояние перед возвратом назад b.SetState(&SelectChannelsForPurchase{ - ProjectID: s.ProjectID, - ProjectTitle: s.ProjectTitle, - CreativeID: s.CreativeID, - CreativeTitle: s.CreativeTitle, - Channels: s.Channels, - Duplicates: []string{}, - ParsingErrors: []ParseError{}, - OptionalDetailsState: s, // Сохраняем текущее состояние - BackState: s.BackState, + ProjectID: s.ProjectID, + ProjectTitle: s.ProjectTitle, + ProjectDefaultLinkType: s.ProjectDefaultLinkType, + CreativeID: s.CreativeID, + CreativeTitle: s.CreativeTitle, + Channels: s.Channels, + Duplicates: []string{}, + ParsingErrors: []ParseError{}, + OptionalDetailsState: s, // Сохраняем текущее состояние + BackState: s.BackState, }, bot.EditMessage) case "next": jwt := b.Session.JWT @@ -314,11 +316,6 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update) } if strings.HasPrefix(u.CallbackQuery.Data, "type:") { value := strings.TrimPrefix(u.CallbackQuery.Data, "type:") - if value == "custom" { - s.InputMode = "type_custom" - s.Enter(b, bot.EditMessage) - return - } // Нормализуем значение для БД switch value { case "self_promo": @@ -372,12 +369,6 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update) } switch s.InputMode { - case "type_custom": - if s.CurrentChannel != "" { - s.PurchaseTypeByChannel[s.CurrentChannel] = text - } else { - s.PurchaseType = text - } case "format_custom": s.setFormatValue(text) case "comment": @@ -639,11 +630,7 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM case "type": text += "Выберите тип закупа" keyboard := Keyboard( - Row( - Button("Самопиар", "type:self_promo"), - Button("Стандарт", "type:standard"), - ), - Row(Button("Другое", "type:custom")), + Row(Button("Самопиар", "type:self_promo"), Button("Стандарт", "type:standard")), Row(Button("← Назад", "back_to_optional")), ) if mode == bot.EditMessage { @@ -652,8 +639,6 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM b.SendNew(text, keyboard) } return - case "type_custom": - text += "Введите тип закупа\n\nНапример:вп" case "cost_value": text += "Ввод стоимости\n\nНапример:15000" if s.CurrentChannel != "" { @@ -1639,6 +1624,12 @@ func (s *PurchaseOptionalDetails) ensureDefaults() { if s.InviteLinkTypeMode == "" { s.InviteLinkTypeMode = "common" } + if s.InviteLinkType == "" { + s.InviteLinkType = s.ProjectDefaultLinkType + if s.InviteLinkType == "" { + s.InviteLinkType = "approval" + } + } if s.PlacementByChannel == nil { s.PlacementByChannel = make(map[string]*time.Time) } @@ -2255,22 +2246,22 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string, placeme // Placement date if s.PlacementMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.PlacementByChannel[channelKey]; ok && value != nil { - formatted := value.Format(time.RFC3339) + formatted := value.UTC().Format(time.RFC3339) details.PlacementAt = &formatted } } else if s.PlacementDateTime != nil { - formatted := s.PlacementDateTime.Format(time.RFC3339) + formatted := s.PlacementDateTime.UTC().Format(time.RFC3339) details.PlacementAt = &formatted } // Payment date if s.PaymentDateMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.PaymentDateByChannel[channelKey]; ok && value != nil { - formatted := value.Format(time.RFC3339) + formatted := value.UTC().Format(time.RFC3339) details.PaymentAt = &formatted } } else if s.PaymentDate != nil { - formatted := s.PaymentDate.Format(time.RFC3339) + formatted := s.PaymentDate.UTC().Format(time.RFC3339) details.PaymentAt = &formatted } diff --git a/tg_bot/screens/select_channels_for_purchase.go b/tg_bot/screens/select_channels_for_purchase.go index e4b0038..ef14542 100644 --- a/tg_bot/screens/select_channels_for_purchase.go +++ b/tg_bot/screens/select_channels_for_purchase.go @@ -22,20 +22,21 @@ type PurchaseChannelInput struct { } type SelectChannelsForPurchase struct { - ProjectID string - ProjectTitle string - ProjectTelegramID int64 - ProjectUsername string - ProjectStatus string - CreativeID string - CreativeTitle string - Channels []PurchaseChannelInput - CurrentPage int - InvalidUsernames []string - Duplicates []string // дубликаты каналов - ParsingErrors []ParseError // ошибки парсинга с предложениями - OptionalDetailsState *PurchaseOptionalDetails // сохранённое состояние деталей - BackState bot.State + ProjectID string + ProjectTitle string + ProjectTelegramID int64 + ProjectUsername string + ProjectStatus string + ProjectDefaultLinkType string + CreativeID string + CreativeTitle string + Channels []PurchaseChannelInput + CurrentPage int + InvalidUsernames []string + Duplicates []string // дубликаты каналов + ParsingErrors []ParseError // ошибки парсинга с предложениями + OptionalDetailsState *PurchaseOptionalDetails // сохранённое состояние деталей + BackState bot.State } // ParseError представляет ошибку парсинга с предложением исправления @@ -225,12 +226,13 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat optionalDetails.Channels = s.Channels } else { optionalDetails = &PurchaseOptionalDetails{ - ProjectID: s.ProjectID, - ProjectTitle: s.ProjectTitle, - CreativeID: s.CreativeID, - CreativeTitle: s.CreativeTitle, - Channels: s.Channels, - BackState: s, + ProjectID: s.ProjectID, + ProjectTitle: s.ProjectTitle, + ProjectDefaultLinkType: s.ProjectDefaultLinkType, + CreativeID: s.CreativeID, + CreativeTitle: s.CreativeTitle, + Channels: s.Channels, + BackState: s, } } b.SetState(optionalDetails, bot.EditMessage) diff --git a/tg_bot/screens/ui/date_time_picker.go b/tg_bot/screens/ui/date_time_picker.go index 1f416db..067cb65 100644 --- a/tg_bot/screens/ui/date_time_picker.go +++ b/tg_bot/screens/ui/date_time_picker.go @@ -130,7 +130,8 @@ func (s *DateTimePicker) HandleCallback(b *bot.Bot, u *echotron.Update) { s.view = pickerViewCalendar } case strings.HasPrefix(payload, "day:"): - s.handleDaySelect(payload) + s.handleDaySelect(payload, b) + return // confirmSelection уже делает Enter case strings.HasPrefix(payload, "hour:"): s.handleHourSelect(payload) case strings.HasPrefix(payload, "min:"): @@ -140,6 +141,7 @@ func (s *DateTimePicker) HandleCallback(b *bot.Bot, u *echotron.Update) { return } + // Если не было подтверждения (для дат без времени), перерисовываем s.Enter(b, bot.EditMessage) } @@ -365,7 +367,7 @@ func (s *DateTimePicker) displayDate() (time.Time, bool) { return s.selectedDate.In(MskLocation), true } -func (s *DateTimePicker) handleDaySelect(payload string) { +func (s *DateTimePicker) handleDaySelect(payload string, b *bot.Bot) { datePart := strings.TrimPrefix(payload, "day:") parsed, err := time.ParseInLocation("2006-01-02", datePart, MskLocation) if err != nil { @@ -377,7 +379,8 @@ func (s *DateTimePicker) handleDaySelect(payload string) { s.selectedHour = nil s.selectedMinute = nil } else { - s.view = pickerViewConfirm + // Сразу подтверждаем выбор для даты без времени + s.confirmSelection(b) } }