правки
This commit is contained in:
@@ -436,7 +436,7 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
await query.prefetch_related('project', 'project__channel', 'channel', 'creative')
|
await query.prefetch_related('project', 'project__channel', 'channel', 'creative')
|
||||||
.order_by('-placement_at', '-created_at')
|
.order_by('-created_at')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -80,11 +80,6 @@ _INVITE_LINK_HTML = re.compile(
|
|||||||
)
|
)
|
||||||
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+', re.IGNORECASE)
|
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+', re.IGNORECASE)
|
||||||
_INVITE_LINK_REPLACED = re.compile(r'<a\s+class=["\']tg-link["\']>(.*?)</a>', re.IGNORECASE | re.DOTALL)
|
_INVITE_LINK_REPLACED = re.compile(r'<a\s+class=["\']tg-link["\']>(.*?)</a>', re.IGNORECASE | re.DOTALL)
|
||||||
_HTML_TAG = re.compile(r'<[^>]+>')
|
|
||||||
|
|
||||||
|
|
||||||
def _strip_html(text: str) -> str:
|
|
||||||
return _HTML_TAG.sub('', text).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def replace_invite_link_with_tag(text: str) -> str:
|
def replace_invite_link_with_tag(text: str) -> str:
|
||||||
@@ -105,7 +100,7 @@ def replace_invite_link_with_tag(text: str) -> str:
|
|||||||
|
|
||||||
# Normalize already replaced links (preserve anchor text)
|
# Normalize already replaced links (preserve anchor text)
|
||||||
def _normalize_replaced(match: re.Match[str]) -> str:
|
def _normalize_replaced(match: re.Match[str]) -> str:
|
||||||
inner = _strip_html(match.group(1))
|
inner = match.group(1).strip()
|
||||||
if inner == "" or _INVITE_LINK_PLAIN.search(inner):
|
if inner == "" or _INVITE_LINK_PLAIN.search(inner):
|
||||||
return '<a class="tg-link"></a>'
|
return '<a class="tg-link"></a>'
|
||||||
return f'<a class="tg-link">{inner}</a>'
|
return f'<a class="tg-link">{inner}</a>'
|
||||||
@@ -114,7 +109,7 @@ def replace_invite_link_with_tag(text: str) -> str:
|
|||||||
|
|
||||||
# Replace HTML links with tracking tags (preserve anchor text)
|
# Replace HTML links with tracking tags (preserve anchor text)
|
||||||
def _replace_html(match: re.Match[str]) -> str:
|
def _replace_html(match: re.Match[str]) -> str:
|
||||||
inner = _strip_html(match.group(1))
|
inner = match.group(1).strip()
|
||||||
if _INVITE_LINK_PLAIN.fullmatch(inner):
|
if _INVITE_LINK_PLAIN.fullmatch(inner):
|
||||||
return '<a class="tg-link"></a>'
|
return '<a class="tg-link"></a>'
|
||||||
return f'<a class="tg-link">{inner}</a>'
|
return f'<a class="tg-link">{inner}</a>'
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
_INVITE_LINK_TAG = re.compile(r'<a\s+class=["\']tg-link["\']>([^<]*)</a>', re.IGNORECASE)
|
_INVITE_LINK_TAG = re.compile(r'<a\s+class=["\']tg-link["\']>(.*?)</a>', re.IGNORECASE | re.DOTALL)
|
||||||
_INVITE_LINK_PLACEHOLDER = '{{invite_link}}'
|
_INVITE_LINK_PLACEHOLDER = '{{invite_link}}'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -615,6 +615,7 @@ type PlacementOutput struct {
|
|||||||
ShortID string `json:"short_id"`
|
ShortID string `json:"short_id"`
|
||||||
Details *PlacementDetails `json:"details,omitempty"`
|
Details *PlacementDetails `json:"details,omitempty"`
|
||||||
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
|
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlacementPostOutput struct {
|
type PlacementPostOutput struct {
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ type exec struct {
|
|||||||
|
|
||||||
var botUsername string
|
var botUsername string
|
||||||
|
|
||||||
func SetBotUsername(username string) { botUsername = username }
|
func SetUsername(username string) { botUsername = username }
|
||||||
|
|
||||||
func BotUsername() string { return botUsername }
|
func Username() string { return botUsername }
|
||||||
|
|
||||||
type lastRender struct {
|
type lastRender struct {
|
||||||
textHash string
|
textHash string
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func main() {
|
|||||||
if me, err := api.GetMe(); err != nil {
|
if me, err := api.GetMe(); err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get bot profile")
|
log.Error().Err(err).Msg("Failed to get bot profile")
|
||||||
} else if me.Result != nil {
|
} else if me.Result != nil {
|
||||||
bot.SetBotUsername(me.Result.Username)
|
bot.SetUsername(me.Result.Username)
|
||||||
log.Info().Str("username", me.Result.Username).Msg("Telegram bot authorized")
|
log.Info().Str("username", me.Result.Username).Msg("Telegram bot authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,47 +2,40 @@ package screens
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/NicoNex/echotron/v3"
|
"github.com/NicoNex/echotron/v3"
|
||||||
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
"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 {
|
type AddProject struct {
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addProjectInstructionText = `
|
||||||
|
<b>Как добавить канал в проект</b>
|
||||||
|
|
||||||
|
Для подключения канала к боту, назначьте @%s администратором канала, выдав следующие права:
|
||||||
|
|
||||||
|
➔ Управление сообщениями
|
||||||
|
➔ Добавление участников
|
||||||
|
|
||||||
|
После добавления бота:
|
||||||
|
• Если у вас <b>1 рабочее пространство</b> — канал добавится автоматически
|
||||||
|
• Если у вас <b>несколько рабочих пространств</b> — вы получите уведомление с кнопкой выбора
|
||||||
|
`
|
||||||
|
|
||||||
func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
|
func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
botUsername := bot.BotUsername()
|
botUsername := strings.TrimPrefix(bot.Username(), "@")
|
||||||
if botUsername == "" {
|
|
||||||
botUsername = "chat_cruiser_bot"
|
|
||||||
}
|
|
||||||
addBotURL := fmt.Sprintf("https://t.me/%s?startchannel&admin=invite_users+post_messages+edit_messages+delete_messages", botUsername)
|
addBotURL := fmt.Sprintf("https://t.me/%s?startchannel&admin=invite_users+post_messages+edit_messages+delete_messages", botUsername)
|
||||||
|
|
||||||
buttons := [][]echotron.InlineKeyboardButton{
|
buttons := [][]echotron.InlineKeyboardButton{
|
||||||
Row(URLButton("Добавить", addBotURL)),
|
Row(URLButton("Назначить администратором", addBotURL)),
|
||||||
Row(Button("← Назад", "back")),
|
Row(Button("← Назад", "back")),
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboard := Keyboard(buttons...)
|
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) {
|
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() {}
|
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 inviteLinkPattern = regexp.MustCompile(`https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+`)
|
||||||
var inviteLinkTagPattern = regexp.MustCompile(`<a\s+class="tg-link">\s*(.*?)\s*</a>`)
|
var inviteLinkTagPattern = regexp.MustCompile(`<a\s+class="tg-link">\s*(.*?)\s*</a>`)
|
||||||
var htmlTagPattern = regexp.MustCompile(`<[^>]+>`)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
inputModeText = "text"
|
inputModeText = "text"
|
||||||
@@ -193,7 +192,7 @@ func (e *CreativeEditorFields) GetCreativeText() string {
|
|||||||
sub := inviteLinkTagPattern.FindStringSubmatch(match)
|
sub := inviteLinkTagPattern.FindStringSubmatch(match)
|
||||||
inner := ""
|
inner := ""
|
||||||
if len(sub) > 1 {
|
if len(sub) > 1 {
|
||||||
inner = strings.TrimSpace(htmlTagPattern.ReplaceAllString(sub[1], ""))
|
inner = strings.TrimSpace(sub[1])
|
||||||
}
|
}
|
||||||
if inner == "" {
|
if inner == "" {
|
||||||
inner = inviteLinkPreviewURL
|
inner = inviteLinkPreviewURL
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/NicoNex/echotron/v3"
|
"github.com/NicoNex/echotron/v3"
|
||||||
"github.com/TelegramExchange/tgex-backend/tg_bot/backend"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/backend"
|
||||||
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
||||||
|
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlacementDetails struct {
|
type PlacementDetails struct {
|
||||||
@@ -247,16 +248,16 @@ func formatDateTime(value string) string {
|
|||||||
|
|
||||||
// Try parsing with timezone
|
// Try parsing with timezone
|
||||||
if parsed, err = time.Parse(time.RFC3339, value); err == nil {
|
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 {
|
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 {
|
if parsed, err = time.ParseInLocation("2006-01-02T15:04:05", value, ui.MskLocation); err == nil {
|
||||||
return formatCompactDateTime(parsed)
|
return formatCompactDateTime(parsed.In(ui.MskLocation))
|
||||||
}
|
}
|
||||||
if parsed, err = time.Parse("2006-01-02", value); err == nil {
|
if parsed, err = time.ParseInLocation("2006-01-02", value, ui.MskLocation); err == nil {
|
||||||
return formatCompactDateTime(parsed)
|
return formatCompactDateTime(parsed.In(ui.MskLocation))
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,8 @@ func (s *PlacementsList) render(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
// Формируем кнопки для каждого размещения
|
// Формируем кнопки для каждого размещения
|
||||||
buttons := make([]echotron.InlineKeyboardButton, 0, len(page.Items))
|
buttons := make([]echotron.InlineKeyboardButton, 0, len(page.Items))
|
||||||
for i, placement := range page.Items {
|
for i, placement := range page.Items {
|
||||||
// Обратный глобальный номер (последнее = #1, первое = #N)
|
// Нумерация по порядку создания: первое размещение = #1 (на последней странице),
|
||||||
|
// последнее размещение = #N (вверху).
|
||||||
globalNum := page.Total - (s.CurrentPage * placementsPerPage) - i
|
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)
|
dateStr = formatDate(*placement.Details.PlacementAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Текст кнопки: #N • Канал (с пробелом и разделителем)
|
// Текст кнопки: #N • Канал • DD.MM
|
||||||
buttonText := fmt.Sprintf("%d • %s", globalNum, channelName)
|
buttonText := fmt.Sprintf("#%d • %s", globalNum, channelName)
|
||||||
if dateStr != "" {
|
if dateStr != "" {
|
||||||
buttonText += fmt.Sprintf(" %s", dateStr)
|
buttonText += fmt.Sprintf(" • %s", dateStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons = append(buttons, echotron.InlineKeyboardButton{
|
buttons = append(buttons, echotron.InlineKeyboardButton{
|
||||||
@@ -225,22 +226,21 @@ func (s *PlacementsList) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
|||||||
|
|
||||||
func (s *PlacementsList) Exit() {}
|
func (s *PlacementsList) Exit() {}
|
||||||
|
|
||||||
// formatDate парсит ISO дату и форматирует как "27 янв"
|
// formatDate парсит ISO дату и форматирует как "27.01"
|
||||||
func formatDate(isoDate string) string {
|
func formatDate(isoDate string) string {
|
||||||
// Пробуем парсить RFC3339
|
// Пробуем парсить RFC3339
|
||||||
t, err := time.Parse(time.RFC3339, isoDate)
|
t, err := time.Parse(time.RFC3339, isoDate)
|
||||||
if err != nil {
|
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 {
|
||||||
|
// Иногда бэкенд может вернуть только дату без времени
|
||||||
|
t, err = time.ParseInLocation("2006-01-02", isoDate, ui.MskLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Русские названия месяцев
|
return t.In(ui.MskLocation).Format("02.01")
|
||||||
months := []string{"янв", "фев", "мар", "апр", "мая", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"}
|
|
||||||
day := t.Day()
|
|
||||||
month := months[t.Month()-1]
|
|
||||||
|
|
||||||
return fmt.Sprintf("%d %s", day, month)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
|
|||||||
Title: "Дата оплаты",
|
Title: "Дата оплаты",
|
||||||
Key: "payment_date",
|
Key: "payment_date",
|
||||||
IncludeTime: false,
|
IncludeTime: false,
|
||||||
|
AllowPast: true,
|
||||||
Selected: s.PaymentDate,
|
Selected: s.PaymentDate,
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}), bot.EditMessage)
|
}), bot.EditMessage)
|
||||||
@@ -508,7 +509,11 @@ func (s *PurchaseOptionalDetails) costTypeLabel() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *PurchaseOptionalDetails) costBeforeTypeLabel() 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 "СРМ"
|
||||||
}
|
}
|
||||||
return "Фикс"
|
return "Фикс"
|
||||||
@@ -1144,6 +1149,7 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
|
|||||||
Title: "Дата и время размещения",
|
Title: "Дата и время размещения",
|
||||||
Key: "placement_datetime",
|
Key: "placement_datetime",
|
||||||
IncludeTime: true,
|
IncludeTime: true,
|
||||||
|
AllowPast: true,
|
||||||
Selected: s.PlacementDateTime,
|
Selected: s.PlacementDateTime,
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}), bot.EditMessage)
|
}), bot.EditMessage)
|
||||||
@@ -1154,6 +1160,7 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
|
|||||||
Title: "Дата оплаты",
|
Title: "Дата оплаты",
|
||||||
Key: "payment_date",
|
Key: "payment_date",
|
||||||
IncludeTime: false,
|
IncludeTime: false,
|
||||||
|
AllowPast: true,
|
||||||
Selected: s.PaymentDate,
|
Selected: s.PaymentDate,
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}), bot.EditMessage)
|
}), bot.EditMessage)
|
||||||
@@ -1201,6 +1208,7 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
|
|||||||
Title: "Дата и время размещения",
|
Title: "Дата и время размещения",
|
||||||
Key: "placement_datetime:" + channelKey,
|
Key: "placement_datetime:" + channelKey,
|
||||||
IncludeTime: true,
|
IncludeTime: true,
|
||||||
|
AllowPast: true,
|
||||||
Selected: s.PlacementByChannel[channelKey],
|
Selected: s.PlacementByChannel[channelKey],
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}), bot.EditMessage)
|
}), bot.EditMessage)
|
||||||
@@ -1210,6 +1218,7 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
|
|||||||
Title: "Дата оплаты",
|
Title: "Дата оплаты",
|
||||||
Key: "payment_date:" + channelKey,
|
Key: "payment_date:" + channelKey,
|
||||||
IncludeTime: false,
|
IncludeTime: false,
|
||||||
|
AllowPast: true,
|
||||||
Selected: s.PaymentDateByChannel[channelKey],
|
Selected: s.PaymentDateByChannel[channelKey],
|
||||||
BackState: s,
|
BackState: s,
|
||||||
}), bot.EditMessage)
|
}), bot.EditMessage)
|
||||||
@@ -2093,13 +2102,13 @@ func (s *PurchaseOptionalDetails) toggleCostType() {
|
|||||||
|
|
||||||
func (s *PurchaseOptionalDetails) toggleCostBeforeType() {
|
func (s *PurchaseOptionalDetails) toggleCostBeforeType() {
|
||||||
if s.CurrentChannel == "" {
|
if s.CurrentChannel == "" {
|
||||||
if s.CostBeforeBargain == nil {
|
|
||||||
s.CostBeforeBargain = &CostEntry{}
|
|
||||||
}
|
|
||||||
if s.costBeforeTypeLabel() == "СРМ" {
|
if s.costBeforeTypeLabel() == "СРМ" {
|
||||||
s.CostBeforeBargain.Type = "fixed"
|
s.CostBeforeType = "fixed"
|
||||||
} else {
|
} else {
|
||||||
s.CostBeforeBargain.Type = "cpm"
|
s.CostBeforeType = "cpm"
|
||||||
|
}
|
||||||
|
if s.CostBeforeBargain != nil {
|
||||||
|
s.CostBeforeBargain.Type = s.CostBeforeType
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type DateTimePickerConfig struct {
|
|||||||
Title string
|
Title string
|
||||||
Key string
|
Key string
|
||||||
IncludeTime bool
|
IncludeTime bool
|
||||||
|
AllowPast bool
|
||||||
Selected *time.Time
|
Selected *time.Time
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,7 @@ type DateTimePicker struct {
|
|||||||
Title string
|
Title string
|
||||||
Key string
|
Key string
|
||||||
IncludeTime bool
|
IncludeTime bool
|
||||||
|
AllowPast bool
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
|
|
||||||
view string
|
view string
|
||||||
@@ -63,6 +65,7 @@ func NewDateTimePicker(cfg DateTimePickerConfig) *DateTimePicker {
|
|||||||
Title: cfg.Title,
|
Title: cfg.Title,
|
||||||
Key: cfg.Key,
|
Key: cfg.Key,
|
||||||
IncludeTime: cfg.IncludeTime,
|
IncludeTime: cfg.IncludeTime,
|
||||||
|
AllowPast: cfg.AllowPast,
|
||||||
BackState: cfg.BackState,
|
BackState: cfg.BackState,
|
||||||
view: pickerViewCalendar,
|
view: pickerViewCalendar,
|
||||||
year: year,
|
year: year,
|
||||||
@@ -208,7 +211,7 @@ func (s *DateTimePicker) calendarKeyboard() echotron.InlineKeyboardMarkup {
|
|||||||
|
|
||||||
for day := 1; day <= daysInMonth; day++ {
|
for day := 1; day <= daysInMonth; day++ {
|
||||||
date := time.Date(s.year, s.month, day, 0, 0, 0, 0, MskLocation)
|
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"})
|
row = append(row, echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"})
|
||||||
} else {
|
} else {
|
||||||
label := fmt.Sprintf("%d", day)
|
label := fmt.Sprintf("%d", day)
|
||||||
@@ -462,6 +465,9 @@ func (s *DateTimePicker) minuteButton(minute int) echotron.InlineKeyboardButton
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DateTimePicker) isHourDisabled(hour int) bool {
|
func (s *DateTimePicker) isHourDisabled(hour int) bool {
|
||||||
|
if s.AllowPast {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if s.selectedDate == nil {
|
if s.selectedDate == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -475,6 +481,9 @@ func (s *DateTimePicker) isHourDisabled(hour int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DateTimePicker) isMinuteDisabled(minute int) bool {
|
func (s *DateTimePicker) isMinuteDisabled(minute int) bool {
|
||||||
|
if s.AllowPast {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if s.selectedDate == nil || s.selectedHour == nil {
|
if s.selectedDate == nil || s.selectedHour == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -491,6 +500,16 @@ func (s *DateTimePicker) isMinuteDisabled(minute int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DateTimePicker) prevMonth() {
|
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)
|
now := time.Now().In(MskLocation)
|
||||||
if s.year == now.Year() && s.month == now.Month() {
|
if s.year == now.Year() && s.month == now.Month() {
|
||||||
return
|
return
|
||||||
@@ -513,6 +532,10 @@ func (s *DateTimePicker) nextMonth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DateTimePicker) monthPrevButton() echotron.InlineKeyboardButton {
|
func (s *DateTimePicker) monthPrevButton() echotron.InlineKeyboardButton {
|
||||||
|
if s.AllowPast {
|
||||||
|
return echotron.InlineKeyboardButton{Text: "←", CallbackData: dtpCallbackPrefix + "month_prev"}
|
||||||
|
}
|
||||||
|
|
||||||
now := time.Now().In(MskLocation)
|
now := time.Now().In(MskLocation)
|
||||||
if s.year == now.Year() && s.month == now.Month() {
|
if s.year == now.Year() && s.month == now.Month() {
|
||||||
return echotron.InlineKeyboardButton{Text: "-", CallbackData: "empty"}
|
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)
|
input := strings.TrimSpace(u.Message.Text)
|
||||||
switch s.view {
|
switch s.view {
|
||||||
case pickerViewCalendar:
|
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 {
|
if !hasDate {
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
return
|
return
|
||||||
@@ -573,7 +596,7 @@ func (s *DateTimePicker) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
case pickerViewTimeCombined:
|
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 {
|
if !hasTime {
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
return
|
return
|
||||||
@@ -587,7 +610,7 @@ func (s *DateTimePicker) HandleMessage(b *bot.Bot, u *echotron.Update) {
|
|||||||
s.selectedMinute = &minute
|
s.selectedMinute = &minute
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
default:
|
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 {
|
if !hasDate && !hasTime {
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
return
|
return
|
||||||
@@ -611,6 +634,7 @@ func parseDateTimeInput(
|
|||||||
currentDate *time.Time,
|
currentDate *time.Time,
|
||||||
currentHour *int,
|
currentHour *int,
|
||||||
currentMinute *int,
|
currentMinute *int,
|
||||||
|
allowPast bool,
|
||||||
) (time.Time, bool, bool) {
|
) (time.Time, bool, bool) {
|
||||||
text := strings.ToLower(strings.TrimSpace(input))
|
text := strings.ToLower(strings.TrimSpace(input))
|
||||||
text = strings.ReplaceAll(text, ",", " ")
|
text = strings.ReplaceAll(text, ",", " ")
|
||||||
@@ -637,7 +661,7 @@ func parseDateTimeInput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
value := time.Date(date.Year(), date.Month(), date.Day(), hour, minute, 0, 0, MskLocation)
|
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 time.Time{}, false, false
|
||||||
}
|
}
|
||||||
return value, dateOk, timeOk
|
return value, dateOk, timeOk
|
||||||
|
|||||||
Reference in New Issue
Block a user