new telegram bot
This commit is contained in:
274
telegram_bot/bot/bot.go
Normal file
274
telegram_bot/bot/bot.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"example.com/m/adapter/backend"
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Button(text, data string) echotron.InlineKeyboardButton {
|
||||
return echotron.InlineKeyboardButton{
|
||||
Text: text,
|
||||
CallbackData: data,
|
||||
}
|
||||
}
|
||||
|
||||
func Row(buttons ...echotron.InlineKeyboardButton) []echotron.InlineKeyboardButton { return buttons }
|
||||
|
||||
func Keyboard(rows ...[]echotron.InlineKeyboardButton) echotron.InlineKeyboardMarkup {
|
||||
return echotron.InlineKeyboardMarkup{
|
||||
InlineKeyboard: rows,
|
||||
}
|
||||
}
|
||||
|
||||
type Exec struct {
|
||||
handled bool
|
||||
transitioned bool
|
||||
}
|
||||
|
||||
type Bot struct {
|
||||
echotron.API
|
||||
ChatID int64
|
||||
exec *Exec
|
||||
CurrentState State
|
||||
Session *Session
|
||||
LastMessageID int // ID последнего сообщения с inline кнопками
|
||||
DefaultStateFactory func() State
|
||||
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{
|
||||
ChatID: chatID,
|
||||
API: echotron.NewAPI(token),
|
||||
Session: &Session{},
|
||||
DefaultStateFactory: defaultStateFactory,
|
||||
Backend: backendClient,
|
||||
GlobalCallbackRouter: globalCallbackRouter,
|
||||
}
|
||||
return bot
|
||||
}
|
||||
|
||||
func (b *Bot) SetState(s State, mode RenderMode) {
|
||||
log.Info().
|
||||
Int64("chat_id", b.ChatID).
|
||||
Str("from_state", fmt.Sprintf("%T", b.CurrentState)).
|
||||
Str("to_state", fmt.Sprintf("%T", s)).
|
||||
Str("mode", fmt.Sprintf("%v", mode)).
|
||||
Msg("State transition")
|
||||
|
||||
b.CurrentState = s
|
||||
b.exec.transitioned = true
|
||||
b.exec.handled = true
|
||||
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)
|
||||
} else {
|
||||
b.SendNew(text, keyboard)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) SendNew(text string, keyboard echotron.InlineKeyboardMarkup) {
|
||||
// Удаляем кнопки у предыдущего сообщения перед отправкой нового
|
||||
if b.LastMessageID != 0 {
|
||||
b.cleanupKeyboard(b.LastMessageID)
|
||||
}
|
||||
|
||||
res, err := b.SendMessage(text, b.ChatID, &echotron.MessageOptions{
|
||||
ReplyMarkup: keyboard,
|
||||
ParseMode: echotron.HTML,
|
||||
})
|
||||
b.exec.handled = true
|
||||
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("SendMessage failed")
|
||||
return
|
||||
}
|
||||
|
||||
if res.Result != nil {
|
||||
b.LastMessageID = res.Result.ID
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) Edit(text string, keyboard echotron.InlineKeyboardMarkup) {
|
||||
if b.LastMessageID != 0 {
|
||||
_, err := b.EditMessageText(
|
||||
text,
|
||||
echotron.NewMessageID(b.ChatID, b.LastMessageID),
|
||||
&echotron.MessageTextOptions{
|
||||
ReplyMarkup: keyboard,
|
||||
ParseMode: echotron.HTML,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("EditMessageText")
|
||||
}
|
||||
} else {
|
||||
b.SendNew(text, keyboard)
|
||||
}
|
||||
b.exec.handled = true
|
||||
}
|
||||
|
||||
func (b *Bot) SendMessageWithURLButton(text, buttonText, url string) {
|
||||
keyboard := echotron.InlineKeyboardMarkup{
|
||||
InlineKeyboard: [][]echotron.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: buttonText,
|
||||
URL: url,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
res, err := b.SendMessage(text, b.ChatID, &echotron.MessageOptions{
|
||||
ReplyMarkup: keyboard,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("SendMessageWithURLButton")
|
||||
}
|
||||
b.exec.handled = true
|
||||
|
||||
if err == nil && res.Result != nil {
|
||||
b.LastMessageID = res.Result.ID
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) Update(u *echotron.Update) {
|
||||
log.Info().
|
||||
Int64("chat_id", b.ChatID).
|
||||
Str("current_state", fmt.Sprintf("%T", b.CurrentState)).
|
||||
Msg("Update received")
|
||||
|
||||
// Обрабатываем системные события (добавление в канал, подписки и т.д.)
|
||||
if u.ChatJoinRequest != nil || u.ChatMember != nil || u.MyChatMember != nil {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
if chatType != "" && chatType != "private" {
|
||||
log.Debug().Str("chat_type", chatType).Msg("Ignoring event from non-private chat")
|
||||
return
|
||||
}
|
||||
|
||||
exec := &Exec{}
|
||||
|
||||
b.exec = exec
|
||||
defer func() { b.exec = nil }()
|
||||
|
||||
if b.CurrentState == nil && b.DefaultStateFactory != nil {
|
||||
b.CurrentState = b.DefaultStateFactory()
|
||||
}
|
||||
|
||||
if u.Message != nil && u.Message.Text == "/start" {
|
||||
if b.DefaultStateFactory != nil {
|
||||
b.SetState(b.DefaultStateFactory(), NewMessage)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if u.CallbackQuery != nil {
|
||||
b.cleanupCallbackUI(u)
|
||||
|
||||
if b.GlobalCallbackRouter != nil && u.CallbackQuery.Data != "" {
|
||||
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 {
|
||||
log.Info().Msg("Common handler")
|
||||
b.CurrentState.Handle(b, u)
|
||||
}
|
||||
|
||||
if !exec.handled && !exec.transitioned {
|
||||
log.Info().Msg("Default feedback")
|
||||
b.SetState(b.DefaultStateFactory(), NewMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) cleanupKeyboard(messageID int) {
|
||||
emptyKeyboard := Keyboard()
|
||||
_, err := b.EditMessageReplyMarkup(
|
||||
echotron.NewMessageID(b.ChatID, messageID),
|
||||
&echotron.MessageReplyMarkupOptions{ReplyMarkup: emptyKeyboard},
|
||||
)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Int("message_id", messageID).Msg("Failed to remove keyboard")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) cleanupCallbackUI(u *echotron.Update) {
|
||||
_, err := b.AnswerCallbackQuery(u.CallbackQuery.ID, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("b.AnswerCallbackQuery error")
|
||||
}
|
||||
|
||||
if u.CallbackQuery.Message != nil {
|
||||
callbackMessageID := u.CallbackQuery.Message.ID
|
||||
|
||||
if b.LastMessageID == callbackMessageID {
|
||||
return
|
||||
}
|
||||
|
||||
b.cleanupKeyboard(callbackMessageID)
|
||||
}
|
||||
}
|
||||
22
telegram_bot/bot/state.go
Normal file
22
telegram_bot/bot/state.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package bot
|
||||
|
||||
import "github.com/NicoNex/echotron/v3"
|
||||
|
||||
type RenderMode int
|
||||
|
||||
const (
|
||||
NewMessage RenderMode = iota
|
||||
EditMessage
|
||||
)
|
||||
|
||||
type State interface {
|
||||
Enter(*Bot, RenderMode)
|
||||
HandleCallback(*Bot, *echotron.Update)
|
||||
HandleMessage(*Bot, *echotron.Update)
|
||||
Handle(*Bot, *echotron.Update)
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
UserName string
|
||||
JWT string
|
||||
}
|
||||
Reference in New Issue
Block a user