фиксы

This commit is contained in:
Artem Tsyrulnikov
2026-01-08 21:50:04 +03:00
parent 3e5bdb8164
commit eb54b01835
20 changed files with 462 additions and 405 deletions

View File

@@ -19,6 +19,13 @@ func Button(text, data string) echotron.InlineKeyboardButton {
}
}
func URLButton(text, url string) echotron.InlineKeyboardButton {
return echotron.InlineKeyboardButton{
Text: text,
URL: url,
}
}
func Row(buttons ...echotron.InlineKeyboardButton) []echotron.InlineKeyboardButton { return buttons }
func Keyboard(rows ...[]echotron.InlineKeyboardButton) echotron.InlineKeyboardMarkup {
@@ -44,28 +51,34 @@ type Bot struct {
exec *Exec
CurrentState State
Session *Session
LastMessageID int // ID последнего сообщения с inline кнопками
DefaultStateFactory func() State
LastMessageID int // ID последнего сообщения с inline кнопками
commandRouter func(cmd string) State // Роутер для команд (например, /start, /help)
globalCallbackRouter func(callbackData string) State // Роутер для глобальных callbacks из уведомлений
Backend *backend.Client
GlobalCallbackRouter func(callbackData string) State // Роутер для глобальных callbacks из уведомлений
}
func NewBot(
chatID int64,
token string,
defaultStateFactory func() State,
backendClient *backend.Client,
globalCallbackRouter func(string) State,
) *Bot {
bot := &Bot{
func NewBot(chatID int64, token string, commandRouter func(string) State, backendClient *backend.Client, globalCallbackRouter func(string) State) *Bot {
if commandRouter == nil {
panic("bot: commandRouter cannot be nil")
}
if globalCallbackRouter == nil {
panic("bot: globalCallbackRouter cannot be nil")
}
if commandRouter("/start") == nil {
panic("bot: commandRouter requires /start state answer")
}
if backendClient == nil {
panic("bot: backendClient cannot be nil")
}
return &Bot{
ChatID: chatID,
API: echotron.NewAPI(token),
Session: &Session{},
DefaultStateFactory: defaultStateFactory,
commandRouter: commandRouter,
Backend: backendClient,
GlobalCallbackRouter: globalCallbackRouter,
globalCallbackRouter: globalCallbackRouter,
}
return bot
}
// GetOrCreateJWT получает JWT токен, передавая актуальные данные пользователя из update
@@ -129,19 +142,6 @@ func (b *Bot) SetState(s State, mode RenderMode) {
s.Enter(b, mode)
}
//func (b *Bot) RemoveLastKeyboard() {
// if b.LastMessageID != 0 {
// emptyKeyboard := Keyboard() // Пустая клавиатура
// _, err := b.EditMessageReplyMarkup(
// echotron.NewMessageID(b.ChatID, b.LastMessageID),
// &echotron.MessageReplyMarkupOptions{ReplyMarkup: emptyKeyboard},
// )
// if err != nil {
// log.Error().Err(err).Msg("RemoveLastKeyboard: b.EditMessageReplyMarkup")
// }
// }
//}
func (b *Bot) Render(text string, keyboard echotron.InlineKeyboardMarkup, mode RenderMode) {
if mode == EditMessage {
b.Edit(text, keyboard)
@@ -262,126 +262,104 @@ func (b *Bot) Update(u *echotron.Update) {
Bool("has_edited_message", u.EditedMessage != nil).
Msg("Update received")
// Обрабатываем системные события (добавление в канал, подписки и т.д.)
// 1. Системные события (ChatJoinRequest, ChatMember, etc.) -> форвардим асинхронно
if u.ChatJoinRequest != nil || u.ChatMember != nil || u.MyChatMember != nil {
go func() {
ctx := context.Background()
if raw, err := json.Marshal(u); err == nil {
log.Info().RawJSON("update", raw).Msg("System event payload")
} else {
log.Error().Err(err).Msg("Failed to marshal system event payload")
raw, err := json.Marshal(u)
if err != nil {
log.Error().RawJSON("update", raw).Msg("Failed to marshal update")
}
err := b.Backend.ForwardTelegramUpdate(ctx, u)
err = b.Backend.ForwardTelegramUpdate(ctx, u)
if err != nil {
log.Error().Err(err).Msg("Failed to forward system event to backend")
return
}
log.Info().Msg("System event forwarded to backend")
}()
// Системные события не требуют ответа бота - выходим
return
}
// Игнорируем все события не из личных чатов (кроме уже обработанных системных)
var chatType string
// 2. Игнорируем события не из private чатов
chatType := ""
switch {
case u.Message != nil:
chatType = u.Message.Chat.Type
case u.EditedMessage != nil:
chatType = u.EditedMessage.Chat.Type
case u.ChannelPost != nil:
chatType = u.ChannelPost.Chat.Type
case u.EditedChannelPost != nil:
chatType = u.EditedChannelPost.Chat.Type
case u.CallbackQuery != nil && u.CallbackQuery.Message != nil:
chatType = u.CallbackQuery.Message.Chat.Type
case u.EditedMessage != nil:
chatType = u.EditedMessage.Chat.Type
case u.ChannelPost != nil || u.EditedChannelPost != nil:
return // Каналы игнорируем сразу
}
if chatType != "" && chatType != "private" {
log.Debug().Str("chat_type", chatType).Msg("Ignoring event from non-private chat")
log.Debug().Str("chat_type", chatType).Msg("Ignoring non-private chat")
return
}
// Игнорируем edited_message обновления, чтобы не сбрасывать состояние после наших Edit операций.
// 3. Игнорируем edited messages
if u.EditedMessage != nil || u.EditedChannelPost != nil || u.EditedBusinessMessage != nil {
log.Debug().Msg("Ignoring edited message update")
log.Debug().Msg("Ignoring edited message")
return
}
// Получаем или создаем JWT, обновляя данные пользователя (username, first_name, last_name)
if err := b.GetOrCreateJWT(u); err != nil {
log.Error().Err(err).Msg("Failed to get or create JWT")
// Не блокируем работу бота если JWT не удалось получить - это не критично для всех действий
}
_ = b.GetOrCreateJWT(u)
exec := &Exec{}
b.exec = exec
defer func() { b.exec = nil }()
if b.CurrentState == nil && b.DefaultStateFactory != nil {
b.CurrentState = b.DefaultStateFactory()
if b.CurrentState == nil {
b.CurrentState = b.commandRouter("/start")
}
if u.Message != nil && u.Message.Text == "/start" {
if b.DefaultStateFactory != nil {
b.SetState(b.DefaultStateFactory(), NewMessage)
}
return
}
// === Обработка входящего события ===
if u.CallbackQuery != nil {
log.Info().
Str("callback_data", u.CallbackQuery.Data).
Int("callback_msg_id", func() int {
if u.CallbackQuery.Message != nil {
return u.CallbackQuery.Message.ID
}
return 0
}()).
Int("last_msg_id", b.LastMessageID).
Msg("Callback received")
b.cleanupCallbackUI(u)
if u.CallbackQuery.Data == "empty" || u.CallbackQuery.Data == "page_info" || u.CallbackQuery.Data == "noop" {
exec.handled = true
// 1. Команды (приоритетный маршрут)
if u.Message != nil {
switch u.Message.Text {
case "/start":
b.SetState(b.commandRouter("/start"), NewMessage)
return
}
if b.GlobalCallbackRouter != nil && u.CallbackQuery.Data != "" {
if newState := b.GlobalCallbackRouter(u.CallbackQuery.Data); newState != nil {
b.SetState(newState, NewMessage)
case "/start login":
b.SetState(b.commandRouter("/start login"), NewMessage)
return
default:
b.CurrentState.HandleMessage(b, u)
if exec.handled || exec.transitioned {
return
}
}
}
// 2. Callback запросы
if u.CallbackQuery != nil {
b.cleanupCallbackUI(u)
if u.CallbackQuery.Data == "empty" {
return
}
// Глобальные callbacks (из уведомлений)
if newState := b.globalCallbackRouter(u.CallbackQuery.Data); newState != nil {
b.SetState(newState, NewMessage)
return
}
log.Info().Msg("Handling callback")
b.CurrentState.HandleCallback(b, u)
} else if u.Message != nil {
log.Info().Msg("Handling message")
b.CurrentState.HandleMessage(b, u)
if exec.handled || exec.transitioned {
return
}
}
if !exec.handled && !exec.transitioned {
log.Info().
Bool("has_callback", u.CallbackQuery != nil).
Bool("has_message", u.Message != nil).
Bool("handled", exec.handled).
Bool("transitioned", exec.transitioned).
Msg("No handler handled update, calling common handler")
log.Info().Msg("Common handler")
b.CurrentState.Handle(b, u)
// 3. Универсальный обработчик состояния (если ничего не подошло)
b.CurrentState.Handle(b, u)
if exec.handled || exec.transitioned {
return
}
if !exec.handled && !exec.transitioned {
log.Info().
Bool("has_callback", u.CallbackQuery != nil).
Bool("has_message", u.Message != nil).
Bool("handled", exec.handled).
Bool("transitioned", exec.transitioned).
Msg("Default feedback triggered")
log.Info().Msg("Default feedback")
b.SetState(b.DefaultStateFactory(), NewMessage)
}
// 4. Fallback - сброс в начальное состояние
b.SetState(b.commandRouter("/start"), NewMessage)
}
func (b *Bot) cleanupKeyboard(messageID int) {