fix: ввод даты, ввод формата
This commit is contained in:
@@ -596,6 +596,8 @@ type PlacementDetails struct {
|
||||
CostBeforeBargain *CostInfo `json:"cost_before_bargain,omitempty"`
|
||||
PlacementType *string `json:"placement_type,omitempty"`
|
||||
Format *string `json:"format,omitempty"`
|
||||
TopTimeMinutes *int `json:"top_time_minutes,omitempty"`
|
||||
FeedTimeMinutes *int `json:"feed_time_minutes,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
CreativeID *string `json:"creative_id,omitempty"`
|
||||
CreativeName *string `json:"creative_name,omitempty"`
|
||||
|
||||
@@ -9,8 +9,11 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/AlekSi/pointer v1.0.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/olebedev/when v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE=
|
||||
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
|
||||
github.com/NicoNex/echotron/v3 v3.43.0 h1:efE2spw3mfU0Ev20m0PqqvgMSm0xHzgSxlWAEmC9RC4=
|
||||
github.com/NicoNex/echotron/v3 v3.43.0/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
@@ -9,6 +11,9 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/olebedev/when v1.1.0 h1:dlpoRa7huImhNtEx4yl0WYfTHVEWmJmIWd7fEkTHayc=
|
||||
github.com/olebedev/when v1.1.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
|
||||
@@ -29,6 +29,13 @@ type PurchaseOptionalDetails struct {
|
||||
CostBeforeBargain *CostEntry
|
||||
PurchaseType string
|
||||
Format string
|
||||
TopTimeMinutes *int
|
||||
FeedTimeMinutes *int // nil = не указан, 0 = без удаления
|
||||
TopTimeByChannel map[string]*int
|
||||
FeedTimeByChannel map[string]*int
|
||||
CustomFormatTopUnit string // "hours" | "minutes"
|
||||
CustomFormatFeedUnit string // "hours" | "days"
|
||||
CustomFormatTopValue *int // промежуточное значение (в минутах)
|
||||
Comment string
|
||||
InviteLinkType string
|
||||
InputMode string
|
||||
@@ -209,6 +216,9 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
|
||||
|
||||
case "format_custom":
|
||||
s.InputMode = "format_custom"
|
||||
s.CustomFormatTopUnit = "hours"
|
||||
s.CustomFormatFeedUnit = "hours"
|
||||
s.CustomFormatTopValue = nil
|
||||
s.Enter(b, bot.EditMessage)
|
||||
|
||||
case "cost_type_toggle":
|
||||
@@ -328,6 +338,90 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(u.CallbackQuery.Data, "format_preset:") {
|
||||
parts := strings.Split(strings.TrimPrefix(u.CallbackQuery.Data, "format_preset:"), ":")
|
||||
if len(parts) == 2 {
|
||||
topMin, err1 := strconv.Atoi(parts[0])
|
||||
feedMin, err2 := strconv.Atoi(parts[1])
|
||||
if err1 == nil && err2 == nil {
|
||||
s.setFormatPreset(topMin, feedMin)
|
||||
}
|
||||
}
|
||||
if s.ReturnMode != "" {
|
||||
s.InputMode = s.ReturnMode
|
||||
s.ReturnMode = ""
|
||||
} else {
|
||||
s.InputMode = ""
|
||||
}
|
||||
s.CurrentChannel = ""
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(u.CallbackQuery.Data, "format_custom_top:") {
|
||||
valStr := strings.TrimPrefix(u.CallbackQuery.Data, "format_custom_top:")
|
||||
minutes, err := strconv.Atoi(valStr)
|
||||
if err == nil && minutes > 0 {
|
||||
s.CustomFormatTopValue = &minutes
|
||||
s.InputMode = "format_custom_feed"
|
||||
}
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(u.CallbackQuery.Data, "format_custom_feed:") {
|
||||
valStr := strings.TrimPrefix(u.CallbackQuery.Data, "format_custom_feed:")
|
||||
feedMinutes, err := strconv.Atoi(valStr)
|
||||
if err == nil && s.CustomFormatTopValue != nil {
|
||||
s.setFormatPreset(*s.CustomFormatTopValue, feedMinutes)
|
||||
s.CustomFormatTopValue = nil
|
||||
if s.ReturnMode != "" {
|
||||
s.InputMode = s.ReturnMode
|
||||
s.ReturnMode = ""
|
||||
} else {
|
||||
s.InputMode = ""
|
||||
}
|
||||
s.CurrentChannel = ""
|
||||
}
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if u.CallbackQuery.Data == "format_custom_top_toggle_unit" {
|
||||
if s.CustomFormatTopUnit == "hours" {
|
||||
s.CustomFormatTopUnit = "minutes"
|
||||
} else {
|
||||
s.CustomFormatTopUnit = "hours"
|
||||
}
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if u.CallbackQuery.Data == "format_custom_feed_toggle_unit" {
|
||||
if s.CustomFormatFeedUnit == "hours" {
|
||||
s.CustomFormatFeedUnit = "days"
|
||||
} else {
|
||||
s.CustomFormatFeedUnit = "hours"
|
||||
}
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if u.CallbackQuery.Data == "format_custom_top_input_mode" {
|
||||
s.InputMode = "format_custom_top_input"
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if u.CallbackQuery.Data == "format_custom_feed_input_mode" {
|
||||
s.InputMode = "format_custom_feed_input"
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if u.CallbackQuery.Data == "format_custom_back_to_select" {
|
||||
s.InputMode = "format_select"
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if u.CallbackQuery.Data == "format_custom_back_to_top" {
|
||||
s.InputMode = "format_custom"
|
||||
s.Enter(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(u.CallbackQuery.Data, "format:") {
|
||||
value := strings.TrimPrefix(u.CallbackQuery.Data, "format:")
|
||||
s.setFormatValue(value)
|
||||
@@ -370,8 +464,36 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update)
|
||||
}
|
||||
|
||||
switch s.InputMode {
|
||||
case "format_custom":
|
||||
s.setFormatValue(text)
|
||||
case "format_custom_top_input":
|
||||
value, err := strconv.Atoi(text)
|
||||
if err != nil || value <= 0 {
|
||||
s.renderInputPrompt(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
// Конвертируем в минуты по текущей единице
|
||||
if s.CustomFormatTopUnit == "hours" {
|
||||
value = value * 60
|
||||
}
|
||||
s.CustomFormatTopValue = &value
|
||||
s.InputMode = "format_custom_feed"
|
||||
s.Enter(b, bot.NewMessage)
|
||||
return
|
||||
case "format_custom_feed_input":
|
||||
value, err := strconv.Atoi(text)
|
||||
if err != nil || value <= 0 {
|
||||
s.renderInputPrompt(b, bot.EditMessage)
|
||||
return
|
||||
}
|
||||
// Конвертируем в минуты по текущей единице
|
||||
if s.CustomFormatFeedUnit == "days" {
|
||||
value = value * 24 * 60
|
||||
} else {
|
||||
value = value * 60
|
||||
}
|
||||
if s.CustomFormatTopValue != nil {
|
||||
s.setFormatPreset(*s.CustomFormatTopValue, value)
|
||||
s.CustomFormatTopValue = nil
|
||||
}
|
||||
case "comment":
|
||||
if s.CurrentChannel != "" {
|
||||
s.CommentByChannel[s.CurrentChannel] = text
|
||||
@@ -699,22 +821,34 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
|
||||
text += "Выберите формат\n\n"
|
||||
keyboard := Keyboard(
|
||||
Row(
|
||||
Button("1 / 24", "format:1 / 24"),
|
||||
Button("1/48", "format:1/48"),
|
||||
Button("1 / 72", "format:1 / 72"),
|
||||
Button("1ч / 24ч", "format_preset:60:1440"),
|
||||
Button("1ч / 36ч", "format_preset:60:2160"),
|
||||
Button("1ч / 48ч", "format_preset:60:2880"),
|
||||
Button("1ч / 72ч", "format_preset:60:4320"),
|
||||
),
|
||||
Row(
|
||||
Button("1 / (7 дней)", "format:1 / (7 дней)"),
|
||||
Button("1 / (30 дней)", "format:1 / (30 дней)"),
|
||||
Button("1 / (без удаления)", "format:1 / (без удаления)"),
|
||||
Button("1ч / 7д", "format_preset:60:10080"),
|
||||
Button("1ч / 30д", "format_preset:60:43200"),
|
||||
Button("1ч / 60д", "format_preset:60:86400"),
|
||||
Button("1ч / 90д", "format_preset:60:129600"),
|
||||
),
|
||||
Row(
|
||||
Button("2 / 24", "format:2 / 24"),
|
||||
Button("2/48", "format:2/48"),
|
||||
Button("2/72", "format:2/72"),
|
||||
Button("1ч / без удаления", "format_preset:60:0"),
|
||||
),
|
||||
Row(
|
||||
Button("Назад", s.backAction()),
|
||||
Button("2ч / 24ч", "format_preset:120:1440"),
|
||||
Button("2ч / 36ч", "format_preset:120:2160"),
|
||||
Button("2ч / 48ч", "format_preset:120:2880"),
|
||||
Button("2ч / 72ч", "format_preset:120:4320"),
|
||||
),
|
||||
Row(
|
||||
Button("2ч / 7д", "format_preset:120:10080"),
|
||||
Button("2ч / 30д", "format_preset:120:43200"),
|
||||
Button("2ч / 60д", "format_preset:120:86400"),
|
||||
Button("2ч / 90д", "format_preset:120:129600"),
|
||||
),
|
||||
Row(
|
||||
Button("← Назад", s.backAction()),
|
||||
Button("Свой формат", "format_custom"),
|
||||
),
|
||||
)
|
||||
@@ -725,7 +859,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
|
||||
}
|
||||
return
|
||||
case "format_custom":
|
||||
text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>"
|
||||
s.renderCustomFormatTop(b, mode)
|
||||
return
|
||||
case "format_custom_top_input":
|
||||
text += "<b>Введите число для времени в топе</b>\n\n"
|
||||
unit := "часах"
|
||||
if s.CustomFormatTopUnit == "minutes" {
|
||||
unit = "минутах"
|
||||
}
|
||||
text += fmt.Sprintf("Единица: <b>%s</b>", unit)
|
||||
case "format_custom_feed":
|
||||
s.renderCustomFormatFeed(b, mode)
|
||||
return
|
||||
case "format_custom_feed_input":
|
||||
text += "<b>Введите число для времени в ленте</b>\n\n"
|
||||
unit := "часах"
|
||||
if s.CustomFormatFeedUnit == "days" {
|
||||
unit = "днях"
|
||||
}
|
||||
text += fmt.Sprintf("Единица: <b>%s</b>", unit)
|
||||
case "invite_link_type":
|
||||
text = "<i>Страница выбора типа ссылки</i>\n\n"
|
||||
if s.CurrentChannel != "" {
|
||||
@@ -777,6 +929,127 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) renderCustomFormatTop(b *bot.Bot, mode bot.RenderMode) {
|
||||
text := "<b>Свой формат — время в топе</b>\n\n"
|
||||
if s.CurrentChannel != "" {
|
||||
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
|
||||
}
|
||||
|
||||
var rows [][]echotron.InlineKeyboardButton
|
||||
|
||||
if s.CustomFormatTopUnit == "minutes" {
|
||||
text += "⏱ Единица: <b>минуты</b>\n\n"
|
||||
text += "Выберите время в топе"
|
||||
rows = append(rows,
|
||||
Row(
|
||||
Button("10", "format_custom_top:10"),
|
||||
Button("15", "format_custom_top:15"),
|
||||
Button("20", "format_custom_top:20"),
|
||||
Button("30", "format_custom_top:30"),
|
||||
),
|
||||
Row(
|
||||
Button("45", "format_custom_top:45"),
|
||||
Button("60", "format_custom_top:60"),
|
||||
Button("90", "format_custom_top:90"),
|
||||
Button("120", "format_custom_top:120"),
|
||||
),
|
||||
Row(
|
||||
Button("↔ Часы", "format_custom_top_toggle_unit"),
|
||||
Button("Ввести число", "format_custom_top_input_mode"),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
text += "⏱ Единица: <b>часы</b>\n\n"
|
||||
text += "Выберите время в топе"
|
||||
rows = append(rows,
|
||||
Row(
|
||||
Button("1", "format_custom_top:60"),
|
||||
Button("2", "format_custom_top:120"),
|
||||
Button("3", "format_custom_top:180"),
|
||||
Button("4", "format_custom_top:240"),
|
||||
),
|
||||
Row(
|
||||
Button("5", "format_custom_top:300"),
|
||||
Button("6", "format_custom_top:360"),
|
||||
Button("8", "format_custom_top:480"),
|
||||
Button("12", "format_custom_top:720"),
|
||||
),
|
||||
Row(
|
||||
Button("↔ Минуты", "format_custom_top_toggle_unit"),
|
||||
Button("Ввести число", "format_custom_top_input_mode"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
rows = append(rows, Row(Button("← Назад", "format_custom_back_to_select")))
|
||||
|
||||
b.Render(text, Keyboard(rows...), mode)
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) renderCustomFormatFeed(b *bot.Bot, mode bot.RenderMode) {
|
||||
text := "<b>Свой формат — время в ленте</b>\n\n"
|
||||
if s.CurrentChannel != "" {
|
||||
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
|
||||
}
|
||||
|
||||
if s.CustomFormatTopValue != nil {
|
||||
text += fmt.Sprintf("Время в топе: <b>%s</b> ✓\n\n", formatDuration(*s.CustomFormatTopValue, "top"))
|
||||
}
|
||||
|
||||
var rows [][]echotron.InlineKeyboardButton
|
||||
|
||||
if s.CustomFormatFeedUnit == "days" {
|
||||
text += "⏱ Единица: <b>дни</b>\n\n"
|
||||
text += "Выберите время в ленте"
|
||||
rows = append(rows,
|
||||
Row(
|
||||
Button("7", "format_custom_feed:10080"),
|
||||
Button("14", "format_custom_feed:20160"),
|
||||
Button("30", "format_custom_feed:43200"),
|
||||
Button("60", "format_custom_feed:86400"),
|
||||
),
|
||||
Row(
|
||||
Button("90", "format_custom_feed:129600"),
|
||||
Button("120", "format_custom_feed:172800"),
|
||||
Button("180", "format_custom_feed:259200"),
|
||||
Button("365", "format_custom_feed:525600"),
|
||||
),
|
||||
Row(
|
||||
Button("↔ Часы", "format_custom_feed_toggle_unit"),
|
||||
Button("Без удаления", "format_custom_feed:0"),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
text += "⏱ Единица: <b>часы</b>\n\n"
|
||||
text += "Выберите время в ленте"
|
||||
rows = append(rows,
|
||||
Row(
|
||||
Button("24", "format_custom_feed:1440"),
|
||||
Button("36", "format_custom_feed:2160"),
|
||||
Button("48", "format_custom_feed:2880"),
|
||||
Button("72", "format_custom_feed:4320"),
|
||||
),
|
||||
Row(
|
||||
Button("96", "format_custom_feed:5760"),
|
||||
Button("120", "format_custom_feed:7200"),
|
||||
Button("144", "format_custom_feed:8640"),
|
||||
Button("168", "format_custom_feed:10080"),
|
||||
),
|
||||
Row(
|
||||
Button("↔ Дни", "format_custom_feed_toggle_unit"),
|
||||
Button("Без удаления", "format_custom_feed:0"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
rows = append(rows,
|
||||
Row(Button("Ввести число", "format_custom_feed_input_mode")),
|
||||
Row(Button("← Назад", "format_custom_back_to_top")),
|
||||
)
|
||||
|
||||
b.Render(text, Keyboard(rows...), mode)
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMode) {
|
||||
text := "<b>⚙ Переключить режим параметра</b>\n\n"
|
||||
|
||||
@@ -1382,6 +1655,8 @@ func (s *PurchaseOptionalDetails) copyCommonValueToChannels(param string) {
|
||||
for _, ch := range s.Channels {
|
||||
key := channelKey(ch)
|
||||
s.FormatByChannel[key] = s.Format
|
||||
s.TopTimeByChannel[key] = s.TopTimeMinutes
|
||||
s.FeedTimeByChannel[key] = s.FeedTimeMinutes
|
||||
}
|
||||
case "invite_link_type":
|
||||
if s.InviteLinkType == "" {
|
||||
@@ -1512,6 +1787,8 @@ func (s *PurchaseOptionalDetails) clearParamValue(param, channelKey string) {
|
||||
delete(s.CommentByChannel, channelKey)
|
||||
case "format":
|
||||
delete(s.FormatByChannel, channelKey)
|
||||
delete(s.TopTimeByChannel, channelKey)
|
||||
delete(s.FeedTimeByChannel, channelKey)
|
||||
case "invite_link_type":
|
||||
delete(s.InviteLinkTypeByChannel, channelKey)
|
||||
}
|
||||
@@ -1660,6 +1937,18 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
|
||||
if s.FormatByChannel == nil {
|
||||
s.FormatByChannel = make(map[string]string)
|
||||
}
|
||||
if s.TopTimeByChannel == nil {
|
||||
s.TopTimeByChannel = make(map[string]*int)
|
||||
}
|
||||
if s.FeedTimeByChannel == nil {
|
||||
s.FeedTimeByChannel = make(map[string]*int)
|
||||
}
|
||||
if s.CustomFormatTopUnit == "" {
|
||||
s.CustomFormatTopUnit = "hours"
|
||||
}
|
||||
if s.CustomFormatFeedUnit == "" {
|
||||
s.CustomFormatFeedUnit = "hours"
|
||||
}
|
||||
if s.InviteLinkTypeByChannel == nil {
|
||||
s.InviteLinkTypeByChannel = make(map[string]string)
|
||||
}
|
||||
@@ -1709,6 +1998,16 @@ func (s *PurchaseOptionalDetails) syncChannelMaps() {
|
||||
delete(s.FormatByChannel, key)
|
||||
}
|
||||
}
|
||||
for key := range s.TopTimeByChannel {
|
||||
if _, ok := valid[key]; !ok {
|
||||
delete(s.TopTimeByChannel, key)
|
||||
}
|
||||
}
|
||||
for key := range s.FeedTimeByChannel {
|
||||
if _, ok := valid[key]; !ok {
|
||||
delete(s.FeedTimeByChannel, key)
|
||||
}
|
||||
}
|
||||
for key := range s.InviteLinkTypeByChannel {
|
||||
if _, ok := valid[key]; !ok {
|
||||
delete(s.InviteLinkTypeByChannel, key)
|
||||
@@ -2156,12 +2455,64 @@ func (s *PurchaseOptionalDetails) setCostBeforeValue(value float64) {
|
||||
s.CostBeforeByChannel[s.CurrentChannel] = entry
|
||||
}
|
||||
|
||||
func formatDuration(minutes int, unit string) string {
|
||||
switch unit {
|
||||
case "top":
|
||||
if minutes > 0 && minutes%60 == 0 {
|
||||
return fmt.Sprintf("%dч", minutes/60)
|
||||
}
|
||||
return fmt.Sprintf("%dмин", minutes)
|
||||
case "feed":
|
||||
if minutes == 0 {
|
||||
return "без удаления"
|
||||
}
|
||||
if minutes >= 7*24*60 && minutes%(24*60) == 0 {
|
||||
return fmt.Sprintf("%dд", minutes/(24*60))
|
||||
}
|
||||
if minutes%60 == 0 {
|
||||
return fmt.Sprintf("%dч", minutes/60)
|
||||
}
|
||||
return fmt.Sprintf("%dмин", minutes)
|
||||
}
|
||||
return fmt.Sprintf("%d", minutes)
|
||||
}
|
||||
|
||||
func formatLabel(topMinutes, feedMinutes int) string {
|
||||
return formatDuration(topMinutes, "top") + " / " + formatDuration(feedMinutes, "feed")
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) setFormatPreset(topMinutes int, feedMinutes int) {
|
||||
label := formatLabel(topMinutes, feedMinutes)
|
||||
topPtr := &topMinutes
|
||||
var feedPtr *int
|
||||
if feedMinutes == 0 {
|
||||
// 0 = без удаления — сохраняем как 0
|
||||
feedPtr = &feedMinutes
|
||||
} else {
|
||||
feedPtr = &feedMinutes
|
||||
}
|
||||
|
||||
if s.CurrentChannel == "" {
|
||||
s.Format = label
|
||||
s.TopTimeMinutes = topPtr
|
||||
s.FeedTimeMinutes = feedPtr
|
||||
} else {
|
||||
s.FormatByChannel[s.CurrentChannel] = label
|
||||
s.TopTimeByChannel[s.CurrentChannel] = topPtr
|
||||
s.FeedTimeByChannel[s.CurrentChannel] = feedPtr
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) setFormatValue(value string) {
|
||||
if s.CurrentChannel == "" {
|
||||
s.Format = value
|
||||
s.TopTimeMinutes = nil
|
||||
s.FeedTimeMinutes = nil
|
||||
return
|
||||
}
|
||||
s.FormatByChannel[s.CurrentChannel] = value
|
||||
delete(s.TopTimeByChannel, s.CurrentChannel)
|
||||
delete(s.FeedTimeByChannel, s.CurrentChannel)
|
||||
}
|
||||
|
||||
func (s *PurchaseOptionalDetails) setInviteLinkTypeValue(value string) {
|
||||
@@ -2314,8 +2665,18 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string, placeme
|
||||
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
|
||||
details.Format = &value
|
||||
}
|
||||
} else if s.Format != "" {
|
||||
details.Format = &s.Format
|
||||
if value, ok := s.TopTimeByChannel[channelKey]; ok && value != nil {
|
||||
details.TopTimeMinutes = value
|
||||
}
|
||||
if value, ok := s.FeedTimeByChannel[channelKey]; ok && value != nil {
|
||||
details.FeedTimeMinutes = value
|
||||
}
|
||||
} else {
|
||||
if s.Format != "" {
|
||||
details.Format = &s.Format
|
||||
}
|
||||
details.TopTimeMinutes = s.TopTimeMinutes
|
||||
details.FeedTimeMinutes = s.FeedTimeMinutes
|
||||
}
|
||||
|
||||
// Invite link type
|
||||
@@ -2351,6 +2712,8 @@ func (s *PurchaseOptionalDetails) isChannelDetailsEmpty(details *backend.Placeme
|
||||
details.CostBeforeBargain == nil &&
|
||||
details.PlacementType == nil &&
|
||||
details.Format == nil &&
|
||||
details.TopTimeMinutes == nil &&
|
||||
details.FeedTimeMinutes == nil &&
|
||||
details.Comment == nil &&
|
||||
details.InviteLinkType == nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
||||
"github.com/olebedev/when"
|
||||
"github.com/olebedev/when/rules/common"
|
||||
"github.com/olebedev/when/rules/ru"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -381,6 +384,7 @@ func (s *DateTimePicker) handleDaySelect(payload string, b *bot.Bot) {
|
||||
s.view = pickerViewTimeCombined
|
||||
s.selectedHour = nil
|
||||
s.selectedMinute = nil
|
||||
s.Enter(b, bot.EditMessage)
|
||||
} else {
|
||||
// Сразу подтверждаем выбор для даты без времени
|
||||
s.confirmSelection(b)
|
||||
@@ -629,192 +633,52 @@ func (s *DateTimePicker) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
||||
|
||||
func (s *DateTimePicker) Exit() {}
|
||||
|
||||
var whenParser *when.Parser
|
||||
|
||||
func init() {
|
||||
whenParser = when.New(nil)
|
||||
whenParser.Add(ru.All...)
|
||||
whenParser.Add(common.All...)
|
||||
}
|
||||
|
||||
var (
|
||||
timeIndicator = regexp.MustCompile(`\d{1,2}[:.]\d{2}|утр|вечер|днём|ночь|час|минут|полдень|полночь`)
|
||||
dateIndicator = regexp.MustCompile(`\d{1,2}[./-]\d|сегодня|завтра|вчера|послезавтра|понедельн|вторник|сред[уыа]|четверг|пятниц|суббот|воскресен|янв|фев|мар|апр|ма[йя]|июн|июл|авг|сен|окт|ноя|дек|через.*дн|через.*недел|через.*месяц|через.*год`)
|
||||
)
|
||||
|
||||
func detectTime(matched string) bool {
|
||||
return timeIndicator.MatchString(matched)
|
||||
}
|
||||
|
||||
func detectDate(matched string) bool {
|
||||
return dateIndicator.MatchString(matched)
|
||||
}
|
||||
|
||||
func parseDateTimeInput(
|
||||
input string,
|
||||
currentDate *time.Time,
|
||||
currentHour *int,
|
||||
currentMinute *int,
|
||||
_ *time.Time,
|
||||
_ *int,
|
||||
_ *int,
|
||||
allowPast bool,
|
||||
) (time.Time, bool, bool) {
|
||||
text := strings.ToLower(strings.TrimSpace(input))
|
||||
text = strings.ReplaceAll(text, ",", " ")
|
||||
text = strings.ReplaceAll(text, " ", " ")
|
||||
|
||||
now := time.Now().In(MskLocation)
|
||||
baseDate := now
|
||||
if currentDate != nil {
|
||||
baseDate = currentDate.In(MskLocation)
|
||||
}
|
||||
|
||||
date, dateOk := parseDate(text, baseDate)
|
||||
hour, minute, timeOk := parseTime(text, currentHour, currentMinute)
|
||||
|
||||
if !dateOk && !timeOk {
|
||||
r, err := whenParser.Parse(input, now)
|
||||
if err != nil || r == nil {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
if !dateOk {
|
||||
date = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), 0, 0, 0, 0, MskLocation)
|
||||
}
|
||||
if !timeOk {
|
||||
hour = 0
|
||||
minute = 0
|
||||
}
|
||||
|
||||
value := time.Date(date.Year(), date.Month(), date.Day(), hour, minute, 0, 0, MskLocation)
|
||||
if !allowPast && value.Before(now) {
|
||||
result := r.Time.In(MskLocation)
|
||||
matched := strings.ToLower(r.Text)
|
||||
|
||||
hasDate := detectDate(matched)
|
||||
hasTime := detectTime(matched)
|
||||
|
||||
if !allowPast && result.Before(now) {
|
||||
return time.Time{}, false, false
|
||||
}
|
||||
return value, dateOk, timeOk
|
||||
}
|
||||
|
||||
func parseDate(text string, baseDate time.Time) (time.Time, bool) {
|
||||
if strings.Contains(text, "сегодня") {
|
||||
return baseDate, true
|
||||
}
|
||||
if strings.Contains(text, "завтра") {
|
||||
return baseDate.AddDate(0, 0, 1), true
|
||||
}
|
||||
if strings.Contains(text, "послезавтра") {
|
||||
return baseDate.AddDate(0, 0, 2), true
|
||||
}
|
||||
|
||||
if date := parseNumericDate(text); !date.IsZero() {
|
||||
return date, true
|
||||
}
|
||||
if date := parseWordDate(text); !date.IsZero() {
|
||||
return date, true
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func parseNumericDate(text string) time.Time {
|
||||
reYMD := regexp.MustCompile(`\b(\d{4})[./-](\d{1,2})[./-](\d{1,2})\b`)
|
||||
if match := reYMD.FindStringSubmatch(text); len(match) == 4 {
|
||||
year := parseInt(match[1])
|
||||
month := parseInt(match[2])
|
||||
day := parseInt(match[3])
|
||||
return safeDate(year, month, day)
|
||||
}
|
||||
|
||||
reDMY := regexp.MustCompile(`\b(\d{1,2})[./-](\d{1,2})(?:[./-](\d{2,4}))?\b`)
|
||||
if match := reDMY.FindStringSubmatch(text); len(match) >= 3 {
|
||||
day := parseInt(match[1])
|
||||
month := parseInt(match[2])
|
||||
year := time.Now().In(MskLocation).Year()
|
||||
if len(match) == 4 && match[3] != "" {
|
||||
year = parseInt(match[3])
|
||||
if year < 100 {
|
||||
year += 2000
|
||||
}
|
||||
}
|
||||
return safeDate(year, month, day)
|
||||
}
|
||||
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func parseWordDate(text string) time.Time {
|
||||
monthMap := map[string]time.Month{
|
||||
"янв": 1, "январ": 1, "января": 1,
|
||||
"фев": 2, "феврал": 2, "февраля": 2,
|
||||
"мар": 3, "марта": 3,
|
||||
"апр": 4, "апрел": 4, "апреля": 4,
|
||||
"май": 5, "мая": 5,
|
||||
"июн": 6, "июня": 6,
|
||||
"июл": 7, "июля": 7,
|
||||
"авг": 8, "август": 8, "августа": 8,
|
||||
"сен": 9, "сент": 9, "сентябр": 9, "сентября": 9,
|
||||
"окт": 10, "октябр": 10, "октября": 10,
|
||||
"ноя": 11, "ноябр": 11, "ноября": 11,
|
||||
"дек": 12, "декабр": 12, "декабря": 12,
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`\b(\d{1,2})\s+([a-zа-яё]+)(?:\s+(\d{2,4}))?\b`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if len(match) == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
day := parseInt(match[1])
|
||||
monthToken := match[2]
|
||||
var month time.Month
|
||||
for key, value := range monthMap {
|
||||
if strings.HasPrefix(monthToken, key) {
|
||||
month = value
|
||||
break
|
||||
}
|
||||
}
|
||||
if month == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
year := time.Now().In(MskLocation).Year()
|
||||
if len(match) == 4 && match[3] != "" {
|
||||
year = parseInt(match[3])
|
||||
if year < 100 {
|
||||
year += 2000
|
||||
}
|
||||
}
|
||||
|
||||
return safeDate(year, int(month), day)
|
||||
}
|
||||
|
||||
func parseTime(text string, currentHour *int, currentMinute *int) (int, int, bool) {
|
||||
re := regexp.MustCompile(`\b(\d{1,2})[:.](\d{2})\b`)
|
||||
if match := re.FindStringSubmatch(text); len(match) == 3 {
|
||||
hour := parseInt(match[1])
|
||||
minute := parseInt(match[2])
|
||||
if hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 {
|
||||
return hour, minute, true
|
||||
}
|
||||
}
|
||||
|
||||
reCompact := regexp.MustCompile(`\b(\d{1,2})\s+(\d{2})\b`)
|
||||
if match := reCompact.FindStringSubmatch(text); len(match) == 3 {
|
||||
hour := parseInt(match[1])
|
||||
minute := parseInt(match[2])
|
||||
if hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 {
|
||||
return hour, minute, true
|
||||
}
|
||||
}
|
||||
|
||||
reHour := regexp.MustCompile(`\b(\d{1,2})\s*(?:ч|час|часов)\b`)
|
||||
if match := reHour.FindStringSubmatch(text); len(match) == 2 {
|
||||
hour := parseInt(match[1])
|
||||
if hour >= 0 && hour <= 23 {
|
||||
minute := 0
|
||||
if currentMinute != nil {
|
||||
minute = *currentMinute
|
||||
}
|
||||
return hour, minute, true
|
||||
}
|
||||
}
|
||||
|
||||
reJustHour := regexp.MustCompile(`\b(\d{1,2})\b`)
|
||||
if match := reJustHour.FindStringSubmatch(text); len(match) == 2 && strings.Contains(text, "в") {
|
||||
hour := parseInt(match[1])
|
||||
if hour >= 0 && hour <= 23 {
|
||||
minute := 0
|
||||
if currentMinute != nil {
|
||||
minute = *currentMinute
|
||||
}
|
||||
return hour, minute, true
|
||||
}
|
||||
}
|
||||
|
||||
if currentHour != nil && currentMinute != nil {
|
||||
return *currentHour, *currentMinute, true
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
func safeDate(year, month, day int) time.Time {
|
||||
if month < 1 || month > 12 {
|
||||
return time.Time{}
|
||||
}
|
||||
if day < 1 || day > 31 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, MskLocation)
|
||||
return result, hasDate, hasTime
|
||||
}
|
||||
|
||||
func WeekdayName(day time.Weekday) string {
|
||||
|
||||
Reference in New Issue
Block a user