правки

This commit is contained in:
Artem Tsyrulnikov
2026-02-18 20:47:19 +03:00
parent e48cd7bccc
commit 4b9e5bf4df
12 changed files with 92 additions and 70 deletions

View File

@@ -2,47 +2,40 @@ package screens
import (
"fmt"
"strings"
"github.com/NicoNex/echotron/v3"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
)
const addProjectInstructionText = `
<b>Как добавить канал в проект</b>
<b>Инструкция:</b>
<b>1.</b> Добавьте бота в ваш канал как администратора
<b>2.</b> Дайте боту следующие права:
• Удаление сообщений
• Приглашение пользователей
<b>3.</b> После добавления бота:
• Если у вас <b>1 рабочее пространство</b> — канал добавится автоматически
• Если у вас <b>несколько рабочих пространств</b> — вы получите уведомление с кнопкой выбора
<b>4.</b> Готово! Канал появится в списке проектов
`
type AddProject struct {
BackState bot.State
}
const addProjectInstructionText = `
<b>Как добавить канал в проект</b>
Для подключения канала к боту, назначьте @%s администратором канала, выдав следующие права:
➔ Управление сообщениями
➔ Добавление участников
После добавления бота:
• Если у вас <b>1 рабочее пространство</b> — канал добавится автоматически
• Если у вас <b>несколько рабочих пространств</b> — вы получите уведомление с кнопкой выбора
`
func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
botUsername := bot.BotUsername()
if botUsername == "" {
botUsername = "chat_cruiser_bot"
}
botUsername := strings.TrimPrefix(bot.Username(), "@")
addBotURL := fmt.Sprintf("https://t.me/%s?startchannel&admin=invite_users+post_messages+edit_messages+delete_messages", botUsername)
buttons := [][]echotron.InlineKeyboardButton{
Row(URLButton("Добавить", addBotURL)),
Row(URLButton("Назначить администратором", addBotURL)),
Row(Button("← Назад", "back")),
}
keyboard := Keyboard(buttons...)
b.Render(addProjectInstructionText, keyboard, mode)
b.Render(fmt.Sprintf(addProjectInstructionText, botUsername), keyboard, mode)
}
func (s *AddProject) HandleCallback(b *bot.Bot, u *echotron.Update) {
@@ -58,8 +51,8 @@ func (s *AddProject) HandleCallback(b *bot.Bot, u *echotron.Update) {
}
}
func (s *AddProject) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
func (s *AddProject) HandleMessage(_ *bot.Bot, _ *echotron.Update) {}
func (s *AddProject) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *AddProject) Handle(_ *bot.Bot, _ *echotron.Update) {}
func (s *AddProject) Exit() {}

View File

