This commit is contained in:
Artem Tsyrulnikov
2026-02-03 22:20:56 +03:00
parent 7c8589e000
commit 09145e510a
10 changed files with 124 additions and 101 deletions

View File

@@ -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}')

View File

@@ -115,7 +115,7 @@ def _build_info_message(
# Add empty line before footer
lines.append('')
lines.append('Пост ниже')
lines.append('Пост ниже 👇')
return '\n'.join(lines)

View File

@@ -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 {

View File

@@ -20,6 +20,7 @@ type AddPurchase struct {
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
ProjectDefaultLinkType string
CreativeID string
CreativeTitle string
ProjectPage int
@@ -35,6 +36,7 @@ type projectInfo struct {
TelegramID int64
Username string
Status string
DefaultInviteType string
}
type creativeInfo struct {
@@ -89,6 +91,7 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) {
TelegramID: project.TelegramID,
Username: username,
Status: project.Status,
DefaultInviteType: project.PurchaseInviteTypeDefault,
}
}
@@ -268,6 +271,7 @@ func (s *AddPurchase) HandleCallback(b *bot.Bot, u *echotron.Update) {
ProjectTelegramID: s.ProjectTelegramID,
ProjectUsername: s.ProjectUsername,
ProjectStatus: s.ProjectStatus,
ProjectDefaultLinkType: s.ProjectDefaultLinkType,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: []PurchaseChannelInput{},
@@ -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

View File

@@ -35,6 +35,7 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) {
res, err := b.SendMessage(fmt.Sprintf(msgHelloLogin, usernameDisplay), b.ChatID, &echotron.MessageOptions{
ReplyMarkup: kb,
ParseMode: echotron.HTML,
LinkPreviewOptions: echotron.LinkPreviewOptions{IsDisabled: true},
})
if err != nil {
log.Err(err).Msg("SendMessage error")

View File

@@ -14,7 +14,7 @@ type MainMenu struct{}
const msgAbout = `
<i>Главное меню</i>
<blockquote>
<blockquote expandable>
За что отвечает каждая кнопка в этом окне:
<b>• Рабочее пространство</b> — Одно можно использовать для своих проектов, а другое, например, для проектов, где вы являетесь закупщиком.

View File

@@ -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...)

View File

@@ -17,6 +17,7 @@ import (
type PurchaseOptionalDetails struct {
ProjectID string
ProjectTitle string
ProjectDefaultLinkType string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
@@ -256,6 +257,7 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
b.SetState(&SelectChannelsForPurchase{
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectDefaultLinkType: s.ProjectDefaultLinkType,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,
@@ -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 += "<b>Выберите тип закупа</b>"
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 += "<b>Введите тип закупа</b>\n\nНапример: <code>вп</code>"
case "cost_value":
text += "<b>Ввод стоимости</b>\n\nНапример: <code>15000</code>"
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
}

View File

@@ -27,6 +27,7 @@ type SelectChannelsForPurchase struct {
ProjectTelegramID int64
ProjectUsername string
ProjectStatus string
ProjectDefaultLinkType string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
@@ -227,6 +228,7 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
optionalDetails = &PurchaseOptionalDetails{
ProjectID: s.ProjectID,
ProjectTitle: s.ProjectTitle,
ProjectDefaultLinkType: s.ProjectDefaultLinkType,
CreativeID: s.CreativeID,
CreativeTitle: s.CreativeTitle,
Channels: s.Channels,

View File

@@ -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)
}
}