refactor
This commit is contained in:
@@ -6,19 +6,13 @@ from src import deps, dto
|
|||||||
internal_router = APIRouter(prefix='/internal', tags=['internal'])
|
internal_router = APIRouter(prefix='/internal', tags=['internal'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CreateLoginTokenResponse(BaseModel):
|
class CreateLoginTokenResponse(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
@internal_router.post('/auth/login-token')
|
@internal_router.post('/auth/login-token')
|
||||||
async def create_login_token(input: dto.CreateLoginTokenRequest) -> str:
|
async def create_login_token(input: dto.CreateLoginTokenRequest) -> str:
|
||||||
token = await deps.get_usecase().create_telegram_login_token(
|
return await deps.get_usecase().create_telegram_login_token(telegram_id=input.telegram_id)
|
||||||
telegram_id=input.telegram_id,
|
|
||||||
username=input.username,
|
|
||||||
)
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
class ValidateTokenRequest(BaseModel):
|
class ValidateTokenRequest(BaseModel):
|
||||||
@@ -27,7 +21,6 @@ class ValidateTokenRequest(BaseModel):
|
|||||||
username: str | None = None
|
username: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@internal_router.get('/auth/jwt')
|
@internal_router.get('/auth/jwt')
|
||||||
async def get_jwt_by_telegram_id(
|
async def get_jwt_by_telegram_id(
|
||||||
telegram_id: int,
|
telegram_id: int,
|
||||||
|
|||||||
@@ -98,19 +98,19 @@ from .analytics import (
|
|||||||
GetChannelAnalyticsOutput,
|
GetChannelAnalyticsOutput,
|
||||||
GetCreativesAnalyticsInput,
|
GetCreativesAnalyticsInput,
|
||||||
GetCreativesAnalyticsOutput,
|
GetCreativesAnalyticsOutput,
|
||||||
|
GetOverviewAnalyticsInput,
|
||||||
|
GetOverviewAnalyticsOutput,
|
||||||
GetPlacementsAnalyticsInput,
|
GetPlacementsAnalyticsInput,
|
||||||
GetPlacementsAnalyticsOutput,
|
GetPlacementsAnalyticsOutput,
|
||||||
GetOverviewAnalyticsInput,
|
|
||||||
GetProjectsAnalyticsInput,
|
GetProjectsAnalyticsInput,
|
||||||
GetProjectsAnalyticsOutput,
|
GetProjectsAnalyticsOutput,
|
||||||
GetSpendingAnalyticsInput,
|
GetSpendingAnalyticsInput,
|
||||||
GetSpendingAnalyticsOutput,
|
GetSpendingAnalyticsOutput,
|
||||||
GetOverviewAnalyticsOutput,
|
|
||||||
NumberWithDelta,
|
NumberWithDelta,
|
||||||
PlacementAnalyticsOutput,
|
|
||||||
OverviewDailyPoint,
|
|
||||||
OverviewChannelPerformance,
|
OverviewChannelPerformance,
|
||||||
|
OverviewDailyPoint,
|
||||||
OverviewProjectSpending,
|
OverviewProjectSpending,
|
||||||
|
PlacementAnalyticsOutput,
|
||||||
ProjectAnalyticsPeriod,
|
ProjectAnalyticsPeriod,
|
||||||
ProjectMetrics,
|
ProjectMetrics,
|
||||||
ProjectMetricsData,
|
ProjectMetricsData,
|
||||||
@@ -189,4 +189,3 @@ from .workspace import (
|
|||||||
|
|
||||||
class CreateLoginTokenRequest(BaseModel):
|
class CreateLoginTokenRequest(BaseModel):
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
username: str | None
|
|
||||||
|
|||||||
@@ -10,14 +10,11 @@ if typing.TYPE_CHECKING:
|
|||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
async def create_telegram_login_token(self: 'Usecase', telegram_id: int, username: str | None) -> str:
|
async def create_telegram_login_token(self: 'Usecase', telegram_id: int) -> str:
|
||||||
telegram_user = await self.database.get_telegram_user(telegram_id=telegram_id)
|
telegram_user = await self.database.get_telegram_user(telegram_id=telegram_id)
|
||||||
if not telegram_user:
|
if not telegram_user:
|
||||||
telegram_user = domain.TelegramUser(telegram_id=telegram_id, username=username)
|
telegram_user = domain.TelegramUser(telegram_id=telegram_id)
|
||||||
await self.database.create_telegram_user(telegram_user)
|
await self.database.create_telegram_user(telegram_user)
|
||||||
elif username and telegram_user.username != username:
|
|
||||||
telegram_user.username = username
|
|
||||||
await self.database.update_telegram_user(telegram_user)
|
|
||||||
|
|
||||||
user = await self.database.get_user(telegram_id=telegram_id)
|
user = await self.database.get_user(telegram_id=telegram_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
|||||||
@@ -89,31 +89,19 @@ func (c *Client) do(ctx context.Context, method string, path string, in any, out
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetLoginURL(token string) string {
|
func (c *Client) LoginURL(token string) string {
|
||||||
return c.loginURL + token
|
return c.loginURL + token
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) CreateLoginToken(
|
func (c *Client) CreateLoginToken(ctx context.Context, telegramID int64) (string, error) {
|
||||||
ctx context.Context,
|
|
||||||
telegramID int64,
|
|
||||||
username *string,
|
|
||||||
) (string, error) {
|
|
||||||
req := struct {
|
req := struct {
|
||||||
TelegramID int64 `json:"telegram_id"`
|
TelegramID int64 `json:"telegram_id"`
|
||||||
Username *string `json:"username"`
|
|
||||||
}{
|
}{
|
||||||
TelegramID: telegramID,
|
TelegramID: telegramID,
|
||||||
Username: username,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var token string
|
var token string
|
||||||
err := c.do(
|
err := c.do(ctx, http.MethodPost, "api/v1/internal/auth/login-token", req, &token)
|
||||||
ctx,
|
|
||||||
http.MethodPost,
|
|
||||||
"api/v1/internal/auth/login-token",
|
|
||||||
req,
|
|
||||||
&token,
|
|
||||||
)
|
|
||||||
|
|
||||||
return token, err
|
return token, err
|
||||||
}
|
}
|
||||||
@@ -313,6 +301,28 @@ func (c *Client) ForwardTelegramUpdate(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) SearchChannels(
|
||||||
|
ctx context.Context,
|
||||||
|
jwt string,
|
||||||
|
username string,
|
||||||
|
) ([]Channel, error) {
|
||||||
|
path := fmt.Sprintf("api/v1/channels?username=%s", username)
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Items []Channel `json:"items"`
|
||||||
|
}
|
||||||
|
err := c.do(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
path,
|
||||||
|
nil,
|
||||||
|
&response,
|
||||||
|
withBearer(jwt),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.Items, err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) AttachChannelToWorkspace(
|
func (c *Client) AttachChannelToWorkspace(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
channelID string,
|
channelID string,
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ 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
|
transitioned bool
|
||||||
@@ -77,23 +81,17 @@ func (b *Bot) GetOrCreateJWT(u *echotron.Update) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запрашиваем JWT с актуальными данными пользователя (или создаем нового если не существует)
|
jwt, err := b.Backend.GetJWTByTelegramUser(context.Background(), b.ChatID, username, firstName, lastName)
|
||||||
jwt, err := b.Backend.GetJWTByTelegramUser(
|
|
||||||
context.Background(),
|
|
||||||
b.ChatID,
|
|
||||||
username,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сохраняем в сессию
|
|
||||||
b.Session.JWT = jwt
|
b.Session.JWT = jwt
|
||||||
if username != nil {
|
|
||||||
b.Session.UserName = *username
|
if firstName != nil {
|
||||||
|
b.Session.FirstName = *firstName
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +274,11 @@ func (b *Bot) Update(u *echotron.Update) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = b.GetOrCreateJWT(u)
|
err := b.GetOrCreateJWT(u)
|
||||||
|
if err != nil || b.Session.JWT == "" {
|
||||||
|
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", emptyKeyboard)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
exec := &Exec{}
|
exec := &Exec{}
|
||||||
b.exec = exec
|
b.exec = exec
|
||||||
@@ -336,10 +338,6 @@ func (b *Bot) Update(u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) cleanupKeyboard(messageID int) {
|
func (b *Bot) cleanupKeyboard(messageID int) {
|
||||||
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,6 +17,6 @@ type State interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
UserName string
|
FirstName string
|
||||||
JWT string
|
JWT string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,42 +6,35 @@ import (
|
|||||||
|
|
||||||
"example.com/m/bot"
|
"example.com/m/bot"
|
||||||
"github.com/NicoNex/echotron/v3"
|
"github.com/NicoNex/echotron/v3"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Login struct{}
|
type Login struct{}
|
||||||
|
|
||||||
func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) {
|
const msgHelloLogin = `
|
||||||
// JWT уже создан в Bot.Update() с актуальными данными пользователя
|
Привет, %s!
|
||||||
// Теперь создаем login token (это разовый токен для авторизации на сайте)
|
👇Нажми кнопку для входа на сайт.
|
||||||
var username *string
|
`
|
||||||
// Получаем username из сессии если он там сохранен
|
|
||||||
if b.Session.UserName != "" {
|
|
||||||
username = &b.Session.UserName
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := b.Backend.CreateLoginToken(context.Background(), b.ChatID, username)
|
func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
|
token, err := b.Backend.CreateLoginToken(context.Background(), b.ChatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to create login token")
|
b.SendNew("❌ Произошла ошибка при создании токена входа. Попробуйте позже.", emptyKeyboard)
|
||||||
b.SendMessage("❌ Произошла ошибка при создании токена входа. Попробуйте позже.", b.ChatID, nil)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loginURL := b.Backend.GetLoginURL(token)
|
loginURL := b.Backend.LoginURL(token)
|
||||||
|
|
||||||
usernameDisplay := "аноним"
|
usernameDisplay := "аноним"
|
||||||
if username != nil && *username != "" {
|
if b.Session.FirstName != "" {
|
||||||
usernameDisplay = *username
|
usernameDisplay = b.Session.FirstName
|
||||||
}
|
}
|
||||||
|
|
||||||
message := fmt.Sprintf("Привет, %s!\n\nНажми кнопку ниже для входа на сайт.", usernameDisplay)
|
kb := Keyboard(Row(URLButton("Войти на сайт", loginURL)))
|
||||||
b.SendMessageWithURLButton(message, "Войти на сайт", loginURL)
|
|
||||||
}
|
|
||||||
func (s *Login) HandleCallback(b *bot.Bot, u *echotron.Update) { return }
|
|
||||||
|
|
||||||
func (s *Login) HandleMessage(b *bot.Bot, u *echotron.Update) { return }
|
b.SendNew(fmt.Sprintf(msgHelloLogin, usernameDisplay), kb)
|
||||||
|
|
||||||
func (s *Login) Handle(b *bot.Bot, u *echotron.Update) {
|
|
||||||
b.SetState(&MainMenu{}, bot.NewMessage)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
func (s *Login) HandleCallback(b *bot.Bot, u *echotron.Update) {}
|
||||||
|
|
||||||
|
func (s *Login) HandleMessage(b *bot.Bot, u *echotron.Update) {}
|
||||||
|
|
||||||
|
func (s *Login) Handle(b *bot.Bot, u *echotron.Update) { b.SetState(&MainMenu{}, bot.NewMessage) }
|
||||||
|
|||||||
@@ -158,6 +158,28 @@ func (s *SelectChannelsForPurchase) HandleCallback(b *bot.Bot, u *echotron.Updat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверяем существование каналов через API
|
||||||
|
jwt := b.Session.JWT
|
||||||
|
if jwt == "" {
|
||||||
|
log.Error().Msg("JWT is empty in session")
|
||||||
|
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notFoundChannels := s.validateChannels(b, jwt)
|
||||||
|
if len(notFoundChannels) > 0 {
|
||||||
|
text := "❌ Следующие каналы не найдены:\n\n"
|
||||||
|
for _, username := range notFoundChannels {
|
||||||
|
text += fmt.Sprintf("• @%s\n", username)
|
||||||
|
}
|
||||||
|
text += "\nУдалите их из списка и попробуйте снова"
|
||||||
|
|
||||||
|
b.Edit(text, Keyboard(
|
||||||
|
Row(Button("← Назад", "back_to_select")),
|
||||||
|
))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
b.SetState(&PurchaseOptionalDetails{
|
b.SetState(&PurchaseOptionalDetails{
|
||||||
WorkspaceID: s.WorkspaceID,
|
WorkspaceID: s.WorkspaceID,
|
||||||
ProjectID: s.ProjectID,
|
ProjectID: s.ProjectID,
|
||||||
@@ -313,6 +335,39 @@ func (s *SelectChannelsForPurchase) HandleMessage(b *bot.Bot, u *echotron.Update
|
|||||||
s.Enter(b, bot.NewMessage)
|
s.Enter(b, bot.NewMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SelectChannelsForPurchase) validateChannels(b *bot.Bot, jwt string) []string {
|
||||||
|
var notFound []string
|
||||||
|
|
||||||
|
for _, ch := range s.Channels {
|
||||||
|
channels, err := b.Backend.SearchChannels(
|
||||||
|
context.Background(),
|
||||||
|
jwt,
|
||||||
|
ch.Username,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("username", ch.Username).Msg("Failed to search channel")
|
||||||
|
notFound = append(notFound, ch.Username)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем что нашли хотя бы один канал с точным совпадением username
|
||||||
|
found := false
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channel.Username != nil && *channel.Username == ch.Username {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
notFound = append(notFound, ch.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notFound
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SelectChannelsForPurchase) Handle(b *bot.Bot, u *echotron.Update) {
|
func (s *SelectChannelsForPurchase) Handle(b *bot.Bot, u *echotron.Update) {
|
||||||
// Обрабатываем callback после успешного создания
|
// Обрабатываем callback после успешного создания
|
||||||
if u.CallbackQuery != nil && u.CallbackQuery.Data == "done" {
|
if u.CallbackQuery != nil && u.CallbackQuery.Data == "done" {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"example.com/m/bot"
|
"example.com/m/bot"
|
||||||
|
"example.com/m/ui"
|
||||||
"github.com/NicoNex/echotron/v3"
|
"github.com/NicoNex/echotron/v3"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
@@ -13,48 +14,66 @@ import (
|
|||||||
type SelectWorkspace struct {
|
type SelectWorkspace struct {
|
||||||
ChannelID string
|
ChannelID string
|
||||||
BackState bot.State
|
BackState bot.State
|
||||||
|
CurrentPage int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const msgChooseWorkspace = `
|
||||||
|
📁 Выбор workspace для канала
|
||||||
|
В какой workspace добавить канал?
|
||||||
|
`
|
||||||
|
|
||||||
|
const msgSuccessChooseWorkspace = `
|
||||||
|
✅ Канал успешно добавлен!
|
||||||
|
📊 Проект ID: %s
|
||||||
|
📝 Название: %s
|
||||||
|
🏷 Статус: %s
|
||||||
|
`
|
||||||
|
|
||||||
func (s *SelectWorkspace) Enter(b *bot.Bot, mode bot.RenderMode) {
|
func (s *SelectWorkspace) Enter(b *bot.Bot, mode bot.RenderMode) {
|
||||||
// JWT уже создан в Bot.Update(), просто берем из сессии
|
workspaces, err := b.Backend.GetWorkspaces(context.Background(), b.Session.JWT)
|
||||||
jwt := b.Session.JWT
|
|
||||||
if jwt == "" {
|
|
||||||
log.Error().Msg("JWT is empty in session")
|
|
||||||
b.SendNew("❌ Ошибка авторизации. Попробуйте /start", Keyboard())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaces, err := b.Backend.GetWorkspaces(context.Background(), jwt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get workspaces")
|
|
||||||
b.SendNew("❌ Не удалось загрузить список workspace'ов", Keyboard())
|
b.SendNew("❌ Не удалось загрузить список workspace'ов", Keyboard())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(workspaces) == 0 {
|
if len(workspaces) == 0 {
|
||||||
b.SendNew("У вас пока нет workspace'ов", Keyboard(
|
kb := Keyboard(Row(Button("Главное меню", "back")))
|
||||||
Row(Button("🏠 Главное меню", "back")),
|
b.SendNew("У вас пока нет workspace'ов", kb)
|
||||||
))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text := "📁 Выбор workspace для канала\n\n"
|
// Создаем кнопки для всех workspace'ов
|
||||||
text += "В какой workspace добавить канал?"
|
var allButtons []echotron.InlineKeyboardButton
|
||||||
|
for _, ws := range workspaces {
|
||||||
|
allButtons = append(allButtons, Button(ws.Name, fmt.Sprintf("select_workspace:%s", ws.ID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем компонент пагинации для автоматической раскладки
|
||||||
|
const workspacesPerPage = 6
|
||||||
var buttons [][]echotron.InlineKeyboardButton
|
var buttons [][]echotron.InlineKeyboardButton
|
||||||
|
|
||||||
// Создаем кнопки для каждого workspace
|
// Раскладываем workspace'ы в grid (2 в ряд) с пагинацией
|
||||||
for _, ws := range workspaces {
|
elementRows := ui.BuildElementRows(ui.ElementLayoutConfig{
|
||||||
buttons = append(buttons, Row(
|
CurrentPage: s.CurrentPage,
|
||||||
Button(ws.Name, fmt.Sprintf("select_workspace:%s", ws.ID)),
|
ItemsPerPage: workspacesPerPage,
|
||||||
))
|
ItemsPerRow: 2,
|
||||||
|
}, allButtons)
|
||||||
|
buttons = append(buttons, elementRows...)
|
||||||
|
|
||||||
|
// Добавляем навигацию (стрелки появятся только если страниц > 1)
|
||||||
|
if navRow := ui.BuildNavigationRow(ui.PaginationConfig{
|
||||||
|
CurrentPage: s.CurrentPage,
|
||||||
|
TotalItems: len(allButtons),
|
||||||
|
ItemsPerPage: workspacesPerPage,
|
||||||
|
}); navRow != nil {
|
||||||
|
buttons = append(buttons, navRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кнопка отмены
|
// Кнопка отмены
|
||||||
buttons = append(buttons, Row(Button("❌ Отмена", "cancel")))
|
buttons = append(buttons, Row(Button("Отмена", "cancel")))
|
||||||
|
|
||||||
keyboard := Keyboard(buttons...)
|
kb := Keyboard(buttons...)
|
||||||
b.Render(text, keyboard, mode)
|
b.Render(msgChooseWorkspace, kb, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
||||||
@@ -65,13 +84,22 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
data := u.CallbackQuery.Data
|
data := u.CallbackQuery.Data
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
case data == "prev":
|
||||||
|
if s.CurrentPage > 0 {
|
||||||
|
s.CurrentPage--
|
||||||
|
}
|
||||||
|
s.Enter(b, bot.EditMessage)
|
||||||
|
|
||||||
|
case data == "next":
|
||||||
|
s.CurrentPage++
|
||||||
|
s.Enter(b, bot.EditMessage)
|
||||||
|
|
||||||
case data == "cancel", data == "back":
|
case data == "cancel", data == "back":
|
||||||
if s.BackState != nil {
|
if s.BackState != nil {
|
||||||
b.SetState(s.BackState, bot.NewMessage)
|
b.SetState(s.BackState, bot.NewMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasPrefix(data, "select_workspace:"):
|
case strings.HasPrefix(data, "select_workspace:"):
|
||||||
// Извлекаем workspace_id
|
|
||||||
parts := strings.Split(data, ":")
|
parts := strings.Split(data, ":")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
log.Error().Str("callback_data", data).Msg("Invalid callback data format")
|
log.Error().Str("callback_data", data).Msg("Invalid callback data format")
|
||||||
@@ -80,10 +108,6 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
|
|
||||||
workspaceID := parts[1]
|
workspaceID := parts[1]
|
||||||
|
|
||||||
// Показываем индикатор загрузки
|
|
||||||
b.Edit("⏳ Добавляем канал в workspace...", Keyboard())
|
|
||||||
|
|
||||||
// Вызываем API для привязки канала к workspace
|
|
||||||
project, err := b.Backend.AttachChannelToWorkspace(
|
project, err := b.Backend.AttachChannelToWorkspace(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
s.ChannelID,
|
s.ChannelID,
|
||||||
@@ -91,27 +115,19 @@ func (s *SelectWorkspace) HandleCallback(b *bot.Bot, u *echotron.Update) {
|
|||||||
b.ChatID,
|
b.ChatID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("channel_id", s.ChannelID).Str("workspace_id", workspaceID).Msg("Failed to attach channel to workspace")
|
|
||||||
b.Edit("❌ Не удалось добавить канал в workspace", Keyboard(
|
b.Edit("❌ Не удалось добавить канал в workspace", Keyboard(
|
||||||
Row(Button("← Назад", "back_to_select")),
|
Row(Button("← Назад", "back_to_select")),
|
||||||
))
|
))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Показываем успешное сообщение
|
successText := fmt.Sprintf(msgSuccessChooseWorkspace, project.ID, project.Title, project.Status)
|
||||||
successText := fmt.Sprintf("✅ Канал успешно добавлен!\n\n"+
|
kb := Keyboard(
|
||||||
"📊 Проект ID: %s\n"+
|
Row(Button("Мои проекты", "my_projects")),
|
||||||
"📝 Название: %s\n"+
|
Row(Button("Главное меню", "main_menu")),
|
||||||
"🏷 Статус: %s",
|
|
||||||
project.ID,
|
|
||||||
project.Title,
|
|
||||||
project.Status,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
b.Edit(successText, Keyboard(
|
b.Edit(successText, kb)
|
||||||
Row(Button("📋 Мои проекты", "my_projects")),
|
|
||||||
Row(Button("🏠 Главное меню", "main_menu")),
|
|
||||||
))
|
|
||||||
|
|
||||||
case data == "back_to_select":
|
case data == "back_to_select":
|
||||||
s.Enter(b, bot.EditMessage)
|
s.Enter(b, bot.EditMessage)
|
||||||
|
|||||||
@@ -96,3 +96,80 @@ func FormatPageInfo(currentPage, totalPages int) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf(" <i>(стр. %d/%d)</i>", currentPage+1, totalPages)
|
return fmt.Sprintf(" <i>(стр. %d/%d)</i>", currentPage+1, totalPages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ElementLayoutConfig конфигурация для автоматической раскладки элементов с пагинацией
|
||||||
|
type ElementLayoutConfig struct {
|
||||||
|
CurrentPage int // Текущая страница (0-indexed)
|
||||||
|
ItemsPerPage int // Элементов на странице
|
||||||
|
ItemsPerRow int // Элементов в одном ряду
|
||||||
|
EmptyButton *echotron.InlineKeyboardButton // Кнопка-заполнитель (опционально, по умолчанию " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildElementRows автоматически раскладывает элементы с пагинацией.
|
||||||
|
// Принимает все элементы, возвращает ряды для текущей страницы с правильной раскладкой.
|
||||||
|
// Автоматически заполняет пустыми кнопками если страниц > 1 (для консистентности высоты).
|
||||||
|
// Возвращает 2D массив кнопок готовый к добавлению в клавиатуру.
|
||||||
|
//
|
||||||
|
// Пример использования:
|
||||||
|
//
|
||||||
|
// config := ui.ElementLayoutConfig{
|
||||||
|
// CurrentPage: 0,
|
||||||
|
// ItemsPerPage: 6,
|
||||||
|
// ItemsPerRow: 2,
|
||||||
|
// }
|
||||||
|
// rows := ui.BuildElementRows(config, allButtons)
|
||||||
|
// keyboard = append(keyboard, rows...)
|
||||||
|
func BuildElementRows(config ElementLayoutConfig, allItems []echotron.InlineKeyboardButton) [][]echotron.InlineKeyboardButton {
|
||||||
|
if len(allItems) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Параметры по умолчанию
|
||||||
|
if config.ItemsPerPage <= 0 {
|
||||||
|
config.ItemsPerPage = 6
|
||||||
|
}
|
||||||
|
if config.ItemsPerRow <= 0 {
|
||||||
|
config.ItemsPerRow = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка-заполнитель по умолчанию
|
||||||
|
emptyButton := echotron.InlineKeyboardButton{Text: " ", CallbackData: "empty"}
|
||||||
|
if config.EmptyButton != nil {
|
||||||
|
emptyButton = *config.EmptyButton
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем границы страницы
|
||||||
|
totalPages := CalculatePages(len(allItems), config.ItemsPerPage)
|
||||||
|
start, end := GetPageBounds(config.CurrentPage, config.ItemsPerPage, len(allItems))
|
||||||
|
|
||||||
|
// Получаем элементы текущей страницы
|
||||||
|
pageItems := allItems[start:end]
|
||||||
|
|
||||||
|
// Если страниц >= 2, заполняем до ItemsPerPage пустыми кнопками
|
||||||
|
// Это обеспечивает одинаковую высоту клавиатуры на всех страницах
|
||||||
|
if totalPages >= 2 {
|
||||||
|
for len(pageItems) < config.ItemsPerPage {
|
||||||
|
pageItems = append(pageItems, emptyButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Раскладываем элементы по рядам
|
||||||
|
var rows [][]echotron.InlineKeyboardButton
|
||||||
|
for i := 0; i < len(pageItems); i += config.ItemsPerRow {
|
||||||
|
var row []echotron.InlineKeyboardButton
|
||||||
|
|
||||||
|
// Добавляем элементы в ряд
|
||||||
|
for j := 0; j < config.ItemsPerRow && i+j < len(pageItems); j++ {
|
||||||
|
row = append(row, pageItems[i+j])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если в ряду меньше элементов чем ItemsPerRow, заполняем пустыми
|
||||||
|
for len(row) < config.ItemsPerRow {
|
||||||
|
row = append(row, emptyButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user