From 13eb3d45de16c3d41d45b1e0726210e467c20d61 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 27 Aug 2025 23:20:15 +0300 Subject: [PATCH] initial commit --- bot.go | 355 ++++++++++++++++++++++++++++++++++++++++++++++++++++ keyboard.go | 4 + logger.go | 162 ++++++++++++++++++++++++ methods.go | 111 ++++++++++++++++ plugins.go | 65 ++++++++++ queue.go | 53 ++++++++ types.go | 170 +++++++++++++++++++++++++ utils.go | 35 ++++++ version.go | 8 ++ 9 files changed, 963 insertions(+) create mode 100644 bot.go create mode 100644 keyboard.go create mode 100644 logger.go create mode 100644 methods.go create mode 100644 plugins.go create mode 100644 queue.go create mode 100644 types.go create mode 100644 utils.go create mode 100644 version.go 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 +)