@@ -65,7 +65,6 @@ const buttonURLPlaceholder = "https://example.com"
var inviteLinkPattern = regexp.MustCompile(`https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+`)
var inviteLinkTagPattern = regexp.MustCompile(`<a\s+class="tg-link">\s*(.*?)\s*</a>`)
var htmlTagPattern = regexp.MustCompile(`<[^>]+>`)
const (
inputModeText = "text"
@@ -193,7 +192,7 @@ func (e *CreativeEditorFields) GetCreativeText() string {
sub := inviteLinkTagPattern.FindStringSubmatch(match)
inner := ""
if len(sub) > 1 {
inner = strings.TrimSpace(htmlTagPattern.ReplaceAllString(sub[1], ""))
inner = strings.TrimSpace(sub[1])
}
if inner == "" {
inner = inviteLinkPreviewURL

View File

@@ -9,6 +9,7 @@ import (
"github.com/NicoNex/echotron/v3"
"github.com/TelegramExchange/tgex-backend/tg_bot/backend"
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
)
type PlacementDetails struct {
@@ -247,16 +248,16 @@ func formatDateTime(value string) string {
// Try parsing with timezone
if parsed, err = time.Parse(time.RFC3339, value); err == nil {
return formatCompactDateTime(parsed)
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
if parsed, err = time.Parse(time.RFC3339Nano, value); err == nil {
return formatCompactDateTime(parsed)
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
if parsed, err = time.Parse("2006-01-02T15:04:05", value); err == nil {
return formatCompactDateTime(parsed)
if parsed, err = time.ParseInLocation("2006-01-02T15:04:05", value, ui.MskLocation); err == nil {
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
if parsed, err = time.Parse("2006-01-02", value); err == nil {
return formatCompactDateTime(parsed)
if parsed, err = time.ParseInLocation("2006-01-02", value, ui.MskLocation); err == nil {
return formatCompactDateTime(parsed.In(ui.MskLocation))
}
return value
}

View File

@@ -133,7 +133,8 @@ func (s *PlacementsList) render(b *bot.Bot, mode bot.RenderMode) {
// Формируем кнопки для каждого размещения
buttons := make([]echotron.InlineKeyboardButton, 0, len(page.Items))
for i, placement := range page.Items {
// Обратный глобальный номер (последнее = #1, первое = #N)
// Нумерация по порядку создания: первое размещение = #1 (на последней странице),
// последнее размещение = #N (вверху).
globalNum := page.Total - (s.CurrentPage * placementsPerPage) - i
// Название канала
@@ -150,10 +151,10 @@ func (s *PlacementsList) render(b *bot.Bot, mode bot.RenderMode) {
dateStr = formatDate(*placement.Details.PlacementAt)
}
// Текст кнопки: #N • Канал (с пробелом и разделителем)
buttonText := fmt.Sprintf("%d • %s", globalNum, channelName)
// Текст кнопки: #N • Канал • DD.MM
buttonText := fmt.Sprintf("#%d • %s", globalNum, channelName)
if dateStr != "" {
buttonText += fmt.Sprintf(" %s", dateStr)
buttonText += fmt.Sprintf(" %s", dateStr)
}
buttons = append(buttons, echotron.InlineKeyboardButton{
@@ -225,22 +226,21 @@ func (s *PlacementsList) Handle(_ *bot.Bot, _ *echotron.Update) { return }
func (s *PlacementsList) Exit() {}
// formatDate парсит ISO дату и форматирует как "27 янв"
// formatDate парсит ISO дату и форматирует как "27.01"
func formatDate(isoDate string) string {
// Пробуем парсить RFC3339
t, err := time.Parse(time.RFC3339, isoDate)
if err != nil {
// Пробуем другие форматы
t, err = time.Parse("2006-01-02T15:04:05", isoDate)
t, err = time.ParseInLocation("2006-01-02T15:04:05", isoDate, ui.MskLocation)
if err != nil {
return ""
// Иногда бэкенд может вернуть только дату без времени
t, err = time.ParseInLocation("2006-01-02", isoDate, ui.MskLocation)
if err != nil {
return ""
}
}
}
// Русские названия месяцев
months := []string{"янв", "фев", "мар", "апр", "мая", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"}
day := t.Day()
month := months[t.Month()-1]
return fmt.Sprintf("%d %s", day, month)
return t.In(ui.MskLocation).Format("02.01")
}

View File

@@ -147,6 +147,7 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
Title: "Дата оплаты",
Key: "payment_date",
IncludeTime: false,
AllowPast: true,
Selected: s.PaymentDate,
BackState: s,
}), bot.EditMessage)
@@ -508,7 +509,11 @@ func (s *PurchaseOptionalDetails) costTypeLabel() string {
}
func (s *PurchaseOptionalDetails) costBeforeTypeLabel() string {
if s.CostBeforeType == "cpm" {
costType := s.CostBeforeType
if costType == "" && s.CostBeforeBargain != nil {
costType = s.CostBeforeBargain.Type
}
if costType == "cpm" {
return "СРМ"
}
return "Фикс"
@@ -1144,6 +1149,7 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
Title: "Дата и время размещения",
Key: "placement_datetime",
IncludeTime: true,
AllowPast: true,
Selected: s.PlacementDateTime,
BackState: s,
}), bot.EditMessage)
@@ -1154,6 +1160,7 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
Title: "Дата оплаты",
Key: "payment_date",
IncludeTime: false,
AllowPast: true,
Selected: s.PaymentDate,
BackState: s,
}), bot.EditMessage)
@@ -1201,6 +1208,7 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
Title: "Дата и время размещения",
Key: "placement_datetime:" + channelKey,
IncludeTime: true,
AllowPast: true,
Selected: s.PlacementByChannel[channelKey],
BackState: s,
}), bot.EditMessage)
@@ -1210,6 +1218,7 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
Title: "Дата оплаты",
Key: "payment_date:" + channelKey,
IncludeTime: false,
AllowPast: true,
Selected: s.PaymentDateByChannel[channelKey],
BackState: s,
}), bot.EditMessage)
@@ -2093,13 +2102,13 @@ func (s *PurchaseOptionalDetails) toggleCostType() {
func (s *PurchaseOptionalDetails) toggleCostBeforeType() {
if s.CurrentChannel == "" {
if s.CostBeforeBargain == nil {
s.CostBeforeBargain = &CostEntry{}
}
if s.costBeforeTypeLabel() == "СРМ" {
s.CostBeforeBargain.Type = "fixed"
s.CostBeforeType = "fixed"
} else {
s.CostBeforeBargain.Type = "cpm"
s.CostBeforeType = "cpm"
}
if s.CostBeforeBargain != nil {
s.CostBeforeBargain.Type = s.CostBeforeType
}
return
}

View File

@@ -24,6 +24,7 @@ type DateTimePickerConfig struct {
Title string
Key string
IncludeTime bool
AllowPast bool
Selected *time.Time
BackState bot.State
}
@@ -36,6 +37,7 @@ type DateTimePicker struct {
Title string
Key string
IncludeTime bool
AllowPast bool
BackState bot.State
view string
@@ -63,6 +65,7 @@ func NewDateTimePicker(cfg DateTimePickerConfig) *DateTimePicker {
Title: cfg.Title,
Key: cfg.Key,
IncludeTime: cfg.IncludeTime,
AllowPast: cfg.AllowPast,
BackState: cfg.BackState,
view: pickerViewCalendar,
year: year,
@@ -208,7 +211,7 @@ func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup {
for day := 1; day <= daysInMonth; day++ {
date := time.Date(s.year, s.month, day, 0, 0, 0, 0, MskLocation)
if date.Before(todayDate) {
if !s.AllowPast && date.Before(todayDate) {
row = append(row, echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"})
} else {
label := fmt.Sprintf("%d", day)
@@ -462,6 +465,9 @@ func (s *DateTimePicker) minuteButton(minute int) echotron.InlineKeyboardButton
}
func (s *DateTimePicker) isHourDisabled(hour int) bool {
if s.AllowPast {
return false
}
if s.selectedDate == nil {
return false
}
@@ -475,6 +481,9 @@ func (s *DateTimePicker) isHourDisabled(hour int) bool {
}
func (s *DateTimePicker) isMinuteDisabled(minute int) bool {
if s.AllowPast {
return false
}
if s.selectedDate == nil || s.selectedHour == nil {
return false
}
@@ -491,6 +500,16 @@ func (s *DateTimePicker) isMinuteDisabled(minute int) bool {
}
func (s *DateTimePicker) prevMonth() {
if s.AllowPast {
if s.month == time.January {
s.month = time.December
s.year--
} else {
s.month--
}
return
}
now := time.Now().In(MskLocation)
if s.year == now.Year() && s.month == now.Month() {
return
@@ -513,6 +532,10 @@ func (s *DateTimePicker) nextMonth() {
}
func (s *DateTimePicker) monthPrevButton() echotron.InlineKeyboardButton {
if s.AllowPast {
return echotron.InlineKeyboardButton{Text: "←", CallbackData: dtpCallbackPrefix + "month_prev"}
}
now := time.Now().In(MskLocation)
if s.year == now.Year() && s.month == now.Month() {
return echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"}
@@ -554,7 +577,7 @@ func (s *DateTimePicker) HandleMessage(b *bot.Bot, u *echotron.Update) {
input := strings.TrimSpace(u.Message.Text)
switch s.view {
case pickerViewCalendar:
value, hasDate, hasTime := parseDateTimeInput(input, s.selectedDate, s.selectedHour, s.selectedMinute)
value, hasDate, hasTime := parseDateTimeInput(input, s.selectedDate, s.selectedHour, s.selectedMinute, s.AllowPast)
if !hasDate {
s.Enter(b, bot.NewMessage)
return
@@ -573,7 +596,7 @@ func (s *DateTimePicker) HandleMessage(b *bot.Bot, u *echotron.Update) {
}
s.Enter(b, bot.NewMessage)
case pickerViewTimeCombined:
value, hasDate, hasTime := parseDateTimeInput(input, s.selectedDate, s.selectedHour, s.selectedMinute)
value, hasDate, hasTime := parseDateTimeInput(input, s.selectedDate, s.selectedHour, s.selectedMinute, s.AllowPast)
if !hasTime {
s.Enter(b, bot.NewMessage)
return
@@ -587,7 +610,7 @@ func (s *DateTimePicker) HandleMessage(b *bot.Bot, u *echotron.Update) {
s.selectedMinute = &minute
s.Enter(b, bot.NewMessage)
default:
value, hasDate, hasTime := parseDateTimeInput(input, s.selectedDate, s.selectedHour, s.selectedMinute)
value, hasDate, hasTime := parseDateTimeInput(input, s.selectedDate, s.selectedHour, s.selectedMinute, s.AllowPast)
if !hasDate && !hasTime {
s.Enter(b, bot.NewMessage)
return
@@ -611,6 +634,7 @@ func parseDateTimeInput(
currentDate *time.Time,
currentHour *int,
currentMinute *int,
allowPast bool,
) (time.Time, bool, bool) {
text := strings.ToLower(strings.TrimSpace(input))
text = strings.ReplaceAll(text, ",", " ")
@@ -637,7 +661,7 @@ func parseDateTimeInput(
}
value := time.Date(date.Year(), date.Month(), date.Day(), hour, minute, 0, 0, MskLocation)
if value.Before(now) {
if !allowPast && value.Before(now) {
return time.Time{}, false, false
}
return value, dateOk, timeOk