commit 13eb3d45de16c3d41d45b1e0726210e467c20d61 Author: ScuroNeko Date: Wed Aug 27 23:20:15 2025 +0300 initial commit diff --git a/bot.go b/bot.go new file mode 100644 index 0000000..3b9c700 --- /dev/null +++ b/bot.go @@ -0,0 +1,355 @@ +package laniakea + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +type ParseMode string + +const ( + ParseMDV2 ParseMode = "MarkdownV2" + ParseHTML ParseMode = "HTML" + ParseMD ParseMode = "Markdown" +) + +type Bot struct { + token string + debug bool + errorTemplate string + + logger *Logger + requestLogger *Logger + + plugins []*Plugin + prefixes []string + + updateOffset int + updateTypes []string + updateQueue *Queue[*Update] +} + +type BotSettings struct { + Token string + Debug bool + ErrorTemplate string + Prefixes []string + UpdateTypes []string + LoggerBasePath string + UseRequestLogger bool +} + +func LoadSettingsFromEnv() *BotSettings { + +} + +type MsgContext struct { + Bot *Bot + Msg *Message + Update *Update + FromID int + Prefix string + Text string + Args []string +} + +func NewBot(settings *BotSettings) *Bot { + updateQueue := CreateQueue[*Update](256) + bot := &Bot{ + updateOffset: 0, plugins: make([]*Plugin, 0), debug: settings.Debug, errorTemplate: "%s", + prefixes: settings.Prefixes, updateTypes: make([]string, 0), + updateQueue: updateQueue, + token: settings.Token, + } + + if len(settings.ErrorTemplate) > 0 { + bot.errorTemplate = settings.ErrorTemplate + } + + if len(settings.LoggerBasePath) == 0 { + settings.LoggerBasePath = "./" + } + level := FATAL + if settings.Debug { + level = DEBUG + } + bot.logger = CreateLogger().Level(level).OpenFile(fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + if settings.UseRequestLogger { + bot.requestLogger = CreateLogger().Level(level).Prefix("REQUESTS").OpenFile(fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + } + + return bot +} + +func (b *Bot) Close() { + err := b.logger.f.Close() + if err != nil { + fmt.Println(err) + } else { + fmt.Println("log closed") + } +} + +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 LoadPrefixesFromEnv() []string { + prefixesS, exists := os.LookupEnv("PREFIXES") + if !exists { + return []string{"!"} + } + return strings.Split(prefixesS, ";") +} + +func (b *Bot) ErrorTemplate(s string) *Bot { + b.errorTemplate = s + return b +} + +func (b *Bot) Debug(debug bool) *Bot { + b.debug = debug + return b +} + +func (b *Bot) AddPlugins(plugin ...*Plugin) *Bot { + b.plugins = append(b.plugins, plugin...) + for _, p := range plugin { + b.logger.Debug(fmt.Sprintf("plugins with name \"%s\" was registered", p.Name)) + } + return b +} + +func (b *Bot) Run() { + if len(b.prefixes) == 0 { + b.logger.Fatal("no prefixes defined") + return + } + + if len(b.plugins) == 0 { + b.logger.Fatal("no plugins defined") + return + } + + b.logger.Info("Bot running. Press CTRL+C to exit.") + + go func() { + for { + _, err := b.Updates() + if err != nil { + b.logger.Error(err) + } + } + }() + + for { + queue := b.updateQueue + if queue.IsEmpty() { + continue + } + + u := queue.Dequeue() + 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) + } + } + + var text string + if update.Message == nil { + return + } + if len(update.Message.Text) > 0 { + text = update.Message.Text + } else { + text = update.Message.Caption + } + + ctx.FromID = update.Message.From.ID + ctx.Msg = update.Message + text = strings.TrimSpace(text) + prefix, hasPrefix := b.checkPrefixes(text) + if !hasPrefix { + return + } + ctx.Prefix = prefix + + text = strings.TrimSpace(text[len(prefix):]) + + for _, plugin := range b.plugins { + + // Check every command + for cmd := range plugin.Commands { + if !strings.HasPrefix(text, cmd) { + continue + } + + ctx.Text = strings.TrimSpace(text[len(cmd):]) + ctx.Args = strings.Split(ctx.Text, " ") + + go 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 + } + go plugin.ExecutePayload(payload, ctx) + } + } +} + +func (b *Bot) checkPrefixes(text string) (string, bool) { + for _, prefix := range b.prefixes { + if strings.HasPrefix(text, prefix) { + return prefix, true + } + } + return "", false +} + +func (ctx *MsgContext) Answer(text string) { + _, err := ctx.Bot.SendMessage(&SendMessageP{ + ChatID: ctx.Msg.Chat.ID, + Text: text, + ParseMode: "markdown", + }) + if err != nil { + ctx.Bot.logger.Error(err) + } +} + +func (ctx *MsgContext) AnswerPhoto(photoId string, text string) { + _, err := ctx.Bot.SendPhoto(&SendPhotoP{ + ChatID: ctx.Msg.Chat.ID, + Caption: text, + Photo: photoId, + ParseMode: ParseMD, + }) + if err != nil { + ctx.Bot.logger.Error(err) + } +} + +func (ctx *MsgContext) Error(err error) { + _, sendErr := ctx.Bot.SendMessage(&SendMessageP{ + ChatID: ctx.Msg.Chat.ID, + Text: fmt.Sprintf(ctx.Bot.errorTemplate, err.Error()), + }) + + if sendErr != nil { + ctx.Bot.logger.Error(sendErr) + } +} + +func (b *Bot) Logger() *Logger { + return b.logger +} + +type ApiResponse struct { + Ok bool `json:"ok"` + Result map[string]interface{} `json:"result,omitempty"` + Description string `json:"description,omitempty"` + ErrorCode int `json:"error_code,omitempty"` +} + +type ApiResponseA struct { + Ok bool `json:"ok"` + Result []interface{} `json:"result,omitempty"` + Description string `json:"description,omitempty"` + ErrorCode int `json:"error_code,omitempty"` +} + +// request is a low-level call to api. +func (b *Bot) request(methodName string, params map[string]interface{}) (map[string]interface{}, error) { + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(params) + if err != nil { + return nil, err + } + + if b.debug && b.requestLogger != nil { + b.requestLogger.Debug(strings.ReplaceAll(fmt.Sprintf( + "POST https://api.telegram.org/bot%s/%s %s", + "", + methodName, + buf.String(), + ), "\n", "")) + } + r, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/%s", b.token, methodName), "application/json", &buf) + if err != nil { + return nil, err + } + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + response := new(ApiResponse) + + var result map[string]interface{} + + err = json.Unmarshal(data, &response) + if err != nil { + responseArray := new(ApiResponseA) + err = json.Unmarshal(data, responseArray) + if err != nil { + return nil, err + } + result = map[string]interface{}{ + "data": responseArray.Result, + } + } else { + result = response.Result + } + if !response.Ok { + return nil, fmt.Errorf("[%d] %s", response.ErrorCode, response.Description) + } + return result, err +} 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 new file mode 100644 index 0000000..2960fde --- /dev/null +++ b/logger.go @@ -0,0 +1,162 @@ +package laniakea + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/fatih/color" +) + +type Logger struct { + prefix string + level LogLevel + printTraceback bool + printTime bool + + f *os.File +} + +type LogLevel struct { + n uint8 + t string + c color.Attribute +} + +type MethodTraceback struct { + Package string + Method string + fullPath string + signature string + filename string + line int +} + +var ( + INFO LogLevel = LogLevel{n: 0, t: "info", c: color.FgWhite} + WARN LogLevel = LogLevel{n: 1, t: "warn", c: color.FgHiYellow} + ERROR LogLevel = LogLevel{n: 2, t: "error", c: color.FgHiRed} + FATAL LogLevel = LogLevel{n: 3, t: "fatal", c: color.FgRed} + DEBUG LogLevel = LogLevel{n: 4, t: "debug", c: color.FgGreen} +) + +func CreateLogger() *Logger { + return &Logger{ + prefix: "LOG", + level: FATAL, + printTraceback: false, + printTime: true, + } +} + +func (l *Logger) OpenFile(name string) *Logger { + err := os.MkdirAll(filepath.Dir(name), os.ModePerm) + if err != nil { + l.Fatal(err) + } + l.f, err = os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + l.Fatal(err) + } + return l +} + +func (l *Logger) Prefix(prefix string) *Logger { + l.prefix = prefix + return l +} +func (l *Logger) Level(level LogLevel) *Logger { + l.level = level + return l +} +func (l *Logger) PrintTraceback(b bool) *Logger { + l.printTraceback = b + return l +} + +func (l *Logger) Info(m ...any) { + l.print(INFO, m) +} + +func (l *Logger) Warn(m ...any) { + l.print(WARN, m) +} + +func (l *Logger) Error(m ...any) { + l.print(ERROR, m) +} + +func (l *Logger) Fatal(m ...any) { + l.print(FATAL, m) + os.Exit(1) +} + +func (l *Logger) Debug(m ...any) { + l.print(DEBUG, m) +} + +func (l *Logger) formatTime(t time.Time) string { + return fmt.Sprintf("%02d.%02d.%02d %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second()) +} + +func (l *Logger) getTraceback() *MethodTraceback { + caller, _, _, _ := runtime.Caller(4) + details := runtime.FuncForPC(caller) + signature := details.Name() + path, line := details.FileLine(caller) + splitPath := strings.Split(path, "/") + + splitSignature := strings.Split(signature, ".") + pkg, method := splitSignature[0], splitSignature[len(splitSignature)-1] + + tb := &MethodTraceback{ + filename: splitPath[len(splitPath)-1], + fullPath: path, + line: line, + signature: signature, + Package: pkg, + Method: method, + } + + return tb +} +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 []any) string { + args := []string{ + fmt.Sprintf("[%s]", l.prefix), + fmt.Sprintf("[%s]", strings.ToUpper(level.t)), + } + + if l.printTraceback { + args = append(args, fmt.Sprintf("[%s]", l.formatTraceback(l.getTraceback()))) + } + + if l.printTime { + args = append(args, fmt.Sprintf("[%s]", l.formatTime(time.Now()))) + } + + 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 []any) { + if l.level.n < level.n { + return + } + color.New(level.c).Println(l.buildString(level, m)) + + if l.f != nil { + if _, err := l.f.Write([]byte(l.buildString(level, m) + "\n")); err != nil { + l.Fatal(err) + } + } +} diff --git a/methods.go b/methods.go new file mode 100644 index 0000000..2793a48 --- /dev/null +++ b/methods.go @@ -0,0 +1,111 @@ +package laniakea + +import "fmt" + +var NO_PARAMS = make(map[string]interface{}) + +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 { + return nil, err + } + res := make([]*Update, 0) + for _, u := range data["data"].([]interface{}) { + updateObj := new(Update) + err = MapToStruct(u.(map[string]interface{}), updateObj) + if err != nil { + return res, err + } + b.updateOffset = updateObj.UpdateID + 1 + err = b.updateQueue.Enqueue(updateObj) + if err != nil { + return res, err + } + res = append(res, updateObj) + + if b.debug && b.requestLogger != nil { + j, err := MapToJson(u.(map[string]interface{})) + if err != nil { + b.logger.Error(err) + } + b.requestLogger.Debug(fmt.Sprintf("UPDATE %s", j)) + } + } + 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 { + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id"` + MessageThreadID int `json:"message_thread_id,omitempty"` + Text string `json:"text"` + ParseMode ParseMode `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) { + dataP, err := StructToMap(params) + if err != nil { + return nil, err + } + data, err := b.request("sendMessage", dataP) + if err != nil { + return nil, err + } + message := new(Message) + err = MapToStruct(data, message) + return message, err +} + +type SendPhotoP struct { + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id"` + MessageThreadID int `json:"message_thread_id,omitempty"` + Photo string `json:"photo"` + Caption string `json:"caption,omitempty"` + ParseMode ParseMode `json:"parse_mode,omitempty"` + CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` + ShowCaptionAboveMedia bool `json:"show_caption_above_media"` + HasSpoiler bool `json:"has_spoiler"` + 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"` +} + +func (b *Bot) SendPhoto(params *SendPhotoP) (*Message, error) { + dataP, err := StructToMap(params) + if err != nil { + return nil, err + } + data, err := b.request("sendPhoto", dataP) + if err != nil { + return nil, err + } + message := new(Message) + err = MapToStruct(data, message) + return message, err +} diff --git a/plugins.go b/plugins.go new file mode 100644 index 0000000..0d3dc00 --- /dev/null +++ b/plugins.go @@ -0,0 +1,65 @@ +package laniakea + +type CommandExecutor func(ctx *MsgContext) + +type PluginBuilder struct { + name string + commands map[string]*CommandExecutor + payloads map[string]*CommandExecutor + updateListener *CommandExecutor +} + +type Plugin struct { + 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(f CommandExecutor, cmd ...string) *PluginBuilder { + for _, c := range cmd { + p.commands[c] = &f + } + return p +} + +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 +} + +func (p *PluginBuilder) Build() *Plugin { + if len(p.commands) == 0 { + return nil + } + plugin := &Plugin{ + Name: p.name, + Commands: p.commands, + Payloads: p.payloads, + UpdateListener: p.updateListener, + } + return 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/queue.go b/queue.go new file mode 100644 index 0000000..79ebeca --- /dev/null +++ b/queue.go @@ -0,0 +1,53 @@ +package laniakea + +import "fmt" + +type Queue[T any] struct { + queue []T + size uint64 +} + +func CreateQueue[T any](size uint64) *Queue[T] { + return &Queue[T]{ + queue: make([]T, 0), + size: size, + } +} + +func (q *Queue[T]) Enqueue(el T) error { + if q.IsFull() { + return fmt.Errorf("queue full") + } + q.queue = append(q.queue, el) + return nil +} + +func (q *Queue[T]) Peak() T { + return q.queue[0] +} + +func (q *Queue[T]) IsEmpty() bool { + return len(q.queue) == 0 +} + +func (q *Queue[T]) IsFull() bool { + return q.Length() == q.size +} + +func (q *Queue[T]) Length() uint64 { + return uint64(len(q.queue)) +} + +func (q *Queue[T]) Dequeue() T { + el := q.queue[0] + if q.Length() == 1 { + q.queue = make([]T, 0) + return el + } + q.queue = q.queue[1:] + return el +} + +func (q *Queue[T]) Raw() []T { + return q.queue +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..1a79e4b --- /dev/null +++ b/types.go @@ -0,0 +1,170 @@ +package laniakea + +type Update struct { + 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 { + ID int `json:"id"` + IsBot bool `json:"is_bot"` + FirstName string `json:"first_name"` + LastName string `json:"last_name,omitempty"` + Username string `json:"username,omitempty"` + LanguageCode string `json:"language_code,omitempty"` + IsPremium bool `json:"is_premium,omitempty"` + AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"` + CanJoinGroups bool `json:"can_join_groups,omitempty"` + CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"` + SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"` + CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"` + HasMainWebApp bool `json:"has_main_web_app,omitempty"` +} + +type Chat struct { + ID int `json:"id"` + Type string `json:"type"` + Title string `json:"title,omitempty"` + Username string `json:"username,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + IsForum bool `json:"is_forum,omitempty"` +} + +type Message struct { + MessageID int `json:"message_id"` + MessageThreadID int `json:"message_thread_id,omitempty"` + From *User `json:"from,omitempty"` + Chat *Chat `json:"chat,omitempty"` + Text string `json:"text"` + + Photo []*PhotoSize `json:"photo,omitempty"` + Caption string `json:"caption,omitempty"` + ReplyToMessage *Message `json:"reply_to_message"` +} + +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 PhotoSize struct { + FileID string `json:"file_id"` + FileUniqueID string `json:"file_unique_id"` + Width int `json:"width"` + Height int `json:"height"` + FileSize int `json:"file_size,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 new file mode 100644 index 0000000..e8c6385 --- /dev/null +++ b/utils.go @@ -0,0 +1,35 @@ +package laniakea + +import "encoding/json" + +func MapToStruct(m map[string]interface{}, s interface{}) error { + data, err := json.Marshal(m) + if err != nil { + return err + } + err = json.Unmarshal(data, s) + return err +} + +func MapToJson(m map[string]interface{}) (string, error) { + data, err := json.Marshal(m) + return string(data), err +} + +func StructToMap(s interface{}) (map[string]interface{}, error) { + data, err := json.Marshal(s) + if err != nil { + return nil, err + } + m := make(map[string]interface{}) + 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 +} diff --git a/version.go b/version.go new file mode 100644 index 0000000..fe6e598 --- /dev/null +++ b/version.go @@ -0,0 +1,8 @@ +package laniakea + +const ( + VERSION_STRING = "0.1.4" + VERSION_MAJOR = 0 + VERSION_MINOR = 1 + VERSION_PATCH = 4 +)