diff --git a/bot.go b/bot.go index e8c9bef..5a84a4d 100644 --- a/bot.go +++ b/bot.go @@ -18,12 +18,14 @@ type Bot struct { prefixes []string updateOffset int + updateTypes []string updateQueue *Queue[*Update] } type MsgContext struct { Bot *Bot Msg *Message + Update *Update FromID int Prefix string Text string @@ -31,11 +33,11 @@ type MsgContext struct { func NewBot(token string) *Bot { logger := CreateLogger() - logger.SetLevel(DEBUG) + logger.Level(DEBUG) updateQueue := CreateQueue[*Update](256) bot := &Bot{ updateOffset: 0, plugins: make([]*Plugin, 0), debug: false, - prefixes: make([]string, 0), + prefixes: make([]string, 0), updateTypes: make([]string, 0), updateQueue: updateQueue, token: token, logger: logger, } @@ -43,8 +45,19 @@ func NewBot(token string) *Bot { return bot } -func (b *Bot) AddPrefixes(prefixes ...string) { +func (b *Bot) UpdateTypes(t ...string) *Bot { + b.updateTypes = make([]string, 0) + b.updateTypes = append(b.updateTypes, t...) + return b +} +func (b *Bot) AddUpdateType(t ...string) *Bot { + b.updateTypes = append(b.updateTypes, t...) + return b +} + +func (b *Bot) AddPrefixes(prefixes ...string) *Bot { b.prefixes = append(b.prefixes, prefixes...) + return b } func (b *Bot) Run() { @@ -58,9 +71,11 @@ func (b *Bot) Run() { return } + b.logger.Info("Bot running. Press CTRL+C to exit.") + go func() { for { - _, err := b.GetUpdates() + _, err := b.Updates() if err != nil { b.logger.Error(err) } @@ -68,44 +83,86 @@ func (b *Bot) Run() { }() for { - queue := b.GetQueue() + queue := b.updateQueue if queue.IsEmpty() { continue } u := queue.Dequeue() - b.handleMessage(u) + if u.CallbackQuery != nil { + b.handleCallback(u) + } else { + b.handleMessage(u) + } } } +// {"callback_query":{"chat_instance":"6202057960757700762","data":"aboba","from":{"first_name":"scuroneko","id":314834933,"is_bot":false,"language_code":"ru","username":"scuroneko"},"id":"1352205741990111553","message":{"chat":{"first_name":"scuroneko","id":314834933,"type":"private","username":"scuroneko"},"date":1734338107,"from":{"first_name":"Kurumi","id":7718900880,"is_bot":true,"username":"kurumi_game_bot"},"message_id":19,"reply_markup":{"inline_keyboard":[[{"callback_data":"aboba","text":"Test"},{"callback_data":"another","text":"Another"}]]},"text":"Aboba"}},"update_id":350979488} + func (b *Bot) handleMessage(update *Update) { + ctx := &MsgContext{ + Bot: b, + Update: update, + } + + for _, plugin := range b.plugins { + if plugin.UpdateListener != nil { + (*plugin.UpdateListener)(ctx) + } + } + + if update.Message == nil { + return + } + + ctx.FromID = update.Message.From.ID + ctx.Msg = update.Message text := strings.TrimSpace(update.Message.Text) prefix, hasPrefix := b.checkPrefixes(text) if !hasPrefix { return } + ctx.Prefix = prefix text = strings.TrimSpace(text[len(prefix):]) for _, plugin := range b.plugins { - for cmd, _ := range plugin.Commands { + + // Check every command + for cmd := range plugin.Commands { if !strings.HasPrefix(text, cmd) { continue } - ctx := &MsgContext{ - Bot: b, - FromID: update.Message.From.ID, - Msg: update.Message, - Prefix: prefix, - Text: text[len(cmd):], - } + ctx.Text = text[len(cmd):] plugin.Execute(cmd, ctx) } } } +func (b *Bot) handleCallback(update *Update) { + ctx := &MsgContext{ + Bot: b, + Update: update, + } + + for _, plugin := range b.plugins { + if plugin.UpdateListener != nil { + (*plugin.UpdateListener)(ctx) + } + } + + for _, plugin := range b.plugins { + for payload := range plugin.Payloads { + if !strings.HasPrefix(update.CallbackQuery.Data, payload) { + continue + } + plugin.ExecutePayload(payload, ctx) + } + } +} + func (b *Bot) checkPrefixes(text string) (string, bool) { for _, prefix := range b.prefixes { if strings.HasPrefix(text, prefix) { @@ -115,16 +172,14 @@ func (b *Bot) checkPrefixes(text string) (string, bool) { return "", false } -func (b *Bot) AddPlugins(plugin ...*Plugin) { +func (b *Bot) AddPlugins(plugin ...*Plugin) *Bot { b.plugins = append(b.plugins, plugin...) + return b } -func (b *Bot) SetDebug(debug bool) { +func (b *Bot) Debug(debug bool) *Bot { b.debug = debug -} - -func (b *Bot) GetQueue() *Queue[*Update] { - return b.updateQueue + return b } func (ctx *MsgContext) Answer(text string) { @@ -137,6 +192,10 @@ func (ctx *MsgContext) Answer(text string) { } } +func (b *Bot) Logger() *Logger { + return b.logger +} + type ApiResponse struct { Ok bool `json:"ok"` Result map[string]interface{} `json:"result,omitempty"` @@ -160,7 +219,12 @@ func (b *Bot) request(methodName string, params map[string]interface{}) (map[str } if b.debug { - b.logger.Debug(fmt.Sprintf("POST https://api.telegram.org/bot%s/%s %s", "", methodName, string(buf.Bytes()))) + b.logger.Debug(fmt.Sprintf( + "POST https://api.telegram.org/bot%s/%s %s", + "", + methodName, + string(buf.Bytes()), + )) } r, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/%s", b.token, methodName), "application/json", &buf) if err != nil { diff --git a/go.mod b/go.mod index 4fd6002..d0179ee 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.nix13.pw/ScuroNeko/Laniakea +module git.nix13.pw/ScuroNeko/laniakea go 1.23.4 diff --git a/keyboard.go b/keyboard.go new file mode 100644 index 0000000..edc15f0 --- /dev/null +++ b/keyboard.go @@ -0,0 +1,4 @@ +package laniakea + +type InlineKeyboard struct { +} diff --git a/logger.go b/logger.go index 26ee277..648fd7d 100644 --- a/logger.go +++ b/logger.go @@ -2,7 +2,6 @@ package laniakea import ( "fmt" - "net/http" "runtime" "strings" "time" @@ -40,17 +39,6 @@ var ( DEBUG LogLevel = LogLevel{n: 4, t: "debug", c: color.FgGreen} ) -var ( - GET LogLevel = LogLevel{n: 0, t: "get", c: color.FgWhite} - POST LogLevel = LogLevel{n: 0, t: "post", c: color.FgBlue} - PUT LogLevel = LogLevel{n: 0, t: "put", c: color.FgYellow} - DELETE LogLevel = LogLevel{n: 0, t: "delete", c: color.FgHiRed} -) - -var methodLevelMap = map[string]LogLevel{ - "get": GET, "post": POST, "put": PUT, "delete": DELETE, -} - func CreateLogger() *Logger { return &Logger{ prefix: "LOG", @@ -60,41 +48,34 @@ func CreateLogger() *Logger { } } -func (l *Logger) GetHTTPLogger(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - level := methodLevelMap[r.Method] - color.New(level.c).Println(l.buildString(methodLevelMap[strings.ToLower(r.Method)], "Hello")) - next.ServeHTTP(w, r) - }) -} - -func (l *Logger) SetPrefix(prefix string) { +func (l *Logger) Prefix(prefix string) { l.prefix = prefix } -func (l *Logger) SetLevel(level LogLevel) { +func (l *Logger) Level(level LogLevel) { l.level = level } -func (l *Logger) SetPrintTraceback(b bool) { +func (l *Logger) PrintTraceback(b bool) { l.printTraceback = b } -func (l *Logger) Info(m interface{}) { +func (l *Logger) Info(m ...any) { l.print(INFO, m) } -func (l *Logger) Warn(m interface{}) { +func (l *Logger) Warn(m ...any) { l.print(WARN, m) } -func (l *Logger) Error(m interface{}) { +func (l *Logger) Error(m ...any) { l.print(ERROR, m) } -func (l *Logger) Fatal(m interface{}) { +func (l *Logger) Fatal(m ...any) { l.print(FATAL, m) + panic(m) } -func (l *Logger) Debug(m interface{}) { +func (l *Logger) Debug(m ...any) { l.print(DEBUG, m) } @@ -127,7 +108,7 @@ func (l *Logger) formatTraceback(mt *MethodTraceback) string { return fmt.Sprintf("%s:%s:%d", mt.filename, mt.Method, mt.line) } -func (l *Logger) buildString(level LogLevel, m interface{}) string { +func (l *Logger) buildString(level LogLevel, m []any) string { args := []string{ fmt.Sprintf("[%s]", l.prefix), fmt.Sprintf("[%s]", strings.ToUpper(level.t)), @@ -141,12 +122,20 @@ func (l *Logger) buildString(level LogLevel, m interface{}) string { args = append(args, fmt.Sprintf("[%s]", l.formatTime(time.Now()))) } - return fmt.Sprintf("%s %v", strings.Join(args, " "), m) + msg := Map(m, func(el any) string { + return fmt.Sprintf("%v", el) + }) + + return fmt.Sprintf("%s %v", strings.Join(args, " "), strings.Join(msg, " ")) } -func (l *Logger) print(level LogLevel, m interface{}) { +func (l *Logger) print(level LogLevel, m []any) { if l.level.n < level.n { return } - color.New(level.c).Print(l.buildString(level, m)) + if level == INFO { + color.New(level.c).Println(l.buildString(level, m)) + } else { + color.New(level.c).Print(l.buildString(level, m)) + } } diff --git a/methods.go b/methods.go index 5315b1c..aa61616 100644 --- a/methods.go +++ b/methods.go @@ -4,10 +4,11 @@ import "fmt" var NO_PARAMS = make(map[string]interface{}) -func (b *Bot) GetUpdates() ([]*Update, error) { +func (b *Bot) Updates() ([]*Update, error) { params := make(map[string]interface{}) params["offset"] = b.updateOffset params["timeout"] = 30 + params["allowed_updates"] = b.updateTypes data, err := b.request("getUpdates", params) if err != nil { @@ -38,9 +39,31 @@ func (b *Bot) GetUpdates() ([]*Update, error) { return res, err } +func (b *Bot) GetMe() (*User, error) { + data, err := b.request("getMe", NO_PARAMS) + if err != nil { + return nil, err + } + user := new(User) + err = MapToStruct(data, user) + return user, err +} + type SendMessageP struct { - ChatID int `json:"chat_id"` - Text string `json:"text"` + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id"` + MessageThreadID int `json:"message_thread_id,omitempty"` + Text string `json:"text"` + ParseMode string `json:"parse_mode,omitempty"` + Entities []*MessageEntity `json:"entities,omitempty"` + LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` + DisableNotifications bool `json:"disable_notifications,omitempty"` + ProtectContent bool `json:"protect_content,omitempty"` + AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"` + MessageEffectID string `json:"message_effect_id,omitempty"` + ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"` + InlineKeyboardMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // ReplyKeyboardMarkup *ReplyKeyboardMarkup `json:"reply_markup,omitempty"` } func (b *Bot) SendMessage(params *SendMessageP) (*Message, error) { diff --git a/plugins.go b/plugins.go index fe279b9..0d3dc00 100644 --- a/plugins.go +++ b/plugins.go @@ -3,31 +3,43 @@ package laniakea type CommandExecutor func(ctx *MsgContext) type PluginBuilder struct { - name string - commands map[string]*CommandExecutor - payload string + name string + commands map[string]*CommandExecutor + payloads map[string]*CommandExecutor + updateListener *CommandExecutor } type Plugin struct { - Name string - Commands map[string]*CommandExecutor - Payload string + Name string + Commands map[string]*CommandExecutor + Payloads map[string]*CommandExecutor + UpdateListener *CommandExecutor } func NewPlugin(name string) *PluginBuilder { return &PluginBuilder{ name: name, commands: make(map[string]*CommandExecutor), + payloads: make(map[string]*CommandExecutor), } } -func (p *PluginBuilder) Command(cmd string, f CommandExecutor) *PluginBuilder { - p.commands[cmd] = &f +func (p *PluginBuilder) Command(f CommandExecutor, cmd ...string) *PluginBuilder { + for _, c := range cmd { + p.commands[c] = &f + } return p } -func (p *PluginBuilder) Payload(payload string) *PluginBuilder { - p.payload = payload +func (p *PluginBuilder) Payload(f CommandExecutor, payloads ...string) *PluginBuilder { + for _, payload := range payloads { + p.payloads[payload] = &f + } + return p +} + +func (p *PluginBuilder) UpdateListener(listener CommandExecutor) *PluginBuilder { + p.updateListener = &listener return p } @@ -36,9 +48,10 @@ func (p *PluginBuilder) Build() *Plugin { return nil } plugin := &Plugin{ - Name: p.name, - Commands: p.commands, - Payload: p.payload, + Name: p.name, + Commands: p.commands, + Payloads: p.payloads, + UpdateListener: p.updateListener, } return plugin } @@ -46,3 +59,7 @@ func (p *PluginBuilder) Build() *Plugin { func (p *Plugin) Execute(cmd string, ctx *MsgContext) { (*p.Commands[cmd])(ctx) } + +func (p *Plugin) ExecutePayload(payload string, ctx *MsgContext) { + (*p.Payloads[payload])(ctx) +} diff --git a/types.go b/types.go index a79e33b..66885b8 100644 --- a/types.go +++ b/types.go @@ -1,8 +1,20 @@ package laniakea type Update struct { - UpdateID int `json:"update_id"` - Message *Message `json:"message"` + UpdateID int `json:"update_id"` + Message *Message `json:"message"` + EditedMessage *Message `json:"edited_message,omitempty"` + ChannelPost *Message `json:"channel_post,omitempty"` + EditedChannelPost *Message `json:"edited_channel_post,omitempty"` + BusinessConnection *BusinessConnection `json:"business_connection,omitempty"` + BusinessMessage *Message `json:"business_message,omitempty"` + EditedBusinessMessage *Message `json:"edited_business_message,omitempty"` + DeletedBusinessMessage *Message `json:"deleted_business_messages,omitempty"` + MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"` + MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` + InlineQuery int + ChosenInlineResult int + CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` } type User struct { @@ -38,3 +50,109 @@ type Message struct { Chat *Chat `json:"chat,omitempty"` Text string `json:"text"` } + +type InaccessableMessage struct { + Chat *Chat `json:"chat"` + MessageID int `json:"message_id"` + Date int `json:"date"` +} + +type MaybeInaccessibleMessage struct { + Message + InaccessableMessage +} + +type MessageEntity struct { + Type string `json:"type"` + Offset int `json:"offset"` + Length int `json:"length"` + URL string `json:"url,omitempty"` + User *User `json:"user,omitempty"` + Language string `json:"language,omitempty"` + CustomEmojiID string `json:"custom_emoji_id,omitempty"` +} + +type ReplyParameters struct { + MessageID int `json:"message_id"` + ChatID int `json:"chat_id,omitempty"` + AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"` + Quote string `json:"quote,omitempty"` + QuoteParsingMode string `json:"quote_parsing_mode,omitempty"` + QuoteEntities []*MessageEntity `json:"quote_entities,omitempty"` + QuotePosition int `json:"quote_postigin,omitempty"` +} + +type LinkPreviewOptions struct { + IsDisabled bool `json:"is_disabled,omitempty"` + URL string `json:"url,omitempty"` + PreferSmallMedia bool `json:"prefer_small_media,omitempty"` + PreferLargeMedia bool `json:"prefer_large_media,omitempty"` + ShowAboveText bool `json:"show_above_text,omitempty"` +} + +type InlineKeyboardMarkup struct { + InlineKeyboard [][]*InlineKeyboardButton `json:"inline_keyboard"` +} + +type InlineKeyboardButton struct { + Text string `json:"text"` + URL string `json:"url,omitempty"` + CallbackData string `json:"callback_data,omitempty"` +} + +type ReplyKeyboardMarkup struct { + Keyboard [][]int `json:"keyboard"` +} + +type CallbackQuery struct { + ID string `json:"id"` + From *User `json:"user"` + Message *MaybeInaccessibleMessage `json:"message"` + + Data string `json:"data"` +} + +type BusinessConnection struct { + ID string `json:"id"` + User *User `json:"user"` + UserChatID int `json:"user_chat_id"` + Date int `json:"date"` + CanReply bool `json:"can_reply"` + IsEnabled bool `json:"id_enabled"` +} + +type MessageReactionUpdated struct { + Chat *Chat `json:"chat"` + MessageID int `json:"message_id"` + User *User `json:"user,omitempty"` + ActorChat *Chat `json:"actor_chat"` + Date int `json:"date"` + OldReaction []*ReactionType `json:"old_reaction"` + NewReaction []*ReactionType `json:"new_reaction"` +} + +type MessageReactionCountUpdated struct { + Chat *Chat `json:"chat"` + MessageID int `json:"message_id"` + Date int `json:"date"` + Reactions []*ReactionCount `json:"reactions"` +} + +type ReactionType struct { + Type string `json:"type"` +} +type ReactionTypeEmoji struct { + ReactionType + Emoji string `json:"emoji"` +} +type ReactionTypeCustomEmoji struct { + ReactionType + CustomEmojiID string `json:"custom_emoji_id"` +} +type ReactionTypePaid struct { + ReactionType +} +type ReactionCount struct { + Type *ReactionType `json:"type"` + TotalCount int `json:"total_count"` +} diff --git a/utils.go b/utils.go index 8ebdec3..e8c6385 100644 --- a/utils.go +++ b/utils.go @@ -25,3 +25,11 @@ func StructToMap(s interface{}) (map[string]interface{}, error) { err = json.Unmarshal(data, &m) return m, err } + +func Map[T, V any](ts []T, fn func(T) V) []V { + result := make([]V, len(ts)) + for i, t := range ts { + result[i] = fn(t) + } + return result +}