правки
This commit is contained in:
@@ -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() {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user