refactor + fix добавление проекта
This commit is contained in:
@@ -14,15 +14,40 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var emptyKeyboard = echotron.InlineKeyboardMarkup{
|
|
||||||
InlineKeyboard: [][]echotron.InlineKeyboardButton{},
|
|
||||||
}
|
|
||||||
|
|
||||||
type exec struct {
|
type exec struct {
|
||||||
handled bool // Событие обработано, дальше не идём
|
handled bool // Событие обработано, дальше не идём
|
||||||
transitioned bool // Был SetState
|
transitioned bool // Был SetState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var botUsername string
|
||||||
|
var botUsernameOnce sync.Once
|
||||||
|
|
||||||
|
func SetBotUsername(username string) { botUsername = username }
|
||||||
|
|
||||||
|
func BotUsername() string { return botUsername }
|
||||||
|
|
||||||
|
func ensureBotUsername(api echotron.API) {
|
||||||
|
if botUsername != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
botUsernameOnce.Do(func() {
|
||||||
|
me, err := api.GetMe()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("GetMe failed, bot username will be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if me.Result != nil {
|
||||||
|
botUsername = me.Result.Username
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type lastRender struct {
|
||||||
|
textHash string
|
||||||
|
kbHash string
|
||||||
|
isMedia bool
|
||||||
|
}
|
||||||
|
|
||||||
type Bot struct {
|
type Bot struct {
|
||||||
echotron.API
|
echotron.API
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -30,10 +55,8 @@ type Bot struct {
|
|||||||
exec *exec
|
exec *exec
|
||||||
CurrentState State
|
CurrentState State
|
||||||
Session *Session
|
Session *Session
|
||||||
LastMessageID int // ID последнего сообщения с inline кнопками
|
LastMessageID int // ID последнего сообщения с inline кнопками
|
||||||
LastMessageIsMedia bool // True if LastMessageID points to media message
|
lastRender lastRender
|
||||||
LastMessageTextHash string // Hash for last text/caption
|
|
||||||
LastMessageKBHash string // Hash for last inline keyboard
|
|
||||||
commandRouter func(cmd string) State // Роутер для команд (например, /start, /help)
|
commandRouter func(cmd string) State // Роутер для команд (например, /start, /help)
|
||||||
globalCallbackRouter func(callbackData string) State // Роутер для глобальных callbacks из уведомлений
|
globalCallbackRouter func(callbackData string) State // Роутер для глобальных callbacks из уведомлений
|
||||||
Backend *backend.Client
|
Backend *backend.Client
|
||||||
@@ -53,9 +76,12 @@ func NewBot(chatID int64, token string, commandRouter func(string) State, backen
|
|||||||
panic("bot: backendClient cannot be nil")
|
panic("bot: backendClient cannot be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api := echotron.NewAPI(token)
|
||||||
|
ensureBotUsername(api)
|
||||||
|
|
||||||
return &Bot{
|
return &Bot{
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
API: echotron.NewAPI(token),
|
API: api,
|
||||||
Session: &Session{},
|
Session: &Session{},
|
||||||
commandRouter: commandRouter,
|
commandRouter: commandRouter,
|
||||||
Backend: backendClient,
|
Backend: backendClient,
|
||||||
@@ -140,7 +166,7 @@ func (b *Bot) Render(text string, keyboard echotron.InlineKeyboardMarkup, mode R
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) SetLastMessageIsMedia(isMedia bool) {
|
func (b *Bot) SetLastMessageIsMedia(isMedia bool) {
|
||||||
b.LastMessageIsMedia = isMedia
|
b.lastRender.isMedia = isMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) DownloadFileBytes(fileID string) ([]byte, error) {
|
func (b *Bot) DownloadFileBytes(fileID string) ([]byte, error) {
|
||||||
@@ -175,9 +201,9 @@ func (b *Bot) SendNew(text string, keyboard echotron.InlineKeyboardMarkup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.LastMessageID = res.Result.ID
|
b.LastMessageID = res.Result.ID
|
||||||
b.LastMessageIsMedia = false
|
b.lastRender.isMedia = false
|
||||||
b.LastMessageTextHash = hashString(text)
|
b.lastRender.textHash = hashString(text)
|
||||||
b.LastMessageKBHash = keyboardHash(keyboard)
|
b.lastRender.kbHash = keyboardHash(keyboard)
|
||||||
log.Info().Int("new_last_msg_id", b.LastMessageID).Msg("Updated LastMessageID in SendNew")
|
log.Info().Int("new_last_msg_id", b.LastMessageID).Msg("Updated LastMessageID in SendNew")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,8 +220,8 @@ func (b *Bot) Edit(text string, keyboard echotron.InlineKeyboardMarkup) {
|
|||||||
newTextHash := hashString(text)
|
newTextHash := hashString(text)
|
||||||
newKBHash := keyboardHash(keyboard)
|
newKBHash := keyboardHash(keyboard)
|
||||||
|
|
||||||
textChanged := newTextHash != b.LastMessageTextHash
|
textChanged := newTextHash != b.lastRender.textHash
|
||||||
kbChanged := newKBHash != b.LastMessageKBHash
|
kbChanged := newKBHash != b.lastRender.kbHash
|
||||||
|
|
||||||
if !textChanged && !kbChanged {
|
if !textChanged && !kbChanged {
|
||||||
log.Info().Msg("Edit skipped: message not modified")
|
log.Info().Msg("Edit skipped: message not modified")
|
||||||
@@ -205,7 +231,7 @@ func (b *Bot) Edit(text string, keyboard echotron.InlineKeyboardMarkup) {
|
|||||||
var err error
|
var err error
|
||||||
msgID := echotron.NewMessageID(b.ChatID, b.LastMessageID)
|
msgID := echotron.NewMessageID(b.ChatID, b.LastMessageID)
|
||||||
|
|
||||||
if b.LastMessageIsMedia {
|
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 {
|
} 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})
|
||||||
@@ -213,10 +239,10 @@ func (b *Bot) Edit(text string, keyboard echotron.InlineKeyboardMarkup) {
|
|||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if textChanged {
|
if textChanged {
|
||||||
b.LastMessageTextHash = newTextHash
|
b.lastRender.textHash = newTextHash
|
||||||
}
|
}
|
||||||
if kbChanged {
|
if kbChanged {
|
||||||
b.LastMessageKBHash = newKBHash
|
b.lastRender.kbHash = newKBHash
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -225,7 +251,7 @@ func (b *Bot) Edit(text string, keyboard echotron.InlineKeyboardMarkup) {
|
|||||||
case strings.Contains(err.Error(), "message is not modified"):
|
case strings.Contains(err.Error(), "message is not modified"):
|
||||||
log.Err(err).Msg("EditMessageText ignored: message is not modified")
|
log.Err(err).Msg("EditMessageText ignored: message is not modified")
|
||||||
case strings.Contains(err.Error(), "no text in the message to edit"):
|
case strings.Contains(err.Error(), "no text in the message to edit"):
|
||||||
b.LastMessageIsMedia = true
|
b.lastRender.isMedia = true
|
||||||
log.Err(err).Msg("EditMessageText ignored: message has no text")
|
log.Err(err).Msg("EditMessageText ignored: message has no text")
|
||||||
default:
|
default:
|
||||||
log.Error().Err(err).Msg("EditMessageText")
|
log.Error().Err(err).Msg("EditMessageText")
|
||||||
@@ -284,7 +310,10 @@ func (b *Bot) Update(u *echotron.Update) {
|
|||||||
|
|
||||||
err := b.GetOrCreateJWT(u)
|
err := b.GetOrCreateJWT(u)
|
||||||
if err != nil || b.Session.JWT == "" {
|
if err != nil || b.Session.JWT == "" {
|
||||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", emptyKeyboard)
|
b.Edit(
|
||||||
|
"❌ Ошибка авторизации",
|
||||||
|
echotron.InlineKeyboardMarkup{InlineKeyboard: [][]echotron.InlineKeyboardButton{{{Text: "↻ Обновить", CallbackData: "refresh"}}}},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,11 +336,11 @@ func (b *Bot) Update(u *echotron.Update) {
|
|||||||
case "/start login":
|
case "/start login":
|
||||||
b.SetState(b.commandRouter("/start login"), NewMessage)
|
b.SetState(b.commandRouter("/start login"), NewMessage)
|
||||||
return
|
return
|
||||||
default:
|
}
|
||||||
b.CurrentState.HandleMessage(b, u)
|
|
||||||
if e.handled || e.transitioned {
|
b.CurrentState.HandleMessage(b, u)
|
||||||
return
|
if e.handled || e.transitioned {
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,11 +348,17 @@ func (b *Bot) Update(u *echotron.Update) {
|
|||||||
if u.CallbackQuery != nil {
|
if u.CallbackQuery != nil {
|
||||||
b.cleanupCallbackUI(u)
|
b.cleanupCallbackUI(u)
|
||||||
|
|
||||||
if u.CallbackQuery.Data == "empty" {
|
switch u.CallbackQuery.Data {
|
||||||
|
case "empty":
|
||||||
|
return
|
||||||
|
case "refresh":
|
||||||
|
if b.CurrentState != nil {
|
||||||
|
b.CurrentState.Enter(b, EditMessage)
|
||||||
|
e.handled = true
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Глобальные callbacks (из уведомлений)
|
|
||||||
if newState := b.globalCallbackRouter(u.CallbackQuery.Data); newState != nil {
|
if newState := b.globalCallbackRouter(u.CallbackQuery.Data); newState != nil {
|
||||||
b.SetState(newState, NewMessage)
|
b.SetState(newState, NewMessage)
|
||||||
return
|
return
|
||||||
@@ -358,6 +393,9 @@ func (b *Bot) Update(u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) cleanupKeyboard(messageID int) {
|
func (b *Bot) cleanupKeyboard(messageID int) {
|
||||||
|
var emptyKeyboard = echotron.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]echotron.InlineKeyboardButton{},
|
||||||
|
}
|
||||||
_, err := b.EditMessageReplyMarkup(
|
_, err := b.EditMessageReplyMarkup(
|
||||||
echotron.NewMessageID(b.ChatID, messageID),
|
echotron.NewMessageID(b.ChatID, messageID),
|
||||||
&echotron.MessageReplyMarkupOptions{ReplyMarkup: emptyKeyboard},
|
&echotron.MessageReplyMarkupOptions{ReplyMarkup: emptyKeyboard},
|
||||||
|
|||||||
@@ -17,12 +17,10 @@ func main() {
|
|||||||
initLogger()
|
initLogger()
|
||||||
|
|
||||||
botToken := mustEnv("TELEGRAM__TOKEN")
|
botToken := mustEnv("TELEGRAM__TOKEN")
|
||||||
loginURL := mustEnv("LOGIN_URL")
|
|
||||||
baseURL := os.Getenv("BACKEND__BASE_URL")
|
|
||||||
|
|
||||||
backendClient := backend.New(backend.Config{
|
backendClient := backend.New(backend.Config{
|
||||||
BaseURL: baseURL,
|
BaseURL: mustEnv("BACKEND__BASE_URL"),
|
||||||
LoginURL: loginURL,
|
LoginURL: mustEnv("LOGIN_URL"),
|
||||||
})
|
})
|
||||||
|
|
||||||
var commandRouter = func(command string) bot.State {
|
var commandRouter = func(command string) bot.State {
|
||||||
@@ -55,8 +53,6 @@ func main() {
|
|||||||
|
|
||||||
dsp := echotron.NewDispatcher(botToken, newBot)
|
dsp := echotron.NewDispatcher(botToken, newBot)
|
||||||
|
|
||||||
log.Info().Msg("bot starting...")
|
|
||||||
|
|
||||||
updateOpts := echotron.UpdateOptions{
|
updateOpts := echotron.UpdateOptions{
|
||||||
AllowedUpdates: []echotron.UpdateType{
|
AllowedUpdates: []echotron.UpdateType{
|
||||||
echotron.MessageUpdate,
|
echotron.MessageUpdate,
|
||||||
@@ -70,7 +66,8 @@ func main() {
|
|||||||
echotron.SetChatRequestLimit(0, 0)
|
echotron.SetChatRequestLimit(0, 0)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if err := dsp.PollOptions(false, updateOpts); err != nil {
|
err := dsp.PollOptions(false, updateOpts)
|
||||||
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("dsp.Poll failed, retrying in 5 seconds...")
|
log.Error().Err(err).Msg("dsp.Poll failed, retrying in 5 seconds...")
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package screens
|
package screens
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -15,7 +17,6 @@ const addProjectInstructionText = `
|
|||||||
<b>2.</b> Дайте боту следующие права:
|
<b>2.</b> Дайте боту следующие права:
|
||||||
• Удаление сообщений
|
• Удаление сообщений
|
||||||
• Приглашение пользователей
|
• Приглашение пользователей
|
||||||
• Блокировка пользователей
|
|
||||||
|
|
||||||
<b>3.</b> После добавления бота:
|
<b>3.</b> После добавления бота:
|
||||||
• Если у вас <b>1 рабочее пространство</b> — канал добавится автоматически
|
• Если у вас <b>1 рабочее пространство</b> — канал добавится автоматически
|
||||||
@@ -29,9 +30,15 @@ type AddProject struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
|
func (s *AddProject) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
|
botUsername := bot.BotUsername()
|
||||||
|
if botUsername == "" {
|
||||||
|
botUsername = "chat_cruiser_bot"
|
||||||
|
}
|
||||||
|
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(Button("← Назад", "back")),
|
Row(Button("← Назад", "back")),
|
||||||
Row(URLButton("Добавить", "tg://resolve?domain=chat_cruiser_bot&startchannel&admin=invite_users+post_messages+edit_messages+delete_messages")),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboard := Keyboard(buttons...)
|
keyboard := Keyboard(buttons...)
|
||||||
@@ -48,11 +55,7 @@ func (s *AddProject) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
if s.BackState != nil {
|
if s.BackState != nil {
|
||||||
b.SetState(s.BackState, bot.EditMessage)
|
b.SetState(s.BackState, bot.EditMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
|
||||||
s.Enter(b, bot.NewMessage)
|
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AddProject) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
func (s *AddProject) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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/bot"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
||||||
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -96,24 +97,15 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
} else {
|
} else {
|
||||||
text += fmt.Sprintf("Проекты%s\n\n", ui.FormatPageInfo(s.ProjectPage, page.Pages))
|
text += fmt.Sprintf("Проекты%s\n\n", ui.FormatPageInfo(s.ProjectPage, page.Pages))
|
||||||
|
|
||||||
var slots []echotron.InlineKeyboardButton
|
buttons = append(buttons, ui.BuildGrid(
|
||||||
for _, project := range page.Items {
|
page.Items,
|
||||||
slots = append(slots, Button(project.Title, fmt.Sprintf("project:%s", project.ID)))
|
2,
|
||||||
}
|
addPurchaseProjectsPerPage,
|
||||||
if page.Pages >= 2 {
|
page.Pages,
|
||||||
for len(slots) < addPurchaseProjectsPerPage {
|
func(project backend.Project) (string, string) {
|
||||||
slots = append(slots, Button(" ", "empty"))
|
return project.Title, "project:" + project.ID
|
||||||
}
|
},
|
||||||
}
|
)...)
|
||||||
for i := 0; i < len(slots); i += 2 {
|
|
||||||
row := []echotron.InlineKeyboardButton{slots[i]}
|
|
||||||
if i+1 < len(slots) {
|
|
||||||
row = append(row, slots[i+1])
|
|
||||||
} else {
|
|
||||||
row = append(row, Button(" ", "empty"))
|
|
||||||
}
|
|
||||||
buttons = append(buttons, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
||||||
CurrentPage: s.ProjectPage,
|
CurrentPage: s.ProjectPage,
|
||||||
@@ -157,24 +149,15 @@ func (s *AddPurchase) renderSelection(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
} else {
|
} else {
|
||||||
text += fmt.Sprintf("Креативы%s\n\n", ui.FormatPageInfo(s.CreativePage, page.Pages))
|
text += fmt.Sprintf("Креативы%s\n\n", ui.FormatPageInfo(s.CreativePage, page.Pages))
|
||||||
|
|
||||||
var slots []echotron.InlineKeyboardButton
|
buttons = append(buttons, ui.BuildGrid(
|
||||||
for _, creative := range page.Items {
|
page.Items,
|
||||||
slots = append(slots, Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)))
|
2,
|
||||||
}
|
addPurchaseCreativesPerPage,
|
||||||
if page.Pages >= 2 {
|
page.Pages,
|
||||||
for len(slots) < addPurchaseCreativesPerPage {
|
func(creative backend.Creative) (string, string) {
|
||||||
slots = append(slots, Button(" ", "empty"))
|
return creative.Name, "creative:" + creative.ID
|
||||||
}
|
},
|
||||||
}
|
)...)
|
||||||
for i := 0; i < len(slots); i += 2 {
|
|
||||||
row := []echotron.InlineKeyboardButton{slots[i]}
|
|
||||||
if i+1 < len(slots) {
|
|
||||||
row = append(row, slots[i+1])
|
|
||||||
} else {
|
|
||||||
row = append(row, Button(" ", "empty"))
|
|
||||||
}
|
|
||||||
buttons = append(buttons, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
||||||
CurrentPage: s.CreativePage,
|
CurrentPage: s.CreativePage,
|
||||||
|
|||||||
@@ -2,17 +2,15 @@ package screens
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"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/bot"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
||||||
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const creativesPerPage = 6
|
|
||||||
|
|
||||||
type Creatives struct {
|
type Creatives struct {
|
||||||
CurrentPage int
|
CurrentPage int
|
||||||
ProjectID string
|
ProjectID string
|
||||||
@@ -23,23 +21,11 @@ type Creatives struct {
|
|||||||
BackState bot.State
|
BackState bot.State
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Creatives) Enter(b *bot.Bot, mode bot.RenderMode) {
|
const creativesPerPage = 6
|
||||||
s.renderCreatives(b, mode)
|
const creativesPerRow = 2
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode) {
|
const msgNoCreatives = `
|
||||||
page, err := b.Backend.GetCreatives(context.Background(), b.Session.JWT, b.Session.WorkspaceID, &s.ProjectID, false, s.CurrentPage+1, creativesPerPage)
|
<b>Креативы</b>
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to get creatives")
|
|
||||||
b.SendNew("❌ Не удалось загрузить креативы", Keyboard())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var text string
|
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
|
||||||
|
|
||||||
if len(page.Items) == 0 {
|
|
||||||
text = `<b>Креативы</b>
|
|
||||||
|
|
||||||
У вас пока нет креативов
|
У вас пока нет креативов
|
||||||
|
|
||||||
@@ -49,65 +35,51 @@ func (s *Creatives) renderCreatives(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
‣ Шаблон для автопостинга
|
‣ Шаблон для автопостинга
|
||||||
‣ Варианты для A/B тестирования
|
‣ Варианты для A/B тестирования
|
||||||
|
|
||||||
Начните с добавления первого креатива`
|
Начните с добавления первого креатива
|
||||||
} else {
|
`
|
||||||
text = fmt.Sprintf(`<b>Креативы</b>%s
|
|
||||||
|
const msgCreatives = `
|
||||||
|
<b>Креативы</b>
|
||||||
|
|
||||||
Управление рекламными материалами
|
Управление рекламными материалами
|
||||||
|
|
||||||
Выберите креатив`,
|
Выберите креатив
|
||||||
ui.FormatPageInfo(s.CurrentPage, page.Pages))
|
`
|
||||||
|
|
||||||
|
func (s *Creatives) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
|
page, err := b.Backend.GetCreatives(context.Background(), b.Session.JWT, b.Session.WorkspaceID, &s.ProjectID, false, s.CurrentPage+1, creativesPerPage)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to get creatives")
|
||||||
|
b.SendNew("❌ Не удалось загрузить креативы", refreshKeyboard)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем слоты для креативов
|
text := msgCreatives
|
||||||
var slots []echotron.InlineKeyboardButton
|
if len(page.Items) == 0 {
|
||||||
for _, creative := range page.Items {
|
text = msgNoCreatives
|
||||||
slots = append(slots, Button(creative.Name, fmt.Sprintf("creative:%s", creative.ID)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если страниц >= 2, заполняем до 6 слотов пустыми кнопками
|
var kbRows [][]echotron.InlineKeyboardButton
|
||||||
if page.Pages >= 2 {
|
|
||||||
for len(slots) < creativesPerPage {
|
|
||||||
slots = append(slots, Button(" ", "empty"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Разбиваем слоты по 2 в ряду
|
rowsGrid := ui.BuildGrid(page.Items, creativesPerRow, creativesPerPage, page.Pages,
|
||||||
for i := 0; i < len(slots); i += 2 {
|
func(creative backend.Creative) (string, string) { return creative.Name, "creative:" + creative.ID },
|
||||||
var row []echotron.InlineKeyboardButton
|
)
|
||||||
row = append(row, slots[i])
|
kbRows = append(kbRows, rowsGrid...)
|
||||||
|
|
||||||
// Второй слот в ряду (если есть)
|
|
||||||
if i+1 < len(slots) {
|
|
||||||
row = append(row, slots[i+1])
|
|
||||||
} else {
|
|
||||||
// Если нечетное количество, добавляем пустую кнопку
|
|
||||||
row = append(row, Button(" ", "empty"))
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons = append(buttons, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ряд: Пагинация + Добавить (динамическая ширина)
|
|
||||||
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
||||||
CurrentPage: s.CurrentPage,
|
CurrentPage: s.CurrentPage,
|
||||||
TotalPages: page.Pages,
|
TotalPages: page.Pages,
|
||||||
MiddleButtons: []echotron.InlineKeyboardButton{Button("+ Добавить", "add_creative")},
|
MiddleButtons: Row(Button("+ Добавить", "add_creative")),
|
||||||
}); navRow != nil {
|
}); navRow != nil {
|
||||||
buttons = append(buttons, navRow)
|
kbRows = append(kbRows, navRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Нижний ряд: Назад + Архив
|
kbRows = append(kbRows, Row(Button("← Назад", "back"), Button("≡ Архив", "archive")))
|
||||||
buttons = append(buttons, Row(
|
|
||||||
Button("← Назад", "back"),
|
|
||||||
Button("≡ Архив", "archive"),
|
|
||||||
))
|
|
||||||
|
|
||||||
keyboard := Keyboard(buttons...)
|
kb := Keyboard(kbRows...)
|
||||||
b.Render(text, keyboard, mode)
|
b.Render(text, kb, mode)
|
||||||
|
|
||||||
messageID := b.LastMessageID
|
updateProjectHeaderMedia(b, b.LastMessageID, text, kb, s.ProjectTelegramID, s.ProjectTitle, s.ProjectUsername, s.ProjectStatus)
|
||||||
updateProjectHeaderMedia(b, messageID, text, keyboard, s.ProjectTelegramID, s.ProjectTitle, s.ProjectUsername, s.ProjectStatus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||||
@@ -134,10 +106,7 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case data == "archive":
|
case data == "archive":
|
||||||
// TODO: реализовать экран архива креативов
|
b.Edit("📦 Архив креативов\n\nЭта функция в разработке", Keyboard(Row(Button("← Назад", "back"))))
|
||||||
b.Edit("📦 Архив креативов\n\nЭта функция в разработке", Keyboard(
|
|
||||||
Row(Button("← Назад", "back")),
|
|
||||||
))
|
|
||||||
|
|
||||||
case data == "add_creative":
|
case data == "add_creative":
|
||||||
b.SetState(&AddCreativeStart{ctx: &AddCreativeCtx{
|
b.SetState(&AddCreativeStart{ctx: &AddCreativeCtx{
|
||||||
@@ -146,26 +115,25 @@ func (s *Creatives) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
}}, bot.EditMessage)
|
}}, bot.EditMessage)
|
||||||
|
|
||||||
case strings.HasPrefix(data, "creative:"):
|
case strings.HasPrefix(data, "creative:"):
|
||||||
// Открытие деталей креатива
|
creativeID, ok := strings.CutPrefix(data, "creative:")
|
||||||
parts := strings.Split(data, ":")
|
if !ok || creativeID == "" {
|
||||||
if len(parts) == 2 {
|
return
|
||||||
creativeID := parts[1]
|
|
||||||
|
|
||||||
b.SetState(&CreativeDetails{
|
|
||||||
CreativeID: creativeID,
|
|
||||||
ProjectID: s.ProjectID,
|
|
||||||
BackState: s,
|
|
||||||
}, bot.NewMessage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.SetState(&CreativeDetails{
|
||||||
|
CreativeID: creativeID,
|
||||||
|
ProjectID: s.ProjectID,
|
||||||
|
BackState: s,
|
||||||
|
}, bot.NewMessage)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Creatives) HandleMessage(_ *bot.Bot, _ *echotron.Update) { return }
|
func (s *Creatives) HandleMessage(_ *bot.Bot, _ *echotron.Update) {}
|
||||||
|
|
||||||
func (s *Creatives) Handle(_ *bot.Bot, _ *echotron.Update) { return }
|
func (s *Creatives) Handle(_ *bot.Bot, _ *echotron.Update) {}
|
||||||
|
|
||||||
func (s *Creatives) Exit() {}
|
func (s *Creatives) Exit() {}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var emptyKeyboard = Keyboard()
|
var emptyKeyboard = Keyboard()
|
||||||
|
var refreshKeyboard = Keyboard(Row(Button("↻ Обновить", "refresh")))
|
||||||
|
|
||||||
func Button(text, data string) echotron.InlineKeyboardButton {
|
func Button(text, data string) echotron.InlineKeyboardButton {
|
||||||
return echotron.InlineKeyboardButton{
|
return echotron.InlineKeyboardButton{
|
||||||
|
|||||||
@@ -2,28 +2,51 @@ package screens
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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/bot"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/bot"
|
||||||
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
"github.com/TelegramExchange/tgex-backend/tg_bot/screens/ui"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const projectsPerPage = 6
|
const projectsPerPage = 6
|
||||||
|
const projectsPerRow = 2
|
||||||
|
|
||||||
type MyProjects struct {
|
type MyProjects struct {
|
||||||
CurrentPage int
|
CurrentPage int
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
OpenPlacements bool
|
OpenPlacements bool
|
||||||
|
|
||||||
// Поля для управления фоновой горутиной polling
|
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
lastProjectCount int
|
lastProjectCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const msgNoProjects = `
|
||||||
|
<b>Мои проекты</b>
|
||||||
|
|
||||||
|
У вас пока нет подключенных каналов.
|
||||||
|
|
||||||
|
<i>Что дает подключение канала:</i>
|
||||||
|
▸ Управление типом приглашений (публичные/с одобрением)
|
||||||
|
▸ Автоматическая отправка креативов в канал
|
||||||
|
▸ Интеграция с планом закупов
|
||||||
|
▸ Статистика и аналитика подписчиков
|
||||||
|
▸ Настройка уведомлений
|
||||||
|
|
||||||
|
Начните с добавления первого проекта
|
||||||
|
`
|
||||||
|
|
||||||
|
const msgProjects = `
|
||||||
|
<b>Мои проекты</b>
|
||||||
|
|
||||||
|
Управление вашими Telegram-каналами.
|
||||||
|
|
||||||
|
Выберите канал
|
||||||
|
`
|
||||||
|
|
||||||
func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
|
func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
s.renderProjects(b, mode)
|
s.renderProjects(b, mode)
|
||||||
|
|
||||||
@@ -33,91 +56,36 @@ func (s *MyProjects) Enter(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode) {
|
func (s *MyProjects) renderProjects(b *bot.Bot, mode bot.RenderMode) {
|
||||||
page, err := b.Backend.GetProjects(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CurrentPage+1, projectsPerPage)
|
page, err := b.Backend.GetProjects(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.CurrentPage+1, projectsPerPage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get projects")
|
b.SendNew("❌ Не удалось загрузить проекты", refreshKeyboard)
|
||||||
b.SendNew("❌ Не удалось загрузить проекты", Keyboard())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем количество проектов для polling
|
|
||||||
s.lastProjectCount = page.Total
|
s.lastProjectCount = page.Total
|
||||||
|
|
||||||
var text string
|
var kbRows [][]echotron.InlineKeyboardButton
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
|
||||||
headerTitle := "Мои проекты"
|
|
||||||
emptyStateText := `У вас пока нет подключенных каналов.
|
|
||||||
|
|
||||||
<i>Что дает подключение канала:</i>
|
grid := ui.BuildGrid(page.Items, projectsPerRow, projectsPerPage, page.Pages,
|
||||||
▸ Управление типом приглашений (публичные/с одобрением)
|
func(project backend.Project) (string, string) { return project.Title, "project:" + project.ID },
|
||||||
▸ Автоматическая отправка креативов в канал
|
)
|
||||||
▸ Интеграция с планом закупов
|
kbRows = append(kbRows, grid...)
|
||||||
▸ Статистика и аналитика подписчиков
|
|
||||||
▸ Настройка уведомлений
|
|
||||||
|
|
||||||
Начните с добавления первого проекта`
|
|
||||||
nonEmptyText := "Управление вашими Telegram-каналами.\n\nВыберите канал"
|
|
||||||
if s.OpenPlacements {
|
|
||||||
headerTitle = "Размещения"
|
|
||||||
nonEmptyText = "Выберите проект для просмотра закупов"
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(page.Items) == 0 {
|
|
||||||
text = fmt.Sprintf(`<b>%s</b>
|
|
||||||
|
|
||||||
%s`, headerTitle, emptyStateText)
|
|
||||||
} else {
|
|
||||||
text = fmt.Sprintf(`<b>%s</b>%s
|
|
||||||
|
|
||||||
%s`,
|
|
||||||
headerTitle,
|
|
||||||
ui.FormatPageInfo(s.CurrentPage, page.Pages),
|
|
||||||
nonEmptyText)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем слоты для проектов
|
|
||||||
var slots []echotron.InlineKeyboardButton
|
|
||||||
for _, project := range page.Items {
|
|
||||||
slots = append(slots, Button(project.Title, fmt.Sprintf("project:%s", project.ID)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если страниц >= 2, заполняем до 6 слотов пустыми кнопками
|
|
||||||
if page.Pages >= 2 {
|
|
||||||
for len(slots) < projectsPerPage {
|
|
||||||
slots = append(slots, Button(" ", "empty"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Разбиваем слоты по 2 в ряду
|
|
||||||
for i := 0; i < len(slots); i += 2 {
|
|
||||||
var row []echotron.InlineKeyboardButton
|
|
||||||
row = append(row, slots[i])
|
|
||||||
|
|
||||||
// Второй слот в ряду (если есть)
|
|
||||||
if i+1 < len(slots) {
|
|
||||||
row = append(row, slots[i+1])
|
|
||||||
} else {
|
|
||||||
// Если нечетное количество, добавляем пустую кнопку
|
|
||||||
row = append(row, Button(" ", "empty"))
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons = append(buttons, row)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ряд: Пагинация + Добавить (динамическая ширина)
|
|
||||||
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
||||||
CurrentPage: s.CurrentPage,
|
CurrentPage: s.CurrentPage,
|
||||||
TotalPages: page.Pages,
|
TotalPages: page.Pages,
|
||||||
MiddleButtons: []echotron.InlineKeyboardButton{Button("+ Добавить", "add_project")},
|
MiddleButtons: Row(Button("+ Добавить", "add_project")),
|
||||||
}); navRow != nil {
|
}); navRow != nil {
|
||||||
buttons = append(buttons, navRow)
|
kbRows = append(kbRows, navRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Нижний ряд: Назад + Архив
|
kbRows = append(kbRows, Row(Button("← Назад", "main_menu"), Button("≡ Архив", "archive")))
|
||||||
buttons = append(buttons, Row(
|
|
||||||
Button("← Назад", "main_menu"),
|
keyboard := Keyboard(kbRows...)
|
||||||
Button("≡ Архив", "archive"),
|
|
||||||
))
|
text := msgProjects
|
||||||
|
if len(page.Items) == 0 {
|
||||||
|
text = msgNoProjects
|
||||||
|
}
|
||||||
|
|
||||||
keyboard := Keyboard(buttons...)
|
|
||||||
b.Render(text, keyboard, mode)
|
b.Render(text, keyboard, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,16 +111,11 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
b.SetState(&MainMenu{}, bot.EditMessage)
|
b.SetState(&MainMenu{}, bot.EditMessage)
|
||||||
|
|
||||||
case data == "archive":
|
case data == "archive":
|
||||||
b.Edit("📦 Архив проектов\n\nЭта функция в разработке", Keyboard(
|
b.Edit("Эта функция в разработке", Keyboard(Row(Button("← Назад", "in_dev"))))
|
||||||
Row(Button("← Назад", "back_to_projects")),
|
|
||||||
))
|
|
||||||
|
|
||||||
case data == "add_project":
|
case data == "add_project":
|
||||||
b.SetState(&AddProject{BackState: &MyProjects{}}, bot.EditMessage)
|
b.SetState(&AddProject{BackState: &MyProjects{}}, bot.EditMessage)
|
||||||
|
|
||||||
case data == "back_to_projects":
|
|
||||||
s.Enter(b, bot.EditMessage)
|
|
||||||
|
|
||||||
case strings.HasPrefix(data, "project:"):
|
case strings.HasPrefix(data, "project:"):
|
||||||
projectID, ok := strings.CutPrefix(data, "project:")
|
projectID, ok := strings.CutPrefix(data, "project:")
|
||||||
if !ok || projectID == "" {
|
if !ok || projectID == "" {
|
||||||
@@ -178,7 +141,7 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlacementDetails struct {
|
type PlacementDetails struct {
|
||||||
@@ -25,7 +24,6 @@ func (s *PlacementDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
|
|||||||
func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
|
func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
|
||||||
placement, err := b.Backend.GetPlacement(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, s.PlacementID)
|
placement, err := b.Backend.GetPlacement(context.Background(), b.Session.JWT, b.Session.WorkspaceID, s.ProjectID, s.PlacementID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get placement")
|
|
||||||
b.SendNew("❌ Не удалось загрузить размещение", Keyboard())
|
b.SendNew("❌ Не удалось загрузить размещение", Keyboard())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
21
tg_bot/screens/ui/buttons.go
Normal file
21
tg_bot/screens/ui/buttons.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "github.com/NicoNex/echotron/v3"
|
||||||
|
|
||||||
|
// BuildGrid builds button rows from items using a callback builder.
|
||||||
|
func BuildGrid[T any](items []T, itemsPerRow, itemsPerPage, totalPages int, itemFn func(T) (title string, callback string)) [][]echotron.InlineKeyboardButton {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons := make([]echotron.InlineKeyboardButton, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
title, callback := itemFn(item)
|
||||||
|
buttons = append(buttons, echotron.InlineKeyboardButton{
|
||||||
|
Text: title,
|
||||||
|
CallbackData: callback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildPageRows(buttons, itemsPerRow, itemsPerPage, totalPages)
|
||||||
|
}
|
||||||
@@ -173,3 +173,47 @@ func BuildElementRows(config ElementLayoutConfig, allItems []echotron.InlineKeyb
|
|||||||
|
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BuildPageRows раскладывает элементы текущей страницы без собственной пагинации.
|
||||||
|
// Автоматически заполняет пустыми кнопками если страниц > 1 (для консистентности высоты).
|
||||||
|
func BuildPageRows(pageItems []echotron.InlineKeyboardButton, itemsPerRow, itemsPerPage, totalPages int) [][]echotron.InlineKeyboardButton {
|
||||||
|
if len(pageItems) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Параметры по умолчанию
|
||||||
|
if itemsPerPage <= 0 {
|
||||||
|
itemsPerPage = 6
|
||||||
|
}
|
||||||
|
if itemsPerRow <= 0 {
|
||||||
|
itemsPerRow = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка-заполнитель по умолчанию
|
||||||
|
emptyButton := echotron.InlineKeyboardButton{Text: " ", CallbackData: "empty"}
|
||||||
|
|
||||||
|
// Если страниц >= 2, заполняем до ItemsPerPage пустыми кнопками
|
||||||
|
if totalPages >= 2 {
|
||||||
|
for len(pageItems) < itemsPerPage {
|
||||||
|
pageItems = append(pageItems, emptyButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Раскладываем элементы по рядам
|
||||||
|
var rows [][]echotron.InlineKeyboardButton
|
||||||
|
for i := 0; i < len(pageItems); i += itemsPerRow {
|
||||||
|
var row []echotron.InlineKeyboardButton
|
||||||
|
|
||||||
|
for j := 0; j < itemsPerRow && i+j < len(pageItems); j++ {
|
||||||
|
row = append(row, pageItems[i+j])
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(row) < itemsPerRow {
|
||||||
|
row = append(row, emptyButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user