From c6b47d18f6d0b00bd061893b180e675d7b0f7824 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Mon, 29 Sep 2025 09:18:48 +0300 Subject: [PATCH 01/22] middlewares --- bot.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++------ plugins.go | 59 ++++++++++++++++++++++++++++++++++++++++++++----- queue.go | 22 +++++++++++++++--- version.go | 12 ++++++---- 4 files changed, 139 insertions(+), 19 deletions(-) diff --git a/bot.go b/bot.go index 3b9c700..79087da 100644 --- a/bot.go +++ b/bot.go @@ -7,7 +7,12 @@ import ( "io" "net/http" "os" + "sort" "strings" + + "github.com/redis/go-redis/v9" + "go.mongodb.org/mongo-driver/v2/mongo" + "gorm.io/gorm" ) type ParseMode string @@ -26,8 +31,11 @@ type Bot struct { logger *Logger requestLogger *Logger - plugins []*Plugin - prefixes []string + plugins []*Plugin + middlewares []*Middleware + prefixes []string + + dbContext *DatabaseContext updateOffset int updateTypes []string @@ -45,7 +53,14 @@ type BotSettings struct { } func LoadSettingsFromEnv() *BotSettings { - + return &BotSettings{ + Token: os.Getenv("TG_TOKEN"), + Debug: os.Getenv("DEBUG") == "true", + ErrorTemplate: os.Getenv("ERROR_TEMPLATE"), + Prefixes: LoadPrefixesFromEnv(), + UpdateTypes: strings.Split(os.Getenv("UPDATE_TYPES"), ";"), + UseRequestLogger: os.Getenv("USE_REQ_LOG") == "true", + } } type MsgContext struct { @@ -58,6 +73,12 @@ type MsgContext struct { Args []string } +type DatabaseContext struct { + PostgresSQL *gorm.DB + MongoDB *mongo.Client + Redis *redis.Client +} + func NewBot(settings *BotSettings) *Bot { updateQueue := CreateQueue[*Update](256) bot := &Bot{ @@ -95,6 +116,11 @@ func (b *Bot) Close() { } } +func (b *Bot) InitDatabaseContext(ctx *DatabaseContext) *Bot { + b.dbContext = ctx + return b +} + func (b *Bot) UpdateTypes(t ...string) *Bot { b.updateTypes = make([]string, 0) b.updateTypes = append(b.updateTypes, t...) @@ -136,6 +162,23 @@ func (b *Bot) AddPlugins(plugin ...*Plugin) *Bot { return b } +func (b *Bot) AddMiddleware(middleware ...*Middleware) *Bot { + sort.Slice(middleware, func(a, b int) bool { + first := middleware[a] + second := middleware[b] + if first.Order == second.Order { + return first.Name < second.Name + } + return middleware[a].Order < middleware[b].Order + }) + + b.middlewares = append(b.middlewares, middleware...) + for _, m := range middleware { + b.logger.Debug(fmt.Sprintf("middleware with name \"%s\" was registered", m.Name)) + } + return b +} + func (b *Bot) Run() { if len(b.prefixes) == 0 { b.logger.Fatal("no prefixes defined") @@ -181,9 +224,13 @@ func (b *Bot) handleMessage(update *Update) { Update: update, } + for _, middleware := range b.middlewares { + middleware.Execute(ctx, b.dbContext) + } + for _, plugin := range b.plugins { if plugin.UpdateListener != nil { - (*plugin.UpdateListener)(ctx) + (*plugin.UpdateListener)(ctx, b.dbContext) } } @@ -219,7 +266,7 @@ func (b *Bot) handleMessage(update *Update) { ctx.Text = strings.TrimSpace(text[len(cmd):]) ctx.Args = strings.Split(ctx.Text, " ") - go plugin.Execute(cmd, ctx) + go plugin.Execute(cmd, ctx, b.dbContext) } } } @@ -230,9 +277,13 @@ func (b *Bot) handleCallback(update *Update) { Update: update, } + for _, m := range b.middlewares { + m.Execute(ctx, b.dbContext) + } + for _, plugin := range b.plugins { if plugin.UpdateListener != nil { - (*plugin.UpdateListener)(ctx) + (*plugin.UpdateListener)(ctx, b.dbContext) } } @@ -241,7 +292,7 @@ func (b *Bot) handleCallback(update *Update) { if !strings.HasPrefix(update.CallbackQuery.Data, payload) { continue } - go plugin.ExecutePayload(payload, ctx) + go plugin.ExecutePayload(payload, ctx, b.dbContext) } } } diff --git a/plugins.go b/plugins.go index 0d3dc00..29f5c27 100644 --- a/plugins.go +++ b/plugins.go @@ -1,6 +1,6 @@ package laniakea -type CommandExecutor func(ctx *MsgContext) +type CommandExecutor func(ctx *MsgContext, dbContext *DatabaseContext) type PluginBuilder struct { name string @@ -56,10 +56,59 @@ func (p *PluginBuilder) Build() *Plugin { return plugin } -func (p *Plugin) Execute(cmd string, ctx *MsgContext) { - (*p.Commands[cmd])(ctx) +func (p *Plugin) Execute(cmd string, ctx *MsgContext, dbContext *DatabaseContext) { + (*p.Commands[cmd])(ctx, dbContext) } -func (p *Plugin) ExecutePayload(payload string, ctx *MsgContext) { - (*p.Payloads[payload])(ctx) +func (p *Plugin) ExecutePayload(payload string, ctx *MsgContext, dbContext *DatabaseContext) { + (*p.Payloads[payload])(ctx, dbContext) +} + +type Middleware struct { + Name string + Executor *CommandExecutor + Order int + Async bool +} +type MiddlewareBuilder struct { + name string + executor *CommandExecutor + order int + async bool +} + +func NewMiddleware(name string) *MiddlewareBuilder { + return &MiddlewareBuilder{name: name, async: false} +} +func (m *MiddlewareBuilder) SetName(name string) *MiddlewareBuilder { + m.name = name + return m +} +func (m *MiddlewareBuilder) SetExecutor(executor CommandExecutor) *MiddlewareBuilder { + m.executor = &executor + return m +} +func (m *MiddlewareBuilder) SetOrder(order int) *MiddlewareBuilder { + m.order = order + return m +} +func (m *MiddlewareBuilder) SetAsync(async bool) *MiddlewareBuilder { + m.async = async + return m +} +func (m *MiddlewareBuilder) Build() *Middleware { + return &Middleware{ + Name: m.name, + Executor: m.executor, + Order: m.order, + Async: m.async, + } +} +func (m *Middleware) Execute(ctx *MsgContext, db *DatabaseContext) { + exec := *m.Executor + if m.Async { + go exec(ctx, db) + } else { + exec(ctx, db) + } } diff --git a/queue.go b/queue.go index 79ebeca..e35028d 100644 --- a/queue.go +++ b/queue.go @@ -1,10 +1,14 @@ package laniakea -import "fmt" +import ( + "fmt" + "sync" +) type Queue[T any] struct { - queue []T size uint64 + mu sync.RWMutex + queue []T } func CreateQueue[T any](size uint64) *Queue[T] { @@ -23,11 +27,13 @@ func (q *Queue[T]) Enqueue(el T) error { } func (q *Queue[T]) Peak() T { + q.mu.RLock() + defer q.mu.RUnlock() return q.queue[0] } func (q *Queue[T]) IsEmpty() bool { - return len(q.queue) == 0 + return q.Length() == 0 } func (q *Queue[T]) IsFull() bool { @@ -35,16 +41,26 @@ func (q *Queue[T]) IsFull() bool { } func (q *Queue[T]) Length() uint64 { + q.mu.RLock() + defer q.mu.RUnlock() return uint64(len(q.queue)) } func (q *Queue[T]) Dequeue() T { + q.mu.RLock() el := q.queue[0] + q.mu.RUnlock() + if q.Length() == 1 { + q.mu.Lock() q.queue = make([]T, 0) + q.mu.Unlock() return el } + + q.mu.Lock() q.queue = q.queue[1:] + q.mu.Unlock() return el } diff --git a/version.go b/version.go index fe6e598..d47af22 100644 --- a/version.go +++ b/version.go @@ -1,8 +1,12 @@ package laniakea +import "os" + const ( - VERSION_STRING = "0.1.4" - VERSION_MAJOR = 0 - VERSION_MINOR = 1 - VERSION_PATCH = 4 + VersionString = "0.1.4" + VersionMajor = 0 + VersionMinor = 1 + VersionPatch = 4 ) + +var GoVersion = os.Getenv("GoV") From 3d1263b3e04a190ee84f73fdbd194d9a8d7feaf7 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Mon, 29 Sep 2025 10:53:33 +0300 Subject: [PATCH 02/22] db logger --- bot.go | 14 ++++++++++++-- logger.go | 35 +++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/bot.go b/bot.go index 79087da..0f16a53 100644 --- a/bot.go +++ b/bot.go @@ -100,6 +100,7 @@ func NewBot(settings *BotSettings) *Bot { level = DEBUG } bot.logger = CreateLogger().Level(level).OpenFile(fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + bot.logger = bot.logger.PrintTraceback(true) if settings.UseRequestLogger { bot.requestLogger = CreateLogger().Level(level).Prefix("REQUESTS").OpenFile(fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/"))) } @@ -120,6 +121,14 @@ func (b *Bot) InitDatabaseContext(ctx *DatabaseContext) *Bot { b.dbContext = ctx return b } +func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) LoggerWriter) *Bot { + w := []LoggerWriter{writer(b.dbContext)} + b.logger.AddWriters(w) + if b.requestLogger != nil { + b.requestLogger.AddWriters(w) + } + return b +} func (b *Bot) UpdateTypes(t ...string) *Bot { b.updateTypes = make([]string, 0) @@ -283,7 +292,8 @@ func (b *Bot) handleCallback(update *Update) { for _, plugin := range b.plugins { if plugin.UpdateListener != nil { - (*plugin.UpdateListener)(ctx, b.dbContext) + lis := *plugin.UpdateListener + lis(ctx, b.dbContext) } } @@ -310,7 +320,7 @@ func (ctx *MsgContext) Answer(text string) { _, err := ctx.Bot.SendMessage(&SendMessageP{ ChatID: ctx.Msg.Chat.ID, Text: text, - ParseMode: "markdown", + ParseMode: ParseMD, }) if err != nil { ctx.Bot.logger.Error(err) diff --git a/logger.go b/logger.go index 2960fde..496e279 100644 --- a/logger.go +++ b/logger.go @@ -11,11 +11,14 @@ import ( "github.com/fatih/color" ) +type LoggerWriter func(level LogLevel, prefix, traceback string, message []any) + type Logger struct { prefix string level LogLevel printTraceback bool printTime bool + writers []LoggerWriter f *os.File } @@ -26,6 +29,10 @@ type LogLevel struct { c color.Attribute } +func (l *LogLevel) GetName() string { + return l.t +} + type MethodTraceback struct { Package string Method string @@ -36,11 +43,11 @@ type MethodTraceback struct { } 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} + INFO = LogLevel{n: 0, t: "info", c: color.FgWhite} + WARN = LogLevel{n: 1, t: "warn", c: color.FgHiYellow} + ERROR = LogLevel{n: 2, t: "error", c: color.FgHiRed} + FATAL = LogLevel{n: 3, t: "fatal", c: color.FgRed} + DEBUG = LogLevel{n: 4, t: "debug", c: color.FgGreen} ) func CreateLogger() *Logger { @@ -76,6 +83,14 @@ func (l *Logger) PrintTraceback(b bool) *Logger { l.printTraceback = b return l } +func (l *Logger) PrintTime(b bool) *Logger { + l.printTime = b + return l +} +func (l *Logger) AddWriters(writers []LoggerWriter) *Logger { + l.writers = append(l.writers, writers...) + return l +} func (l *Logger) Info(m ...any) { l.print(INFO, m) @@ -152,7 +167,15 @@ func (l *Logger) print(level LogLevel, m []any) { if l.level.n < level.n { return } - color.New(level.c).Println(l.buildString(level, m)) + _, err := color.New(level.c).Println(l.buildString(level, m)) + if err != nil { + l.Fatal(err) + return + } + + for _, writer := range l.writers { + writer(level, l.prefix, l.formatTraceback(l.getTraceback()), m) + } if l.f != nil { if _, err := l.f.Write([]byte(l.buildString(level, m) + "\n")); err != nil { From 0cc146edd9dd7c09114b8fd61ee366a2da2c2a7a Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Mon, 29 Sep 2025 16:44:08 +0300 Subject: [PATCH 03/22] logger --- bot.go | 54 ++++++++++++++++++------------------------------------ logger.go | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/bot.go b/bot.go index 0f16a53..bf9a0bc 100644 --- a/bot.go +++ b/bot.go @@ -217,32 +217,31 @@ func (b *Bot) Run() { } u := queue.Dequeue() + ctx := &MsgContext{ + Bot: b, + Update: u, + } + for _, middleware := range b.middlewares { + middleware.Execute(ctx, b.dbContext) + } + + for _, plugin := range b.plugins { + if plugin.UpdateListener != nil { + (*plugin.UpdateListener)(ctx, b.dbContext) + } + } + if u.CallbackQuery != nil { - b.handleCallback(u) + b.handleCallback(u, ctx) } else { - b.handleMessage(u) + b.handleMessage(u, ctx) } } } // {"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 _, middleware := range b.middlewares { - middleware.Execute(ctx, b.dbContext) - } - - for _, plugin := range b.plugins { - if plugin.UpdateListener != nil { - (*plugin.UpdateListener)(ctx, b.dbContext) - } - } - +func (b *Bot) handleMessage(update *Update, ctx *MsgContext) { var text string if update.Message == nil { return @@ -265,7 +264,6 @@ func (b *Bot) handleMessage(update *Update) { text = strings.TrimSpace(text[len(prefix):]) for _, plugin := range b.plugins { - // Check every command for cmd := range plugin.Commands { if !strings.HasPrefix(text, cmd) { @@ -280,23 +278,7 @@ func (b *Bot) handleMessage(update *Update) { } } -func (b *Bot) handleCallback(update *Update) { - ctx := &MsgContext{ - Bot: b, - Update: update, - } - - for _, m := range b.middlewares { - m.Execute(ctx, b.dbContext) - } - - for _, plugin := range b.plugins { - if plugin.UpdateListener != nil { - lis := *plugin.UpdateListener - lis(ctx, b.dbContext) - } - } - +func (b *Bot) handleCallback(update *Update, ctx *MsgContext) { for _, plugin := range b.plugins { for payload := range plugin.Payloads { if !strings.HasPrefix(update.CallbackQuery.Data, payload) { diff --git a/logger.go b/logger.go index 496e279..da31495 100644 --- a/logger.go +++ b/logger.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "strings" "time" @@ -142,6 +143,47 @@ func (l *Logger) formatTraceback(mt *MethodTraceback) string { return fmt.Sprintf("%s:%s:%d", mt.filename, mt.Method, mt.line) } +func (l *Logger) getFullTraceback(skip int) []*MethodTraceback { + pc := make([]uintptr, 15) + runtime.Callers(skip, pc) + list := make([]*MethodTraceback, 0) + frames := runtime.CallersFrames(pc) + for { + frame, more := frames.Next() + if !more { + break + } + details := runtime.FuncForPC(frame.PC) + signature := details.Name() + path, line := details.FileLine(frame.PC) + 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, + } + list = append(list, tb) + } + sort.Slice(list, func(i, j int) bool { + return j < i + }) + return list +} +func (l *Logger) formatFullTraceback(tracebacks []*MethodTraceback) string { + formatted := make([]string, 0) + for _, tb := range tracebacks { + formatted = append(formatted, l.formatTraceback(tb)) + } + return strings.Join(formatted, "->") +} + func (l *Logger) buildString(level LogLevel, m []any) string { args := []string{ fmt.Sprintf("[%s]", l.prefix), @@ -174,7 +216,7 @@ func (l *Logger) print(level LogLevel, m []any) { } for _, writer := range l.writers { - writer(level, l.prefix, l.formatTraceback(l.getTraceback()), m) + writer(level, l.prefix, l.formatFullTraceback(l.getFullTraceback(4)), m) } if l.f != nil { From b88715d6d33b1eb8eaa1db6870b973df291afbaf Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 14 Jan 2026 12:07:05 +0300 Subject: [PATCH 04/22] some fixes --- bot.go | 62 +++++++++++++++++++++++++++++++++++++++++----- methods.go | 73 +++++++++++++++++++++++++++++++++++++++++++----------- utils.go | 21 +++++++++++++++- 3 files changed, 135 insertions(+), 21 deletions(-) diff --git a/bot.go b/bot.go index bf9a0bc..c63e761 100644 --- a/bot.go +++ b/bot.go @@ -11,8 +11,8 @@ import ( "strings" "github.com/redis/go-redis/v9" + "github.com/vinovest/sqlx" "go.mongodb.org/mongo-driver/v2/mongo" - "gorm.io/gorm" ) type ParseMode string @@ -74,7 +74,7 @@ type MsgContext struct { } type DatabaseContext struct { - PostgresSQL *gorm.DB + PostgresSQL *sqlx.DB MongoDB *mongo.Client Redis *redis.Client } @@ -217,6 +217,10 @@ func (b *Bot) Run() { } u := queue.Dequeue() + if u == nil { + b.logger.Error("update is nil") + continue + } ctx := &MsgContext{ Bot: b, Update: u, @@ -298,18 +302,28 @@ func (b *Bot) checkPrefixes(text string) (string, bool) { return "", false } -func (ctx *MsgContext) Answer(text string) { - _, err := ctx.Bot.SendMessage(&SendMessageP{ +type AnswerMessage struct { + MessageID int + IsMedia bool + ctx *MsgContext +} + +func (ctx *MsgContext) Answer(text string) *AnswerMessage { + msg, err := ctx.Bot.SendMessage(&SendMessageP{ ChatID: ctx.Msg.Chat.ID, Text: text, ParseMode: ParseMD, }) if err != nil { ctx.Bot.logger.Error(err) + return nil + } + return &AnswerMessage{ + MessageID: msg.MessageID, ctx: ctx, IsMedia: false, } } -func (ctx *MsgContext) AnswerPhoto(photoId string, text string) { +func (ctx *MsgContext) AnswerPhoto(photoId string, text string) *AnswerMessage { _, err := ctx.Bot.SendPhoto(&SendPhotoP{ ChatID: ctx.Msg.Chat.ID, Caption: text, @@ -319,6 +333,41 @@ func (ctx *MsgContext) AnswerPhoto(photoId string, text string) { if err != nil { ctx.Bot.logger.Error(err) } + return &AnswerMessage{ + MessageID: ctx.Msg.MessageID, ctx: ctx, IsMedia: true, + } +} + +func (m *AnswerMessage) Edit(text string) *AnswerMessage { + var msg *Message + var err error + if m.IsMedia { + msg, err = m.ctx.Bot.EditMessageCaption(&EditMessageCaptionP{ + MessageID: m.MessageID, + ChatID: m.ctx.Msg.Chat.ID, + Caption: text, + ParseMode: ParseMD, + }) + } else { + msg, err = m.ctx.Bot.EditMessageText(&EditMessageTextP{ + MessageID: m.MessageID, + ChatID: m.ctx.Msg.Chat.ID, + Text: text, + ParseMode: ParseMD, + }) + } + if err != nil { + m.ctx.Bot.logger.Error(err) + } + m.MessageID = msg.MessageID + return m +} + +func (m *AnswerMessage) Delete() { + _, err := m.ctx.Bot.DeleteMessage(&DeleteMessageP{MessageID: m.MessageID, ChatID: m.ctx.Msg.Chat.ID}) + if err != nil { + m.ctx.Bot.logger.Error(err) + } } func (ctx *MsgContext) Error(err error) { @@ -351,7 +400,7 @@ type ApiResponseA struct { } // request is a low-level call to api. -func (b *Bot) request(methodName string, params map[string]interface{}) (map[string]interface{}, error) { +func (b *Bot) request(methodName string, params any) (map[string]interface{}, error) { var buf bytes.Buffer err := json.NewEncoder(&buf).Encode(params) if err != nil { @@ -370,6 +419,7 @@ func (b *Bot) request(methodName string, params map[string]interface{}) (map[str if err != nil { return nil, err } + defer r.Body.Close() data, err := io.ReadAll(r.Body) if err != nil { return nil, err diff --git a/methods.go b/methods.go index 2793a48..87375fd 100644 --- a/methods.go +++ b/methods.go @@ -2,7 +2,7 @@ package laniakea import "fmt" -var NO_PARAMS = make(map[string]interface{}) +var NoParams = make(map[string]interface{}) func (b *Bot) Updates() ([]*Update, error) { params := make(map[string]interface{}) @@ -40,7 +40,7 @@ func (b *Bot) Updates() ([]*Update, error) { } func (b *Bot) GetMe() (*User, error) { - data, err := b.request("getMe", NO_PARAMS) + data, err := b.request("getMe", NoParams) if err != nil { return nil, err } @@ -53,8 +53,8 @@ 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"` + Text string `json:"text"` Entities []*MessageEntity `json:"entities,omitempty"` LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` DisableNotifications bool `json:"disable_notifications,omitempty"` @@ -67,11 +67,7 @@ type SendMessageP struct { } func (b *Bot) SendMessage(params *SendMessageP) (*Message, error) { - dataP, err := StructToMap(params) - if err != nil { - return nil, err - } - data, err := b.request("sendMessage", dataP) + data, err := b.request("sendMessage", params) if err != nil { return nil, err } @@ -84,9 +80,9 @@ type SendPhotoP struct { BusinessConnectionID string `json:"business_connection_id,omitempty"` ChatID int `json:"chat_id"` MessageThreadID int `json:"message_thread_id,omitempty"` + ParseMode ParseMode `json:"parse_mode,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"` @@ -97,11 +93,60 @@ type SendPhotoP struct { } func (b *Bot) SendPhoto(params *SendPhotoP) (*Message, error) { - dataP, err := StructToMap(params) - if err != nil { - return nil, err - } - data, err := b.request("sendPhoto", dataP) + data, err := b.request("sendPhoto", params) + if err != nil { + return nil, err + } + message := new(Message) + err = MapToStruct(data, message) + return message, err +} + +type EditMessageTextP struct { + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id,omitempty"` + MessageID int `json:"message_id,omitempty"` + InlineMessageID string `json:"inline_message_id,omitempty"` + Text string `json:"text"` + ParseMode ParseMode `json:"parse_mode,omitempty"` +} + +func (b *Bot) EditMessageText(params *EditMessageTextP) (*Message, error) { + data, err := b.request("editMessageText", params) + if err != nil { + return nil, err + } + message := new(Message) + err = MapToStruct(data, message) + return message, err +} + +type EditMessageCaptionP struct { + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id,omitempty"` + MessageID int `json:"message_id,omitempty"` + InlineMessageID string `json:"inline_message_id,omitempty"` + Caption string `json:"caption"` + ParseMode ParseMode `json:"parse_mode,omitempty"` +} + +func (b *Bot) EditMessageCaption(params *EditMessageCaptionP) (*Message, error) { + data, err := b.request("editMessageCaption", params) + if err != nil { + return nil, err + } + message := new(Message) + err = MapToStruct(data, message) + return message, err +} + +type DeleteMessageP struct { + ChatID int `json:"chat_id"` + MessageID int `json:"message_id"` +} + +func (b *Bot) DeleteMessage(params *DeleteMessageP) (*Message, error) { + data, err := b.request("deleteMessage", params) if err != nil { return nil, err } diff --git a/utils.go b/utils.go index e8c6385..e015dc1 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,10 @@ package laniakea -import "encoding/json" +import ( + "encoding/json" + "fmt" + "strings" +) func MapToStruct(m map[string]interface{}, s interface{}) error { data, err := json.Marshal(m) @@ -33,3 +37,18 @@ func Map[T, V any](ts []T, fn func(T) V) []V { } return result } + +func EscapeMarkdown(s string) string { + s = strings.ReplaceAll(s, "_", "\\_") + s = strings.ReplaceAll(s, "*", "\\*") + s = strings.ReplaceAll(s, "[", "\\[") + return strings.ReplaceAll(s, "`", "\\`") +} + +func EscapeMarkdownV2(s string) string { + symbols := []string{"_", "*", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"} + for _, symbol := range symbols { + s = strings.ReplaceAll(s, symbol, fmt.Sprintf("\\%s", symbol)) + } + return s +} From 21623788c610d089cac54fd0c72ddc4a7931e525 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Fri, 16 Jan 2026 15:34:43 +0300 Subject: [PATCH 05/22] some fixes --- bot.go | 13 ++++++++++-- utils.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++--- version.go | 6 +++--- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/bot.go b/bot.go index c63e761..11561c5 100644 --- a/bot.go +++ b/bot.go @@ -50,6 +50,7 @@ type BotSettings struct { UpdateTypes []string LoggerBasePath string UseRequestLogger bool + WriteToFile bool } func LoadSettingsFromEnv() *BotSettings { @@ -60,6 +61,7 @@ func LoadSettingsFromEnv() *BotSettings { Prefixes: LoadPrefixesFromEnv(), UpdateTypes: strings.Split(os.Getenv("UPDATE_TYPES"), ";"), UseRequestLogger: os.Getenv("USE_REQ_LOG") == "true", + WriteToFile: os.Getenv("WRITE_TO_FILE") == "true", } } @@ -99,10 +101,16 @@ func NewBot(settings *BotSettings) *Bot { if settings.Debug { level = DEBUG } - bot.logger = CreateLogger().Level(level).OpenFile(fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + bot.logger = CreateLogger().Level(level) + if settings.WriteToFile { + bot.logger = bot.logger.OpenFile(fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + } bot.logger = bot.logger.PrintTraceback(true) if settings.UseRequestLogger { - bot.requestLogger = CreateLogger().Level(level).Prefix("REQUESTS").OpenFile(fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + bot.requestLogger = CreateLogger().Level(level).Prefix("REQUESTS") + if settings.WriteToFile { + bot.requestLogger = bot.requestLogger.OpenFile(fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + } } return bot @@ -375,6 +383,7 @@ func (ctx *MsgContext) Error(err error) { ChatID: ctx.Msg.Chat.ID, Text: fmt.Sprintf(ctx.Bot.errorTemplate, err.Error()), }) + ctx.Bot.logger.Error(err) if sendErr != nil { ctx.Bot.logger.Error(sendErr) diff --git a/utils.go b/utils.go index e015dc1..6725da0 100644 --- a/utils.go +++ b/utils.go @@ -39,9 +39,9 @@ func Map[T, V any](ts []T, fn func(T) V) []V { } func EscapeMarkdown(s string) string { - s = strings.ReplaceAll(s, "_", "\\_") - s = strings.ReplaceAll(s, "*", "\\*") - s = strings.ReplaceAll(s, "[", "\\[") + s = strings.ReplaceAll(s, "_", `\_`) + s = strings.ReplaceAll(s, "*", `\*`) + s = strings.ReplaceAll(s, "[", `\[`) return strings.ReplaceAll(s, "`", "\\`") } @@ -52,3 +52,57 @@ func EscapeMarkdownV2(s string) string { } return s } + +func GetUnclosedTag(markdown string) string { + // order is important! + var tags = []string{ + "```", + "`", + "*", + "_", + } + var currentTag = "" + + markdownRunes := []rune(markdown) + + var i = 0 +outer: + for i < len(markdownRunes) { + // skip escaped characters (only outside tags) + if markdownRunes[i] == '\\' && currentTag == "" { + i += 2 + continue + } + if currentTag != "" { + if strings.HasPrefix(string(markdownRunes[i:]), currentTag) { + // turn a tag off + i += len(currentTag) + currentTag = "" + continue + } + } else { + for _, tag := range tags { + if strings.HasPrefix(string(markdownRunes[i:]), tag) { + // turn a tag on + currentTag = tag + i += len(currentTag) + continue outer + } + } + } + i++ + } + + return currentTag +} +func IsValid(markdown string) bool { + return GetUnclosedTag(markdown) == "" +} + +func FixMarkdown(markdown string) string { + tag := GetUnclosedTag(markdown) + if tag == "" { + return markdown + } + return markdown + tag +} diff --git a/version.go b/version.go index d47af22..1a090c8 100644 --- a/version.go +++ b/version.go @@ -3,10 +3,10 @@ package laniakea import "os" const ( - VersionString = "0.1.4" + VersionString = "0.2.0" VersionMajor = 0 - VersionMinor = 1 - VersionPatch = 4 + VersionMinor = 2 + VersionPatch = 0 ) var GoVersion = os.Getenv("GoV") From 684d56acbacb0a5cd468ed03038924596baeae58 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 21 Jan 2026 09:08:46 +0300 Subject: [PATCH 06/22] small fixes and addition --- bot.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bot.go b/bot.go index 11561c5..690f10c 100644 --- a/bot.go +++ b/bot.go @@ -330,6 +330,10 @@ func (ctx *MsgContext) Answer(text string) *AnswerMessage { MessageID: msg.MessageID, ctx: ctx, IsMedia: false, } } +func (ctx *MsgContext) Answerf(template string, args ...any) *AnswerMessage { + test := fmt.Sprintf(template, args...) + return ctx.Answer(test) +} func (ctx *MsgContext) AnswerPhoto(photoId string, text string) *AnswerMessage { _, err := ctx.Bot.SendPhoto(&SendPhotoP{ @@ -372,7 +376,9 @@ func (m *AnswerMessage) Edit(text string) *AnswerMessage { } func (m *AnswerMessage) Delete() { - _, err := m.ctx.Bot.DeleteMessage(&DeleteMessageP{MessageID: m.MessageID, ChatID: m.ctx.Msg.Chat.ID}) + _, err := m.ctx.Bot.DeleteMessage(&DeleteMessageP{ + MessageID: m.MessageID, ChatID: m.ctx.Msg.Chat.ID, + }) if err != nil { m.ctx.Bot.logger.Error(err) } @@ -381,7 +387,7 @@ func (m *AnswerMessage) Delete() { func (ctx *MsgContext) Error(err error) { _, sendErr := ctx.Bot.SendMessage(&SendMessageP{ ChatID: ctx.Msg.Chat.ID, - Text: fmt.Sprintf(ctx.Bot.errorTemplate, err.Error()), + Text: fmt.Sprintf(ctx.Bot.errorTemplate, EscapeMarkdown(err.Error())), }) ctx.Bot.logger.Error(err) From ce13b1967658012da013965d60f44f0cd4a6ccf5 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 21 Jan 2026 15:53:30 +0300 Subject: [PATCH 07/22] inline keyboard --- bot.go | 43 +++++++++++++++++++++++++++++++------------ keyboard.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ methods.go | 2 +- types.go | 8 +++++++- 4 files changed, 88 insertions(+), 14 deletions(-) diff --git a/bot.go b/bot.go index 690f10c..a6c64ba 100644 --- a/bot.go +++ b/bot.go @@ -291,13 +291,20 @@ func (b *Bot) handleMessage(update *Update, ctx *MsgContext) { } func (b *Bot) handleCallback(update *Update, ctx *MsgContext) { + data := new(CallbackData) + err := json.Unmarshal([]byte(update.CallbackQuery.Data), data) + if err != nil { + b.logger.Error(err) + return + } + for _, plugin := range b.plugins { - for payload := range plugin.Payloads { - if !strings.HasPrefix(update.CallbackQuery.Data, payload) { - continue - } - go plugin.ExecutePayload(payload, ctx, b.dbContext) + _, ok := plugin.Payloads[data.Command] + if !ok { + continue } + go plugin.ExecutePayload(data.Command, ctx, b.dbContext) + break } } @@ -312,27 +319,39 @@ func (b *Bot) checkPrefixes(text string) (string, bool) { type AnswerMessage struct { MessageID int + Text string IsMedia bool + Keyboard *InlineKeyboard ctx *MsgContext } -func (ctx *MsgContext) Answer(text string) *AnswerMessage { - msg, err := ctx.Bot.SendMessage(&SendMessageP{ +func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard) *AnswerMessage { + params := &SendMessageP{ ChatID: ctx.Msg.Chat.ID, Text: text, ParseMode: ParseMD, - }) + } + if keyboard != nil { + params.ReplyMarkup = keyboard.Get() + } + + msg, err := ctx.Bot.SendMessage(params) if err != nil { ctx.Bot.logger.Error(err) return nil } return &AnswerMessage{ - MessageID: msg.MessageID, ctx: ctx, IsMedia: false, + MessageID: msg.MessageID, ctx: ctx, IsMedia: false, Text: text, } } +func (ctx *MsgContext) Answer(text string) *AnswerMessage { + return ctx.answer(text, nil) +} func (ctx *MsgContext) Answerf(template string, args ...any) *AnswerMessage { - test := fmt.Sprintf(template, args...) - return ctx.Answer(test) + return ctx.answer(fmt.Sprintf(template, args...), nil) +} +func (ctx *MsgContext) Keyboard(text string, kb *InlineKeyboard) *AnswerMessage { + return ctx.answer(text, kb) } func (ctx *MsgContext) AnswerPhoto(photoId string, text string) *AnswerMessage { @@ -346,7 +365,7 @@ func (ctx *MsgContext) AnswerPhoto(photoId string, text string) *AnswerMessage { ctx.Bot.logger.Error(err) } return &AnswerMessage{ - MessageID: ctx.Msg.MessageID, ctx: ctx, IsMedia: true, + MessageID: ctx.Msg.MessageID, ctx: ctx, IsMedia: true, Text: text, } } diff --git a/keyboard.go b/keyboard.go index edc15f0..14cba31 100644 --- a/keyboard.go +++ b/keyboard.go @@ -1,4 +1,53 @@ package laniakea +import "encoding/json" + type InlineKeyboard struct { + CurrentLine []InlineKeyboardButton + Lines [][]InlineKeyboardButton +} + +func NewInlineKeyboard() *InlineKeyboard { + return &InlineKeyboard{ + CurrentLine: make([]InlineKeyboardButton, 0), + Lines: make([][]InlineKeyboardButton, 0), + } +} + +func (in *InlineKeyboard) append(button InlineKeyboardButton) *InlineKeyboard { + in.CurrentLine = append(in.CurrentLine, button) + return in +} +func (in *InlineKeyboard) AddUrlButton(text, url string) *InlineKeyboard { + return in.append(InlineKeyboardButton{Text: text, URL: url}) +} +func (in *InlineKeyboard) AddCallbackButton(text string, data CallbackData) *InlineKeyboard { + return in.append(InlineKeyboardButton{Text: text, CallbackData: data.ToJson()}) +} + +func (in *InlineKeyboard) AddLine() *InlineKeyboard { + in.Lines = append(in.Lines, in.CurrentLine) + in.CurrentLine = make([]InlineKeyboardButton, 0) + return in +} +func (in *InlineKeyboard) Get() *InlineKeyboardMarkup { + if len(in.CurrentLine) > 0 { + in.Lines = append(in.Lines, in.CurrentLine) + } + return &InlineKeyboardMarkup{ + InlineKeyboard: in.Lines, + } +} + +type CallbackData struct { + Command string `json:"cmd"` + Args []any `json:"args"` +} + +func (d *CallbackData) ToJson() string { + data, err := json.Marshal(d) + if err != nil { + return `{"cmd":""}` + } + return string(data) } diff --git a/methods.go b/methods.go index 87375fd..6fd6040 100644 --- a/methods.go +++ b/methods.go @@ -62,7 +62,7 @@ type SendMessageP struct { 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"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` // ReplyKeyboardMarkup *ReplyKeyboardMarkup `json:"reply_markup,omitempty"` } diff --git a/types.go b/types.go index 1a79e4b..549eeef 100644 --- a/types.go +++ b/types.go @@ -43,6 +43,10 @@ type Chat struct { IsForum bool `json:"is_forum,omitempty"` } +type MessageReplyMarkup struct { + InlineKeyboard *InlineKeyboardMarkup `json:"inline_keyboard"` +} + type Message struct { MessageID int `json:"message_id"` MessageThreadID int `json:"message_thread_id,omitempty"` @@ -53,6 +57,8 @@ type Message struct { Photo []*PhotoSize `json:"photo,omitempty"` Caption string `json:"caption,omitempty"` ReplyToMessage *Message `json:"reply_to_message"` + + ReplyMarkup *MessageReplyMarkup `json:"reply_markup,omitempty"` } type InaccessableMessage struct { @@ -103,7 +109,7 @@ type LinkPreviewOptions struct { } type InlineKeyboardMarkup struct { - InlineKeyboard [][]*InlineKeyboardButton `json:"inline_keyboard"` + InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` } type InlineKeyboardButton struct { From 7a3e40a74dc6c1e233ae96760fb5005114409969 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Thu, 22 Jan 2026 13:47:28 +0300 Subject: [PATCH 08/22] inline keyboard --- bot.go | 103 +++++++++++++++++++++++++++++++--------------------- keyboard.go | 37 +++++++++++++++---- methods.go | 61 ++++++++++++++++++------------- types.go | 14 +++---- 4 files changed, 133 insertions(+), 82 deletions(-) diff --git a/bot.go b/bot.go index a6c64ba..b2e6c31 100644 --- a/bot.go +++ b/bot.go @@ -9,6 +9,7 @@ import ( "os" "sort" "strings" + "time" "github.com/redis/go-redis/v9" "github.com/vinovest/sqlx" @@ -66,13 +67,15 @@ func LoadSettingsFromEnv() *BotSettings { } type MsgContext struct { - Bot *Bot - Msg *Message - Update *Update - FromID int - Prefix string - Text string - Args []string + Bot *Bot + Msg *Message + Update *Update + From *User + CallbackMsgId int + FromID int + Prefix string + Text string + Args []string } type DatabaseContext struct { @@ -221,6 +224,7 @@ func (b *Bot) Run() { for { queue := b.updateQueue if queue.IsEmpty() { + time.Sleep(time.Millisecond * 25) continue } @@ -265,6 +269,7 @@ func (b *Bot) handleMessage(update *Update, ctx *MsgContext) { } ctx.FromID = update.Message.From.ID + ctx.From = update.Message.From ctx.Msg = update.Message text = strings.TrimSpace(text) prefix, hasPrefix := b.checkPrefixes(text) @@ -298,6 +303,12 @@ func (b *Bot) handleCallback(update *Update, ctx *MsgContext) { return } + ctx.FromID = update.CallbackQuery.From.ID + ctx.From = update.CallbackQuery.From + ctx.Msg = update.CallbackQuery.Message + ctx.CallbackMsgId = update.CallbackQuery.Message.MessageID + ctx.Args = data.Args + for _, plugin := range b.plugins { _, ok := plugin.Payloads[data.Command] if !ok { @@ -325,6 +336,40 @@ type AnswerMessage struct { ctx *MsgContext } +func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard) *AnswerMessage { + params := &EditMessageTextP{ + MessageID: messageId, + ChatID: ctx.Msg.Chat.ID, + Text: text, + ParseMode: ParseMD, + } + if keyboard != nil { + params.ReplyMarkup = keyboard.Get() + } + msg, err := ctx.Bot.EditMessageText(params) + if err != nil { + ctx.Bot.logger.Error(err) + return nil + } + return &AnswerMessage{ + MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: false, + } +} +func (m *AnswerMessage) Edit(text string) *AnswerMessage { + return m.ctx.edit(m.MessageID, text, nil) +} +func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage { + if ctx.CallbackMsgId == 0 { + ctx.Bot.logger.Error("Can't edit non-callback update message") + return nil + } + + return ctx.edit(ctx.CallbackMsgId, text, keyboard) +} +func (ctx *MsgContext) EditCallbackf(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage { + return ctx.EditCallback(fmt.Sprintf(format, args...), keyboard) +} + func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard) *AnswerMessage { params := &SendMessageP{ ChatID: ctx.Msg.Chat.ID, @@ -369,31 +414,6 @@ func (ctx *MsgContext) AnswerPhoto(photoId string, text string) *AnswerMessage { } } -func (m *AnswerMessage) Edit(text string) *AnswerMessage { - var msg *Message - var err error - if m.IsMedia { - msg, err = m.ctx.Bot.EditMessageCaption(&EditMessageCaptionP{ - MessageID: m.MessageID, - ChatID: m.ctx.Msg.Chat.ID, - Caption: text, - ParseMode: ParseMD, - }) - } else { - msg, err = m.ctx.Bot.EditMessageText(&EditMessageTextP{ - MessageID: m.MessageID, - ChatID: m.ctx.Msg.Chat.ID, - Text: text, - ParseMode: ParseMD, - }) - } - if err != nil { - m.ctx.Bot.logger.Error(err) - } - m.MessageID = msg.MessageID - return m -} - func (m *AnswerMessage) Delete() { _, err := m.ctx.Bot.DeleteMessage(&DeleteMessageP{ MessageID: m.MessageID, ChatID: m.ctx.Msg.Chat.ID, @@ -420,17 +440,17 @@ func (b *Bot) Logger() *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"` + Ok bool `json:"ok"` + Result map[string]any `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"` + Ok bool `json:"ok"` + Result []any `json:"result,omitempty"` + Description string `json:"description,omitempty"` + ErrorCode int `json:"error_code,omitempty"` } // request is a low-level call to api. @@ -458,9 +478,10 @@ func (b *Bot) request(methodName string, params any) (map[string]interface{}, er if err != nil { return nil, err } + b.requestLogger.Debug(fmt.Sprintf("RES %s %s", methodName, string(data))) response := new(ApiResponse) - var result map[string]interface{} + var result map[string]any err = json.Unmarshal(data, &response) if err != nil { diff --git a/keyboard.go b/keyboard.go index 14cba31..bdb9e35 100644 --- a/keyboard.go +++ b/keyboard.go @@ -1,49 +1,70 @@ package laniakea -import "encoding/json" +import ( + "encoding/json" + "fmt" +) type InlineKeyboard struct { CurrentLine []InlineKeyboardButton Lines [][]InlineKeyboardButton + maxRow int } -func NewInlineKeyboard() *InlineKeyboard { +func NewInlineKeyboard(maxRow int) *InlineKeyboard { return &InlineKeyboard{ CurrentLine: make([]InlineKeyboardButton, 0), Lines: make([][]InlineKeyboardButton, 0), + maxRow: maxRow, } } func (in *InlineKeyboard) append(button InlineKeyboardButton) *InlineKeyboard { + if len(in.CurrentLine) == in.maxRow { + in.AddLine() + } in.CurrentLine = append(in.CurrentLine, button) return in } func (in *InlineKeyboard) AddUrlButton(text, url string) *InlineKeyboard { return in.append(InlineKeyboardButton{Text: text, URL: url}) } -func (in *InlineKeyboard) AddCallbackButton(text string, data CallbackData) *InlineKeyboard { - return in.append(InlineKeyboardButton{Text: text, CallbackData: data.ToJson()}) +func (in *InlineKeyboard) AddCallbackButton(text string, cmd string, args ...any) *InlineKeyboard { + return in.append(InlineKeyboardButton{Text: text, CallbackData: NewCallbackData(cmd, args...).ToJson()}) } func (in *InlineKeyboard) AddLine() *InlineKeyboard { + if len(in.CurrentLine) == 0 { + return in + } in.Lines = append(in.Lines, in.CurrentLine) in.CurrentLine = make([]InlineKeyboardButton, 0) return in } -func (in *InlineKeyboard) Get() *InlineKeyboardMarkup { +func (in *InlineKeyboard) Get() InlineKeyboardMarkup { if len(in.CurrentLine) > 0 { in.Lines = append(in.Lines, in.CurrentLine) } - return &InlineKeyboardMarkup{ + return InlineKeyboardMarkup{ InlineKeyboard: in.Lines, } } type CallbackData struct { - Command string `json:"cmd"` - Args []any `json:"args"` + Command string `json:"cmd"` + Args []string `json:"args"` } +func NewCallbackData(command string, args ...any) *CallbackData { + stringArgs := make([]string, len(args)) + for i, arg := range args { + stringArgs[i] = fmt.Sprint(arg) + } + return &CallbackData{ + Command: command, + Args: stringArgs, + } +} func (d *CallbackData) ToJson() string { data, err := json.Marshal(d) if err != nil { diff --git a/methods.go b/methods.go index 6fd6040..1695e64 100644 --- a/methods.go +++ b/methods.go @@ -1,11 +1,14 @@ package laniakea -import "fmt" +import ( + "encoding/json" + "fmt" +) -var NoParams = make(map[string]interface{}) +var NoParams = make(map[string]any) func (b *Bot) Updates() ([]*Update, error) { - params := make(map[string]interface{}) + params := make(map[string]any) params["offset"] = b.updateOffset params["timeout"] = 30 params["allowed_updates"] = b.updateTypes @@ -15,12 +18,20 @@ func (b *Bot) Updates() ([]*Update, error) { return nil, err } res := make([]*Update, 0) - for _, u := range data["data"].([]interface{}) { + for _, u := range data["data"].([]any) { updateObj := new(Update) - err = MapToStruct(u.(map[string]interface{}), updateObj) + data, err := json.Marshal(u) if err != nil { return res, err } + err = json.Unmarshal(data, updateObj) + if err != nil { + return res, err + } + //err = MapToStruct(u.(map[string]any), updateObj) + //if err != nil { + // return res, err + //} b.updateOffset = updateObj.UpdateID + 1 err = b.updateQueue.Enqueue(updateObj) if err != nil { @@ -50,20 +61,19 @@ func (b *Bot) GetMe() (*User, error) { } type SendMessageP struct { - BusinessConnectionID string `json:"business_connection_id,omitempty"` - ChatID int `json:"chat_id"` - MessageThreadID int `json:"message_thread_id,omitempty"` - ParseMode ParseMode `json:"parse_mode,omitempty"` - Text string `json:"text"` - 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"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // ReplyKeyboardMarkup *ReplyKeyboardMarkup `json:"reply_markup,omitempty"` + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id"` + MessageThreadID int `json:"message_thread_id,omitempty"` + ParseMode ParseMode `json:"parse_mode,omitempty"` + Text string `json:"text"` + 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"` + ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` } func (b *Bot) SendMessage(params *SendMessageP) (*Message, error) { @@ -103,12 +113,13 @@ func (b *Bot) SendPhoto(params *SendPhotoP) (*Message, error) { } type EditMessageTextP struct { - BusinessConnectionID string `json:"business_connection_id,omitempty"` - ChatID int `json:"chat_id,omitempty"` - MessageID int `json:"message_id,omitempty"` - InlineMessageID string `json:"inline_message_id,omitempty"` - Text string `json:"text"` - ParseMode ParseMode `json:"parse_mode,omitempty"` + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id,omitempty"` + MessageID int `json:"message_id,omitempty"` + InlineMessageID string `json:"inline_message_id,omitempty"` + Text string `json:"text"` + ParseMode ParseMode `json:"parse_mode,omitempty"` + ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` } func (b *Bot) EditMessageText(params *EditMessageTextP) (*Message, error) { diff --git a/types.go b/types.go index 549eeef..f2fa3b7 100644 --- a/types.go +++ b/types.go @@ -12,9 +12,9 @@ type Update struct { DeletedBusinessMessage *Message `json:"deleted_business_messages,omitempty"` MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"` MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` + CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` InlineQuery int ChosenInlineResult int - CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` } type User struct { @@ -44,7 +44,7 @@ type Chat struct { } type MessageReplyMarkup struct { - InlineKeyboard *InlineKeyboardMarkup `json:"inline_keyboard"` + InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` } type Message struct { @@ -68,8 +68,6 @@ type InaccessableMessage struct { } type MaybeInaccessibleMessage struct { - Message - InaccessableMessage } type MessageEntity struct { @@ -109,7 +107,7 @@ type LinkPreviewOptions struct { } type InlineKeyboardMarkup struct { - InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` + InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard,omitempty"` } type InlineKeyboardButton struct { @@ -123,9 +121,9 @@ type ReplyKeyboardMarkup struct { } type CallbackQuery struct { - ID string `json:"id"` - From *User `json:"user"` - Message *MaybeInaccessibleMessage `json:"message"` + ID string `json:"id"` + From *User `json:"from"` + Message *Message `json:"message"` Data string `json:"data"` } From 49ec217d33bd02d6fb2ed4849394fd37199cfdcb Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Thu, 22 Jan 2026 17:12:15 +0300 Subject: [PATCH 09/22] small fixes and addition --- bot.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bot.go b/bot.go index b2e6c31..7924f87 100644 --- a/bot.go +++ b/bot.go @@ -414,14 +414,21 @@ func (ctx *MsgContext) AnswerPhoto(photoId string, text string) *AnswerMessage { } } -func (m *AnswerMessage) Delete() { - _, err := m.ctx.Bot.DeleteMessage(&DeleteMessageP{ - MessageID: m.MessageID, ChatID: m.ctx.Msg.Chat.ID, +func (ctx *MsgContext) delete(messageId int) { + _, err := ctx.Bot.DeleteMessage(&DeleteMessageP{ + ChatID: ctx.Msg.Chat.ID, + MessageID: messageId, }) if err != nil { - m.ctx.Bot.logger.Error(err) + ctx.Bot.logger.Error(err) } } +func (m *AnswerMessage) Delete() { + m.ctx.delete(m.MessageID) +} +func (ctx *MsgContext) CallbackDelete() { + ctx.delete(ctx.CallbackMsgId) +} func (ctx *MsgContext) Error(err error) { _, sendErr := ctx.Bot.SendMessage(&SendMessageP{ From c503b68814c9f4c027fedc45cd5a7ebff698cf9d Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Thu, 22 Jan 2026 20:08:23 +0300 Subject: [PATCH 10/22] photo keyboard --- bot.go | 43 +++++++++++++++++++++++++++++++++++++++---- methods.go | 40 +++++++++++++++++++++------------------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/bot.go b/bot.go index 7924f87..c87e03b 100644 --- a/bot.go +++ b/bot.go @@ -370,6 +370,31 @@ func (ctx *MsgContext) EditCallbackf(format string, keyboard *InlineKeyboard, ar return ctx.EditCallback(fmt.Sprintf(format, args...), keyboard) } +func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeyboard) *AnswerMessage { + params := &EditMessageCaptionP{ + ChatID: ctx.Msg.Chat.ID, + MessageID: messageId, + Caption: text, + ParseMode: ParseMD, + } + if kb != nil { + params.ReplyMarkup = kb.Get() + } + msg, err := ctx.Bot.EditMessageCaption(params) + if err != nil { + ctx.Bot.logger.Error(err) + } + return &AnswerMessage{ + MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, + } +} +func (m *AnswerMessage) EditCaption(text string) *AnswerMessage { + return m.ctx.editPhotoText(m.MessageID, text, nil) +} +func (m *AnswerMessage) EditCaptionKeyboard(text string, kb *InlineKeyboard) *AnswerMessage { + return m.ctx.editPhotoText(m.MessageID, text, kb) +} + func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard) *AnswerMessage { params := &SendMessageP{ ChatID: ctx.Msg.Chat.ID, @@ -399,20 +424,30 @@ func (ctx *MsgContext) Keyboard(text string, kb *InlineKeyboard) *AnswerMessage return ctx.answer(text, kb) } -func (ctx *MsgContext) AnswerPhoto(photoId string, text string) *AnswerMessage { - _, err := ctx.Bot.SendPhoto(&SendPhotoP{ +func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard) *AnswerMessage { + params := &SendPhotoP{ ChatID: ctx.Msg.Chat.ID, Caption: text, Photo: photoId, ParseMode: ParseMD, - }) + } + if kb != nil { + params.ReplyMarkup = kb.Get() + } + msg, err := ctx.Bot.SendPhoto(params) if err != nil { ctx.Bot.logger.Error(err) } return &AnswerMessage{ - MessageID: ctx.Msg.MessageID, ctx: ctx, IsMedia: true, Text: text, + MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, } } +func (ctx *MsgContext) AnswerPhoto(photoId, text string) *AnswerMessage { + return ctx.answerPhoto(photoId, text, nil) +} +func (ctx *MsgContext) AnswerPhotoKeyboard(photoId, text string, kb *InlineKeyboard) *AnswerMessage { + return ctx.answerPhoto(photoId, text, kb) +} func (ctx *MsgContext) delete(messageId int) { _, err := ctx.Bot.DeleteMessage(&DeleteMessageP{ diff --git a/methods.go b/methods.go index 1695e64..19bdc9b 100644 --- a/methods.go +++ b/methods.go @@ -87,19 +87,20 @@ func (b *Bot) SendMessage(params *SendMessageP) (*Message, error) { } type SendPhotoP struct { - BusinessConnectionID string `json:"business_connection_id,omitempty"` - ChatID int `json:"chat_id"` - MessageThreadID int `json:"message_thread_id,omitempty"` - ParseMode ParseMode `json:"parse_mode,omitempty"` - Photo string `json:"photo"` - Caption string `json:"caption,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"` + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id"` + MessageThreadID int `json:"message_thread_id,omitempty"` + ParseMode ParseMode `json:"parse_mode,omitempty"` + Photo string `json:"photo"` + Caption string `json:"caption,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"` + ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` } func (b *Bot) SendPhoto(params *SendPhotoP) (*Message, error) { @@ -133,12 +134,13 @@ func (b *Bot) EditMessageText(params *EditMessageTextP) (*Message, error) { } type EditMessageCaptionP struct { - BusinessConnectionID string `json:"business_connection_id,omitempty"` - ChatID int `json:"chat_id,omitempty"` - MessageID int `json:"message_id,omitempty"` - InlineMessageID string `json:"inline_message_id,omitempty"` - Caption string `json:"caption"` - ParseMode ParseMode `json:"parse_mode,omitempty"` + BusinessConnectionID string `json:"business_connection_id,omitempty"` + ChatID int `json:"chat_id,omitempty"` + MessageID int `json:"message_id,omitempty"` + InlineMessageID string `json:"inline_message_id,omitempty"` + Caption string `json:"caption"` + ParseMode ParseMode `json:"parse_mode,omitempty"` + ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` } func (b *Bot) EditMessageCaption(params *EditMessageCaptionP) (*Message, error) { From 37397ba90f9865e8c2a2d06cf5443e948fa92c83 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Mon, 26 Jan 2026 14:36:42 +0300 Subject: [PATCH 11/22] added sleep on fetching updates --- bot.go | 1 + 1 file changed, 1 insertion(+) diff --git a/bot.go b/bot.go index c87e03b..e966900 100644 --- a/bot.go +++ b/bot.go @@ -218,6 +218,7 @@ func (b *Bot) Run() { if err != nil { b.logger.Error(err) } + time.Sleep(time.Millisecond * 10) } }() From 05dadc3de320a18a618e9997c550f407be0408c5 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 13:40:18 +0300 Subject: [PATCH 12/22] logger enchancment --- bot.go | 44 ++++++++++++++++++++----------- logger.go | 76 ++++++++++++++++++++++++++++++++++++++---------------- plugins.go | 6 +++-- 3 files changed, 87 insertions(+), 39 deletions(-) diff --git a/bot.go b/bot.go index e966900..dce1d24 100644 --- a/bot.go +++ b/bot.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "os" "sort" @@ -104,15 +105,28 @@ func NewBot(settings *BotSettings) *Bot { if settings.Debug { level = DEBUG } - bot.logger = CreateLogger().Level(level) + + bot.logger = CreateLogger().Level(level).PrintTraceback(true) + bot.logger.AddWriter(bot.logger.CreateStdoutWriter()) if settings.WriteToFile { - bot.logger = bot.logger.OpenFile(fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + path := fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/")) + fileWriter, err := bot.logger.CreateFileWriter(path) + if err != nil { + bot.logger.Fatal(err) + } + bot.logger.AddWriter(fileWriter) } - bot.logger = bot.logger.PrintTraceback(true) + if settings.UseRequestLogger { bot.requestLogger = CreateLogger().Level(level).Prefix("REQUESTS") + bot.requestLogger.AddWriter(bot.requestLogger.CreateStdoutWriter()) if settings.WriteToFile { - bot.requestLogger = bot.requestLogger.OpenFile(fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/"))) + path := fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/")) + fileWriter, err := bot.requestLogger.CreateFileWriter(path) + if err != nil { + bot.logger.Fatal(err) + } + bot.requestLogger.AddWriter(fileWriter) } } @@ -120,11 +134,11 @@ func NewBot(settings *BotSettings) *Bot { } func (b *Bot) Close() { - err := b.logger.f.Close() - if err != nil { - fmt.Println(err) - } else { - fmt.Println("log closed") + for _, writer := range b.logger.writers { + err := writer.Close() + if err != nil { + log.Println(err) + } } } @@ -132,11 +146,11 @@ func (b *Bot) InitDatabaseContext(ctx *DatabaseContext) *Bot { b.dbContext = ctx return b } -func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) LoggerWriter) *Bot { - w := []LoggerWriter{writer(b.dbContext)} - b.logger.AddWriters(w) +func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) *LoggerWriter) *Bot { + w := writer(b.dbContext) + b.logger.AddWriter(w) if b.requestLogger != nil { - b.requestLogger.AddWriters(w) + b.requestLogger.AddWriter(w) } return b } @@ -177,7 +191,7 @@ func (b *Bot) Debug(debug bool) *Bot { 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)) + b.logger.Debug(fmt.Sprintf("plugins with name \"%s\" registered", p.Name)) } return b } @@ -194,7 +208,7 @@ func (b *Bot) AddMiddleware(middleware ...*Middleware) *Bot { b.middlewares = append(b.middlewares, middleware...) for _, m := range middleware { - b.logger.Debug(fmt.Sprintf("middleware with name \"%s\" was registered", m.Name)) + b.logger.Debug(fmt.Sprintf("middleware with name \"%s\" registered", m.Name)) } return b } diff --git a/logger.go b/logger.go index da31495..483c514 100644 --- a/logger.go +++ b/logger.go @@ -2,6 +2,7 @@ package laniakea import ( "fmt" + "io" "os" "path/filepath" "runtime" @@ -12,16 +13,30 @@ import ( "github.com/fatih/color" ) -type LoggerWriter func(level LogLevel, prefix, traceback string, message []any) +type LoggerWriter struct { + writer io.Writer + writeFn LoggerWriterFn + logger *Logger +} + +func (w *LoggerWriter) SetFn(fn LoggerWriterFn) { + w.writeFn = fn +} +func (w *LoggerWriter) Write(p []byte) (n int, err error) { + return w.writer.Write(p) +} +func (w *LoggerWriter) Close() error { + return w.writer.(io.Closer).Close() +} + +type LoggerWriterFn func(level LogLevel, prefix, traceback string, message []any) error type Logger struct { prefix string level LogLevel printTraceback bool printTime bool - writers []LoggerWriter - - f *os.File + writers []*LoggerWriter } type LogLevel struct { @@ -60,16 +75,34 @@ func CreateLogger() *Logger { } } -func (l *Logger) OpenFile(name string) *Logger { - err := os.MkdirAll(filepath.Dir(name), os.ModePerm) +func (l *Logger) CreateFileWriter(path string) (*LoggerWriter, error) { + err := os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { - l.Fatal(err) + return nil, err } - l.f, err = os.OpenFile(name, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - l.Fatal(err) + return nil, err } - return l + + writer := &LoggerWriter{ + writer: file, logger: l, + } + writer.writeFn = func(level LogLevel, prefix, traceback string, message []any) error { + _, err = writer.Write([]byte(writer.logger.buildString(level, message) + "\n")) + return err + } + return writer, nil +} +func (l *Logger) CreateStdoutWriter() *LoggerWriter { + writer := &LoggerWriter{ + writer: os.Stdout, logger: l, + } + writer.writeFn = func(level LogLevel, prefix, traceback string, message []any) error { + _, err := color.New(level.c).Fprint(writer.writer, writer.logger.buildString(level, message)) + return err + } + return writer } func (l *Logger) Prefix(prefix string) *Logger { @@ -88,11 +121,18 @@ func (l *Logger) PrintTime(b bool) *Logger { l.printTime = b return l } -func (l *Logger) AddWriters(writers []LoggerWriter) *Logger { +func (l *Logger) AddWriters(writers ...*LoggerWriter) *Logger { l.writers = append(l.writers, writers...) return l } +func (l *Logger) AddWriter(writer *LoggerWriter) *Logger { + l.writers = append(l.writers, writer) + return l +} +func (l *Logger) Infof(format string, m ...any) { + l.print(INFO, []any{fmt.Sprintf(format, m...)}) +} func (l *Logger) Info(m ...any) { l.print(INFO, m) } @@ -209,19 +249,11 @@ func (l *Logger) print(level LogLevel, m []any) { if l.level.n < level.n { return } - _, err := color.New(level.c).Println(l.buildString(level, m)) - if err != nil { - l.Fatal(err) - return - } for _, writer := range l.writers { - writer(level, l.prefix, l.formatFullTraceback(l.getFullTraceback(4)), m) - } - - if l.f != nil { - if _, err := l.f.Write([]byte(l.buildString(level, m) + "\n")); err != nil { - l.Fatal(err) + err := writer.writeFn(level, l.prefix, l.formatFullTraceback(l.getFullTraceback(0)), m) + if err != nil { + l.Error(err) } } } diff --git a/plugins.go b/plugins.go index 29f5c27..eb56574 100644 --- a/plugins.go +++ b/plugins.go @@ -1,5 +1,7 @@ package laniakea +import "log" + type CommandExecutor func(ctx *MsgContext, dbContext *DatabaseContext) type PluginBuilder struct { @@ -44,8 +46,8 @@ func (p *PluginBuilder) UpdateListener(listener CommandExecutor) *PluginBuilder } func (p *PluginBuilder) Build() *Plugin { - if len(p.commands) == 0 { - return nil + if len(p.commands) == 0 && len(p.payloads) == 0 { + log.Println("no command or payloads") } plugin := &Plugin{ Name: p.name, From 8f8182039d0fb56192248a94abc3d1531e96f84a Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 16:31:02 +0300 Subject: [PATCH 13/22] v0.2.0 --- bot.go => laniakea/bot.go | 0 keyboard.go => laniakea/keyboard.go | 0 logger.go => laniakea/logger.go | 0 methods.go => laniakea/methods.go | 0 plugins.go => laniakea/plugins.go | 0 queue.go => laniakea/queue.go | 0 types.go => laniakea/types.go | 0 utils.go => laniakea/utils.go | 0 version.go => laniakea/version.go | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename bot.go => laniakea/bot.go (100%) rename keyboard.go => laniakea/keyboard.go (100%) rename logger.go => laniakea/logger.go (100%) rename methods.go => laniakea/methods.go (100%) rename plugins.go => laniakea/plugins.go (100%) rename queue.go => laniakea/queue.go (100%) rename types.go => laniakea/types.go (100%) rename utils.go => laniakea/utils.go (100%) rename version.go => laniakea/version.go (100%) diff --git a/bot.go b/laniakea/bot.go similarity index 100% rename from bot.go rename to laniakea/bot.go diff --git a/keyboard.go b/laniakea/keyboard.go similarity index 100% rename from keyboard.go rename to laniakea/keyboard.go diff --git a/logger.go b/laniakea/logger.go similarity index 100% rename from logger.go rename to laniakea/logger.go diff --git a/methods.go b/laniakea/methods.go similarity index 100% rename from methods.go rename to laniakea/methods.go diff --git a/plugins.go b/laniakea/plugins.go similarity index 100% rename from plugins.go rename to laniakea/plugins.go diff --git a/queue.go b/laniakea/queue.go similarity index 100% rename from queue.go rename to laniakea/queue.go diff --git a/types.go b/laniakea/types.go similarity index 100% rename from types.go rename to laniakea/types.go diff --git a/utils.go b/laniakea/utils.go similarity index 100% rename from utils.go rename to laniakea/utils.go diff --git a/version.go b/laniakea/version.go similarity index 100% rename from version.go rename to laniakea/version.go From ee51d29c3008f8466a1afb506c74962172c9118d Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 16:41:26 +0300 Subject: [PATCH 14/22] v0.2.0 --- go.mod | 28 +++++++++++++++++++ go.sum | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea68a59 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +module git.nix13.pw/ScuroNeko/Laniakea + +go 1.25.6 + +require ( + github.com/fatih/color v1.18.0 + github.com/redis/go-redis/v9 v9.17.3 + github.com/vinovest/sqlx v1.7.1 + go.mongodb.org/mongo-driver/v2 v2.4.2 +) + +require ( + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/muir/sqltoken v0.1.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7c6a547 --- /dev/null +++ b/go.sum @@ -0,0 +1,87 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/muir/sqltoken v0.1.0 h1:edosEGsOClOZNfgGQNQSgxR9O6LiVefm2rDRqp2InuI= +github.com/muir/sqltoken v0.1.0/go.mod h1:lgOIORnKekMsuc/ZwdPOfwz/PtWLPCke43cEbT3uDuY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4= +github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vinovest/sqlx v1.7.1 h1:kdq4v0N9kRLpytWGSWOw4aulOGdQPmIoMR6Y+cTBxow= +github.com/vinovest/sqlx v1.7.1/go.mod h1:3fAv74r4iDMv2PpFomADb+vex5ukzfYn4GseC9KngD8= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver/v2 v2.4.2 h1:HrJ+Auygxceby9MLp3YITobef5a8Bv4HcPFIkml1U7U= +go.mongodb.org/mongo-driver/v2 v2.4.2/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 75be66d5a9d84b77bd34a524942f9e9b0102c209 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 16:42:45 +0300 Subject: [PATCH 15/22] v0.2.2 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ea68a59..208135b 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.25.6 From d1c75ac0a68005fe139e6fe44a82152834d9778b Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 16:44:11 +0300 Subject: [PATCH 16/22] v0.2.3 --- laniakea/bot.go | 12 +-- laniakea/logger.go | 243 +++++++++++++++++++++++++++++++++------------ 2 files changed, 184 insertions(+), 71 deletions(-) diff --git a/laniakea/bot.go b/laniakea/bot.go index dce1d24..8901e7e 100644 --- a/laniakea/bot.go +++ b/laniakea/bot.go @@ -106,11 +106,11 @@ func NewBot(settings *BotSettings) *Bot { level = DEBUG } - bot.logger = CreateLogger().Level(level).PrintTraceback(true) - bot.logger.AddWriter(bot.logger.CreateStdoutWriter()) + bot.logger = CreateLogger().Level(level) + bot.logger.AddWriter(bot.logger.CreateJsonStdoutWriter()) if settings.WriteToFile { path := fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/")) - fileWriter, err := bot.logger.CreateFileWriter(path) + fileWriter, err := bot.logger.CreateTextFileWriter(path) if err != nil { bot.logger.Fatal(err) } @@ -119,10 +119,10 @@ func NewBot(settings *BotSettings) *Bot { if settings.UseRequestLogger { bot.requestLogger = CreateLogger().Level(level).Prefix("REQUESTS") - bot.requestLogger.AddWriter(bot.requestLogger.CreateStdoutWriter()) + bot.requestLogger.AddWriter(bot.requestLogger.CreateJsonStdoutWriter()) if settings.WriteToFile { path := fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/")) - fileWriter, err := bot.requestLogger.CreateFileWriter(path) + fileWriter, err := bot.requestLogger.CreateTextFileWriter(path) if err != nil { bot.logger.Fatal(err) } @@ -146,7 +146,7 @@ func (b *Bot) InitDatabaseContext(ctx *DatabaseContext) *Bot { b.dbContext = ctx return b } -func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) *LoggerWriter) *Bot { +func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) LoggerWriter) *Bot { w := writer(b.dbContext) b.logger.AddWriter(w) if b.requestLogger != nil { diff --git a/laniakea/logger.go b/laniakea/logger.go index 483c514..b4ae674 100644 --- a/laniakea/logger.go +++ b/laniakea/logger.go @@ -1,6 +1,8 @@ package laniakea import ( + "bufio" + "encoding/json" "fmt" "io" "os" @@ -13,30 +15,91 @@ import ( "github.com/fatih/color" ) -type LoggerWriter struct { - writer io.Writer - writeFn LoggerWriterFn - logger *Logger +type LoggerWriter interface { + Close() error + Write(p []byte) (n int, err error) + Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error +} +type LoggerTextWriter struct { + LoggerWriter + writer io.Writer + printTraceback bool + printTime bool } -func (w *LoggerWriter) SetFn(fn LoggerWriterFn) { - w.writeFn = fn +func (w *LoggerTextWriter) Write(p []byte) (n int, err error) { + n, err = w.writer.Write(p) + if err != nil { + return n, err + } + err = bufio.NewWriter(w.writer).Flush() + return n, err } -func (w *LoggerWriter) Write(p []byte) (n int, err error) { - return w.writer.Write(p) +func (w *LoggerTextWriter) Print(level LogLevel, prefix string, tb []*MethodTraceback, messages ...any) error { + s := buildString(level, prefix, true, w.printTraceback, w.printTime, messages...) + _, err := w.Write([]byte(s)) + return err } -func (w *LoggerWriter) Close() error { +func (w *LoggerTextWriter) Close() error { return w.writer.(io.Closer).Close() } -type LoggerWriterFn func(level LogLevel, prefix, traceback string, message []any) error +type LoggerJsonWriter struct { + LoggerWriter + writer io.Writer + pretty bool +} +type LoggerJsonMessage struct { + Time time.Time `json:"time"` + Level string `json:"level"` + Prefix string `json:"prefix"` + Message string `json:"message"` + Traceback []*MethodTraceback `json:"traceback"` +} + +func (w *LoggerJsonWriter) Write(data []byte) (int, error) { + n, err := w.writer.Write(data) + if err != nil { + return n, err + } + err = bufio.NewWriter(w.writer).Flush() + return n, err +} +func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error { + msg := Map(messages, func(el any) string { + return fmt.Sprintf("%v", el) + }) + m := LoggerJsonMessage{ + Time: time.Now(), + Level: level.GetName(), + Prefix: prefix, + Message: strings.Join(msg, " "), + Traceback: traceback, + } + var data []byte + var err error + if w.pretty { + data, err = json.MarshalIndent(m, "", " ") + } else { + data, err = json.Marshal(m) + } + if err != nil { + return err + } + _, err = w.Write(append(data, []byte("\n")...)) + return err +} +func (w *LoggerJsonWriter) Close() error { + return w.writer.(io.Closer).Close() +} type Logger struct { prefix string level LogLevel printTraceback bool printTime bool - writers []*LoggerWriter + jsonPretty bool + writers []LoggerWriter } type LogLevel struct { @@ -50,12 +113,12 @@ func (l *LogLevel) GetName() string { } type MethodTraceback struct { - Package string - Method string + Package string `json:"package"` + Method string `json:"method"` fullPath string signature string - filename string - line int + Filename string `json:"filename"` + Line int `json:"line"` } var ( @@ -75,7 +138,7 @@ func CreateLogger() *Logger { } } -func (l *Logger) CreateFileWriter(path string) (*LoggerWriter, error) { +func (l *Logger) CreateTextFileWriter(path string) (*LoggerTextWriter, error) { err := os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { return nil, err @@ -85,22 +148,46 @@ func (l *Logger) CreateFileWriter(path string) (*LoggerWriter, error) { return nil, err } - writer := &LoggerWriter{ - writer: file, logger: l, - } - writer.writeFn = func(level LogLevel, prefix, traceback string, message []any) error { - _, err = writer.Write([]byte(writer.logger.buildString(level, message) + "\n")) - return err + writer := &LoggerTextWriter{ + writer: file, printTraceback: l.printTraceback, printTime: l.printTime, } return writer, nil } -func (l *Logger) CreateStdoutWriter() *LoggerWriter { - writer := &LoggerWriter{ - writer: os.Stdout, logger: l, +func (l *Logger) CreateTextStdoutWriter() *LoggerTextWriter { + writer := &LoggerTextWriter{ + writer: os.Stdout, printTraceback: l.printTraceback, printTime: l.printTime, } - writer.writeFn = func(level LogLevel, prefix, traceback string, message []any) error { - _, err := color.New(level.c).Fprint(writer.writer, writer.logger.buildString(level, message)) - return err + return writer +} +func (l *Logger) CreateJsonStdoutWriter() *LoggerJsonWriter { + writer := &LoggerJsonWriter{ + writer: os.Stdout, pretty: l.jsonPretty, + } + return writer +} +func (l *Logger) CreateJsonFileWriter(path string) (*LoggerJsonWriter, error) { + err := os.MkdirAll(filepath.Dir(path), os.ModePerm) + if err != nil { + return nil, err + } + file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, err + } + writer := &LoggerJsonWriter{ + writer: file, pretty: l.jsonPretty, + } + return writer, nil +} +func (l *Logger) CreateTextWriter(w io.Writer) *LoggerTextWriter { + writer := &LoggerTextWriter{ + writer: w, printTraceback: l.printTraceback, printTime: l.printTime, + } + return writer +} +func (l *Logger) CreateJsonWriter(w io.Writer) *LoggerJsonWriter { + writer := &LoggerJsonWriter{ + writer: w, pretty: l.jsonPretty, } return writer } @@ -121,44 +208,61 @@ func (l *Logger) PrintTime(b bool) *Logger { l.printTime = b return l } -func (l *Logger) AddWriters(writers ...*LoggerWriter) *Logger { +func (l *Logger) JsonPretty(b bool) *Logger { + l.jsonPretty = b + return l +} +func (l *Logger) AddWriters(writers ...LoggerWriter) *Logger { l.writers = append(l.writers, writers...) return l } -func (l *Logger) AddWriter(writer *LoggerWriter) *Logger { +func (l *Logger) AddWriter(writer LoggerWriter) *Logger { l.writers = append(l.writers, writer) return l } -func (l *Logger) Infof(format string, m ...any) { - l.print(INFO, []any{fmt.Sprintf(format, m...)}) +func (l *Logger) Infof(format string, args ...any) { + l.print(INFO, fmt.Sprintf(format, args...)) } func (l *Logger) Info(m ...any) { - l.print(INFO, m) + l.println(INFO, m...) } +func (l *Logger) Warnf(format string, args ...any) { + l.print(WARN, fmt.Sprintf(format, args...)) +} func (l *Logger) Warn(m ...any) { - l.print(WARN, m) + l.println(WARN, m...) } func (l *Logger) Error(m ...any) { - l.print(ERROR, m) + l.println(ERROR, m...) } func (l *Logger) Fatal(m ...any) { - l.print(FATAL, m) + l.println(FATAL, m...) os.Exit(1) } func (l *Logger) Debug(m ...any) { - l.print(DEBUG, m) + l.println(DEBUG, m...) } -func (l *Logger) formatTime(t time.Time) string { +func 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 formatTraceback(mt *MethodTraceback) string { + return fmt.Sprintf("%s:%s:%d", mt.Filename, mt.Method, mt.Line) +} +func FormatFullTraceback(tracebacks []*MethodTraceback) string { + formatted := make([]string, 0) + for _, tb := range tracebacks { + formatted = append(formatted, formatTraceback(tb)) + } + return strings.Join(formatted, "->") +} -func (l *Logger) getTraceback() *MethodTraceback { +func getTraceback() *MethodTraceback { caller, _, _, _ := runtime.Caller(4) details := runtime.FuncForPC(caller) signature := details.Name() @@ -169,9 +273,9 @@ func (l *Logger) getTraceback() *MethodTraceback { pkg, method := splitSignature[0], splitSignature[len(splitSignature)-1] tb := &MethodTraceback{ - filename: splitPath[len(splitPath)-1], + Filename: splitPath[len(splitPath)-1], fullPath: path, - line: line, + Line: line, signature: signature, Package: pkg, Method: method, @@ -179,11 +283,7 @@ func (l *Logger) getTraceback() *MethodTraceback { return tb } -func (l *Logger) formatTraceback(mt *MethodTraceback) string { - return fmt.Sprintf("%s:%s:%d", mt.filename, mt.Method, mt.line) -} - -func (l *Logger) getFullTraceback(skip int) []*MethodTraceback { +func getFullTraceback(skip int) []*MethodTraceback { pc := make([]uintptr, 15) runtime.Callers(skip, pc) list := make([]*MethodTraceback, 0) @@ -202,9 +302,9 @@ func (l *Logger) getFullTraceback(skip int) []*MethodTraceback { pkg, method := splitSignature[0], splitSignature[len(splitSignature)-1] tb := &MethodTraceback{ - filename: splitPath[len(splitPath)-1], + Filename: splitPath[len(splitPath)-1], fullPath: path, - line: line, + Line: line, signature: signature, Package: pkg, Method: method, @@ -216,42 +316,55 @@ func (l *Logger) getFullTraceback(skip int) []*MethodTraceback { }) return list } -func (l *Logger) formatFullTraceback(tracebacks []*MethodTraceback) string { - formatted := make([]string, 0) - for _, tb := range tracebacks { - formatted = append(formatted, l.formatTraceback(tb)) - } - return strings.Join(formatted, "->") -} -func (l *Logger) buildString(level LogLevel, m []any) string { +func buildString(level LogLevel, prefix string, newline, printTime, printTraceback bool, m ...any) string { args := []string{ - fmt.Sprintf("[%s]", l.prefix), + fmt.Sprintf("[%s]", prefix), fmt.Sprintf("[%s]", strings.ToUpper(level.t)), } - if l.printTraceback { - args = append(args, fmt.Sprintf("[%s]", l.formatTraceback(l.getTraceback()))) + if printTraceback { + args = append(args, fmt.Sprintf("[%s]", formatTraceback(getTraceback()))) } - if l.printTime { - args = append(args, fmt.Sprintf("[%s]", l.formatTime(time.Now()))) + if printTime { + args = append(args, fmt.Sprintf("[%s]", 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, " ")) + s := fmt.Sprintf("%s %s", strings.Join(args, " "), strings.Join(msg, " ")) + if newline { + s += "\n" + } + return s } -func (l *Logger) print(level LogLevel, m []any) { +func (l *Logger) print(level LogLevel, m ...any) { if l.level.n < level.n { return } + tb := getFullTraceback(0) for _, writer := range l.writers { - err := writer.writeFn(level, l.prefix, l.formatFullTraceback(l.getFullTraceback(0)), m) + err := writer.Print(level, l.prefix, tb, m...) + if err != nil { + l.Error(err) + } + } +} + +// Docker requires "\n" at end to write to log. +// print not work for docker, otherwise it will work and write into stdout +func (l *Logger) println(level LogLevel, m ...any) { + if l.level.n < level.n { + return + } + + tb := getFullTraceback(0) + for _, writer := range l.writers { + err := writer.Print(level, l.prefix, tb, m...) if err != nil { l.Error(err) } From 199940359d8cfd30279cea8afd3ffbd4b41ae199 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 16:46:44 +0300 Subject: [PATCH 17/22] v0.2.4 --- .idea/Laniakea.iml | 9 +++ .idea/go.imports.xml | 11 ++++ .idea/inspectionProfiles/Project_Default.xml | 14 ++++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ .idea/workspace.xml | 67 ++++++++++++++++++++ laniakea/bot.go => bot.go | 0 laniakea/keyboard.go => keyboard.go | 0 laniakea/logger.go => logger.go | 0 laniakea/methods.go => methods.go | 0 laniakea/plugins.go => plugins.go | 0 laniakea/queue.go => queue.go | 0 laniakea/types.go => types.go | 0 laniakea/utils.go => utils.go | 0 laniakea/version.go => version.go | 0 15 files changed, 115 insertions(+) create mode 100644 .idea/Laniakea.iml create mode 100644 .idea/go.imports.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml rename laniakea/bot.go => bot.go (100%) rename laniakea/keyboard.go => keyboard.go (100%) rename laniakea/logger.go => logger.go (100%) rename laniakea/methods.go => methods.go (100%) rename laniakea/plugins.go => plugins.go (100%) rename laniakea/queue.go => queue.go (100%) rename laniakea/types.go => types.go (100%) rename laniakea/utils.go => utils.go (100%) rename laniakea/version.go => version.go (100%) diff --git a/.idea/Laniakea.iml b/.idea/Laniakea.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/Laniakea.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..d7202f0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..967f534 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f13cae3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..cd5629b --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1769606862833 + + + + \ No newline at end of file diff --git a/laniakea/bot.go b/bot.go similarity index 100% rename from laniakea/bot.go rename to bot.go diff --git a/laniakea/keyboard.go b/keyboard.go similarity index 100% rename from laniakea/keyboard.go rename to keyboard.go diff --git a/laniakea/logger.go b/logger.go similarity index 100% rename from laniakea/logger.go rename to logger.go diff --git a/laniakea/methods.go b/methods.go similarity index 100% rename from laniakea/methods.go rename to methods.go diff --git a/laniakea/plugins.go b/plugins.go similarity index 100% rename from laniakea/plugins.go rename to plugins.go diff --git a/laniakea/queue.go b/queue.go similarity index 100% rename from laniakea/queue.go rename to queue.go diff --git a/laniakea/types.go b/types.go similarity index 100% rename from laniakea/types.go rename to types.go diff --git a/laniakea/utils.go b/utils.go similarity index 100% rename from laniakea/utils.go rename to utils.go diff --git a/laniakea/version.go b/version.go similarity index 100% rename from laniakea/version.go rename to version.go From 601eb99b9b52e9c9040a5ea29fb1bbe9cc8fdeb3 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 16:47:43 +0300 Subject: [PATCH 18/22] v0.2.4 --- .gitignore | 1 + .idea/Laniakea.iml | 9 --- .idea/go.imports.xml | 11 ---- .idea/inspectionProfiles/Project_Default.xml | 14 ---- .idea/modules.xml | 8 --- .idea/vcs.xml | 6 -- .idea/workspace.xml | 67 -------------------- 7 files changed, 1 insertion(+), 115 deletions(-) create mode 100644 .gitignore delete mode 100644 .idea/Laniakea.iml delete mode 100644 .idea/go.imports.xml delete mode 100644 .idea/inspectionProfiles/Project_Default.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/workspace.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f11b75 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ diff --git a/.idea/Laniakea.iml b/.idea/Laniakea.iml deleted file mode 100644 index 5e764c4..0000000 --- a/.idea/Laniakea.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml deleted file mode 100644 index d7202f0..0000000 --- a/.idea/go.imports.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 967f534..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index f13cae3..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index cd5629b..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1769606862833 - - - - \ No newline at end of file From 4bee2e5168880b6d80442b34f23ced16846cfa54 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 17:40:56 +0300 Subject: [PATCH 19/22] logger now external package and some chages --- .gitignore | 1 + bot.go | 70 +++++----- go.mod | 9 +- go.sum | 12 +- logger.go | 372 ----------------------------------------------------- methods.go | 2 +- queue.go | 6 +- version.go | 8 +- 8 files changed, 49 insertions(+), 431 deletions(-) delete mode 100644 logger.go diff --git a/.gitignore b/.gitignore index 9f11b75..4d72fe9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea/ +test/ \ No newline at end of file diff --git a/bot.go b/bot.go index 8901e7e..9d1cea9 100644 --- a/bot.go +++ b/bot.go @@ -5,13 +5,13 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "os" "sort" "strings" "time" + "git.nix13.pw/scuroneko/slog" "github.com/redis/go-redis/v9" "github.com/vinovest/sqlx" "go.mongodb.org/mongo-driver/v2/mongo" @@ -30,8 +30,8 @@ type Bot struct { debug bool errorTemplate string - logger *Logger - requestLogger *Logger + logger *slog.Logger + requestLogger *slog.Logger plugins []*Plugin middlewares []*Middleware @@ -101,12 +101,12 @@ func NewBot(settings *BotSettings) *Bot { if len(settings.LoggerBasePath) == 0 { settings.LoggerBasePath = "./" } - level := FATAL + level := slog.FATAL if settings.Debug { - level = DEBUG + level = slog.DEBUG } - bot.logger = CreateLogger().Level(level) + bot.logger = slog.CreateLogger().Level(level).Prefix("BOT") bot.logger.AddWriter(bot.logger.CreateJsonStdoutWriter()) if settings.WriteToFile { path := fmt.Sprintf("%s/main.log", strings.TrimRight(settings.LoggerBasePath, "/")) @@ -118,7 +118,7 @@ func NewBot(settings *BotSettings) *Bot { } if settings.UseRequestLogger { - bot.requestLogger = CreateLogger().Level(level).Prefix("REQUESTS") + bot.requestLogger = slog.CreateLogger().Level(level).Prefix("REQUESTS") bot.requestLogger.AddWriter(bot.requestLogger.CreateJsonStdoutWriter()) if settings.WriteToFile { path := fmt.Sprintf("%s/requests.log", strings.TrimRight(settings.LoggerBasePath, "/")) @@ -134,19 +134,15 @@ func NewBot(settings *BotSettings) *Bot { } func (b *Bot) Close() { - for _, writer := range b.logger.writers { - err := writer.Close() - if err != nil { - log.Println(err) - } - } + b.logger.Close() + b.requestLogger.Close() } func (b *Bot) InitDatabaseContext(ctx *DatabaseContext) *Bot { b.dbContext = ctx return b } -func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) LoggerWriter) *Bot { +func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) slog.LoggerWriter) *Bot { w := writer(b.dbContext) b.logger.AddWriter(w) if b.requestLogger != nil { @@ -164,12 +160,10 @@ 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 { @@ -177,25 +171,21 @@ func LoadPrefixesFromEnv() []string { } return strings.Split(prefixesS, ";") } - func (b *Bot) ErrorTemplate(s string) *Bot { b.errorTemplate = s return b } - -func (b *Bot) Debug(debug bool) *Bot { +func (b *Bot) Debugln(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\" registered", p.Name)) + b.logger.Debugln(fmt.Sprintf("plugins with name \"%s\" registered", p.Name)) } return b } - func (b *Bot) AddMiddleware(middleware ...*Middleware) *Bot { sort.Slice(middleware, func(a, b int) bool { first := middleware[a] @@ -208,29 +198,29 @@ func (b *Bot) AddMiddleware(middleware ...*Middleware) *Bot { b.middlewares = append(b.middlewares, middleware...) for _, m := range middleware { - b.logger.Debug(fmt.Sprintf("middleware with name \"%s\" registered", m.Name)) + b.logger.Debugln(fmt.Sprintf("middleware with name \"%s\" registered", m.Name)) } return b } func (b *Bot) Run() { if len(b.prefixes) == 0 { - b.logger.Fatal("no prefixes defined") + b.logger.Fatalln("no prefixes defined") return } if len(b.plugins) == 0 { - b.logger.Fatal("no plugins defined") + b.logger.Fatalln("no plugins defined") return } - b.logger.Info("Bot running. Press CTRL+C to exit.") + b.logger.Infoln("Bot running. Press CTRL+C to exit.") go func() { for { _, err := b.Updates() if err != nil { - b.logger.Error(err) + b.logger.Errorln(err) } time.Sleep(time.Millisecond * 10) } @@ -245,7 +235,7 @@ func (b *Bot) Run() { u := queue.Dequeue() if u == nil { - b.logger.Error("update is nil") + b.logger.Errorln("update is nil") continue } ctx := &MsgContext{ @@ -314,7 +304,7 @@ func (b *Bot) handleCallback(update *Update, ctx *MsgContext) { data := new(CallbackData) err := json.Unmarshal([]byte(update.CallbackQuery.Data), data) if err != nil { - b.logger.Error(err) + b.logger.Errorln(err) return } @@ -363,7 +353,7 @@ func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard } msg, err := ctx.Bot.EditMessageText(params) if err != nil { - ctx.Bot.logger.Error(err) + ctx.Bot.logger.Errorln(err) return nil } return &AnswerMessage{ @@ -375,7 +365,7 @@ func (m *AnswerMessage) Edit(text string) *AnswerMessage { } func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage { if ctx.CallbackMsgId == 0 { - ctx.Bot.logger.Error("Can't edit non-callback update message") + ctx.Bot.logger.Errorln("Can't edit non-callback update message") return nil } @@ -397,7 +387,7 @@ func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeybo } msg, err := ctx.Bot.EditMessageCaption(params) if err != nil { - ctx.Bot.logger.Error(err) + ctx.Bot.logger.Errorln(err) } return &AnswerMessage{ MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, @@ -422,7 +412,7 @@ func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard) *AnswerMess msg, err := ctx.Bot.SendMessage(params) if err != nil { - ctx.Bot.logger.Error(err) + ctx.Bot.logger.Errorln(err) return nil } return &AnswerMessage{ @@ -451,7 +441,7 @@ func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard) *An } msg, err := ctx.Bot.SendPhoto(params) if err != nil { - ctx.Bot.logger.Error(err) + ctx.Bot.logger.Errorln(err) } return &AnswerMessage{ MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, @@ -470,7 +460,7 @@ func (ctx *MsgContext) delete(messageId int) { MessageID: messageId, }) if err != nil { - ctx.Bot.logger.Error(err) + ctx.Bot.logger.Errorln(err) } } func (m *AnswerMessage) Delete() { @@ -485,14 +475,14 @@ func (ctx *MsgContext) Error(err error) { ChatID: ctx.Msg.Chat.ID, Text: fmt.Sprintf(ctx.Bot.errorTemplate, EscapeMarkdown(err.Error())), }) - ctx.Bot.logger.Error(err) + ctx.Bot.logger.Errorln(err) if sendErr != nil { - ctx.Bot.logger.Error(sendErr) + ctx.Bot.logger.Errorln(sendErr) } } -func (b *Bot) Logger() *Logger { +func (b *Bot) Logger() *slog.Logger { return b.logger } @@ -519,7 +509,7 @@ func (b *Bot) request(methodName string, params any) (map[string]interface{}, er } if b.debug && b.requestLogger != nil { - b.requestLogger.Debug(strings.ReplaceAll(fmt.Sprintf( + b.requestLogger.Debugln(strings.ReplaceAll(fmt.Sprintf( "POST https://api.telegram.org/bot%s/%s %s", "", methodName, @@ -535,7 +525,7 @@ func (b *Bot) request(methodName string, params any) (map[string]interface{}, er if err != nil { return nil, err } - b.requestLogger.Debug(fmt.Sprintf("RES %s %s", methodName, string(data))) + b.requestLogger.Debugln(fmt.Sprintf("RES %s %s", methodName, string(data))) response := new(ApiResponse) var result map[string]any diff --git a/go.mod b/go.mod index 208135b..519aafc 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,9 @@ module git.nix13.pw/scuroneko/laniakea -go 1.25.6 +go 1.25 require ( - github.com/fatih/color v1.18.0 + git.nix13.pw/scuroneko/slog v1.0.0 github.com/redis/go-redis/v9 v9.17.3 github.com/vinovest/sqlx v1.7.1 go.mongodb.org/mongo-driver/v2 v2.4.2 @@ -12,9 +12,10 @@ require ( require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/color v1.18.0 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/klauspost/compress v1.16.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/muir/sqltoken v0.1.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -23,6 +24,6 @@ require ( github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/crypto v0.33.0 // indirect golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 7c6a547..798ad5b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.nix13.pw/scuroneko/slog v1.0.0 h1:PI0YePrmCopjrljUfwCtBIEwNYB+PBgDzPcCXbetpcE= +git.nix13.pw/scuroneko/slog v1.0.0/go.mod h1:3Qm2wzkR5KjwOponMfG7TcGSDjmYaFqRAmLvSPTuWJI= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -22,9 +24,8 @@ github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGC github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= @@ -67,10 +68,9 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/logger.go b/logger.go deleted file mode 100644 index b4ae674..0000000 --- a/logger.go +++ /dev/null @@ -1,372 +0,0 @@ -package laniakea - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "sort" - "strings" - "time" - - "github.com/fatih/color" -) - -type LoggerWriter interface { - Close() error - Write(p []byte) (n int, err error) - Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error -} -type LoggerTextWriter struct { - LoggerWriter - writer io.Writer - printTraceback bool - printTime bool -} - -func (w *LoggerTextWriter) Write(p []byte) (n int, err error) { - n, err = w.writer.Write(p) - if err != nil { - return n, err - } - err = bufio.NewWriter(w.writer).Flush() - return n, err -} -func (w *LoggerTextWriter) Print(level LogLevel, prefix string, tb []*MethodTraceback, messages ...any) error { - s := buildString(level, prefix, true, w.printTraceback, w.printTime, messages...) - _, err := w.Write([]byte(s)) - return err -} -func (w *LoggerTextWriter) Close() error { - return w.writer.(io.Closer).Close() -} - -type LoggerJsonWriter struct { - LoggerWriter - writer io.Writer - pretty bool -} -type LoggerJsonMessage struct { - Time time.Time `json:"time"` - Level string `json:"level"` - Prefix string `json:"prefix"` - Message string `json:"message"` - Traceback []*MethodTraceback `json:"traceback"` -} - -func (w *LoggerJsonWriter) Write(data []byte) (int, error) { - n, err := w.writer.Write(data) - if err != nil { - return n, err - } - err = bufio.NewWriter(w.writer).Flush() - return n, err -} -func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error { - msg := Map(messages, func(el any) string { - return fmt.Sprintf("%v", el) - }) - m := LoggerJsonMessage{ - Time: time.Now(), - Level: level.GetName(), - Prefix: prefix, - Message: strings.Join(msg, " "), - Traceback: traceback, - } - var data []byte - var err error - if w.pretty { - data, err = json.MarshalIndent(m, "", " ") - } else { - data, err = json.Marshal(m) - } - if err != nil { - return err - } - _, err = w.Write(append(data, []byte("\n")...)) - return err -} -func (w *LoggerJsonWriter) Close() error { - return w.writer.(io.Closer).Close() -} - -type Logger struct { - prefix string - level LogLevel - printTraceback bool - printTime bool - jsonPretty bool - writers []LoggerWriter -} - -type LogLevel struct { - n uint8 - t string - c color.Attribute -} - -func (l *LogLevel) GetName() string { - return l.t -} - -type MethodTraceback struct { - Package string `json:"package"` - Method string `json:"method"` - fullPath string - signature string - Filename string `json:"filename"` - Line int `json:"line"` -} - -var ( - INFO = LogLevel{n: 0, t: "info", c: color.FgWhite} - WARN = LogLevel{n: 1, t: "warn", c: color.FgHiYellow} - ERROR = LogLevel{n: 2, t: "error", c: color.FgHiRed} - FATAL = LogLevel{n: 3, t: "fatal", c: color.FgRed} - DEBUG = LogLevel{n: 4, t: "debug", c: color.FgGreen} -) - -func CreateLogger() *Logger { - return &Logger{ - prefix: "LOG", - level: FATAL, - printTraceback: false, - printTime: true, - } -} - -func (l *Logger) CreateTextFileWriter(path string) (*LoggerTextWriter, error) { - err := os.MkdirAll(filepath.Dir(path), os.ModePerm) - if err != nil { - return nil, err - } - file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - - writer := &LoggerTextWriter{ - writer: file, printTraceback: l.printTraceback, printTime: l.printTime, - } - return writer, nil -} -func (l *Logger) CreateTextStdoutWriter() *LoggerTextWriter { - writer := &LoggerTextWriter{ - writer: os.Stdout, printTraceback: l.printTraceback, printTime: l.printTime, - } - return writer -} -func (l *Logger) CreateJsonStdoutWriter() *LoggerJsonWriter { - writer := &LoggerJsonWriter{ - writer: os.Stdout, pretty: l.jsonPretty, - } - return writer -} -func (l *Logger) CreateJsonFileWriter(path string) (*LoggerJsonWriter, error) { - err := os.MkdirAll(filepath.Dir(path), os.ModePerm) - if err != nil { - return nil, err - } - file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - writer := &LoggerJsonWriter{ - writer: file, pretty: l.jsonPretty, - } - return writer, nil -} -func (l *Logger) CreateTextWriter(w io.Writer) *LoggerTextWriter { - writer := &LoggerTextWriter{ - writer: w, printTraceback: l.printTraceback, printTime: l.printTime, - } - return writer -} -func (l *Logger) CreateJsonWriter(w io.Writer) *LoggerJsonWriter { - writer := &LoggerJsonWriter{ - writer: w, pretty: l.jsonPretty, - } - return writer -} - -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) PrintTime(b bool) *Logger { - l.printTime = b - return l -} -func (l *Logger) JsonPretty(b bool) *Logger { - l.jsonPretty = b - return l -} -func (l *Logger) AddWriters(writers ...LoggerWriter) *Logger { - l.writers = append(l.writers, writers...) - return l -} -func (l *Logger) AddWriter(writer LoggerWriter) *Logger { - l.writers = append(l.writers, writer) - return l -} - -func (l *Logger) Infof(format string, args ...any) { - l.print(INFO, fmt.Sprintf(format, args...)) -} -func (l *Logger) Info(m ...any) { - l.println(INFO, m...) -} - -func (l *Logger) Warnf(format string, args ...any) { - l.print(WARN, fmt.Sprintf(format, args...)) -} -func (l *Logger) Warn(m ...any) { - l.println(WARN, m...) -} - -func (l *Logger) Error(m ...any) { - l.println(ERROR, m...) -} - -func (l *Logger) Fatal(m ...any) { - l.println(FATAL, m...) - os.Exit(1) -} - -func (l *Logger) Debug(m ...any) { - l.println(DEBUG, m...) -} - -func 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 formatTraceback(mt *MethodTraceback) string { - return fmt.Sprintf("%s:%s:%d", mt.Filename, mt.Method, mt.Line) -} -func FormatFullTraceback(tracebacks []*MethodTraceback) string { - formatted := make([]string, 0) - for _, tb := range tracebacks { - formatted = append(formatted, formatTraceback(tb)) - } - return strings.Join(formatted, "->") -} - -func 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 getFullTraceback(skip int) []*MethodTraceback { - pc := make([]uintptr, 15) - runtime.Callers(skip, pc) - list := make([]*MethodTraceback, 0) - frames := runtime.CallersFrames(pc) - for { - frame, more := frames.Next() - if !more { - break - } - details := runtime.FuncForPC(frame.PC) - signature := details.Name() - path, line := details.FileLine(frame.PC) - 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, - } - list = append(list, tb) - } - sort.Slice(list, func(i, j int) bool { - return j < i - }) - return list -} - -func buildString(level LogLevel, prefix string, newline, printTime, printTraceback bool, m ...any) string { - args := []string{ - fmt.Sprintf("[%s]", prefix), - fmt.Sprintf("[%s]", strings.ToUpper(level.t)), - } - - if printTraceback { - args = append(args, fmt.Sprintf("[%s]", formatTraceback(getTraceback()))) - } - - if printTime { - args = append(args, fmt.Sprintf("[%s]", formatTime(time.Now()))) - } - - msg := Map(m, func(el any) string { - return fmt.Sprintf("%v", el) - }) - s := fmt.Sprintf("%s %s", strings.Join(args, " "), strings.Join(msg, " ")) - if newline { - s += "\n" - } - return s -} - -func (l *Logger) print(level LogLevel, m ...any) { - if l.level.n < level.n { - return - } - - tb := getFullTraceback(0) - for _, writer := range l.writers { - err := writer.Print(level, l.prefix, tb, m...) - if err != nil { - l.Error(err) - } - } -} - -// Docker requires "\n" at end to write to log. -// print not work for docker, otherwise it will work and write into stdout -func (l *Logger) println(level LogLevel, m ...any) { - if l.level.n < level.n { - return - } - - tb := getFullTraceback(0) - for _, writer := range l.writers { - err := writer.Print(level, l.prefix, tb, m...) - if err != nil { - l.Error(err) - } - } -} diff --git a/methods.go b/methods.go index 19bdc9b..cc82b21 100644 --- a/methods.go +++ b/methods.go @@ -44,7 +44,7 @@ func (b *Bot) Updates() ([]*Update, error) { if err != nil { b.logger.Error(err) } - b.requestLogger.Debug(fmt.Sprintf("UPDATE %s", j)) + b.requestLogger.Debugln(fmt.Sprintf("UPDATE %s", j)) } } return res, err diff --git a/queue.go b/queue.go index e35028d..3789918 100644 --- a/queue.go +++ b/queue.go @@ -1,10 +1,12 @@ package laniakea import ( - "fmt" + "errors" "sync" ) +var QueueFullErr = errors.New("queue is full") + type Queue[T any] struct { size uint64 mu sync.RWMutex @@ -20,7 +22,7 @@ func CreateQueue[T any](size uint64) *Queue[T] { func (q *Queue[T]) Enqueue(el T) error { if q.IsFull() { - return fmt.Errorf("queue full") + return QueueFullErr } q.queue = append(q.queue, el) return nil diff --git a/version.go b/version.go index 1a090c8..cc24bed 100644 --- a/version.go +++ b/version.go @@ -1,12 +1,8 @@ package laniakea -import "os" - const ( - VersionString = "0.2.0" + VersionString = "0.2.5" VersionMajor = 0 VersionMinor = 2 - VersionPatch = 0 + VersionPatch = 5 ) - -var GoVersion = os.Getenv("GoV") From eaa5dbaf10b17dcee7a23ef474dd4a07c6337fff Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 21:46:56 +0300 Subject: [PATCH 20/22] runners and some fixes --- api.go | 68 +++++++++++ bot.go | 310 ++++++++----------------------------------------- go.mod | 2 +- methods.go | 28 ++--- msg_context.go | 164 ++++++++++++++++++++++++++ runners.go | 80 +++++++++++++ utils.go | 25 +++- 7 files changed, 396 insertions(+), 281 deletions(-) create mode 100644 api.go create mode 100644 msg_context.go create mode 100644 runners.go diff --git a/api.go b/api.go new file mode 100644 index 0000000..9a2babc --- /dev/null +++ b/api.go @@ -0,0 +1,68 @@ +package laniakea + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +type ApiResponse struct { + Ok bool `json:"ok"` + Description string `json:"description,omitempty"` + Result any `json:"result,omitempty"` + ErrorCode int `json:"error_code,omitempty"` +} + +// request is a low-level call to api. +func (b *Bot) request(methodName string, params any) (map[string]any, 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.Debugln(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 + } + defer r.Body.Close() + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + b.requestLogger.Debugln(fmt.Sprintf("RES %s %s", methodName, string(data))) + + response := new(ApiResponse) + err = json.Unmarshal(data, &response) + if err != nil { + return nil, err + } + + if !response.Ok { + return nil, fmt.Errorf("[%d] %s", response.ErrorCode, response.Description) + } + if res, ok := response.Result.(bool); ok { + return map[string]any{ + "data": res, + }, nil + } else if res, ok := response.Result.([]any); ok { + return map[string]any{ + "data": res, + }, nil + } else if res, ok := response.Result.(map[string]any); ok { + return res, nil + } + return map[string]any{}, errors.New("can't parse response") +} diff --git a/bot.go b/bot.go index 9d1cea9..91b92b9 100644 --- a/bot.go +++ b/bot.go @@ -1,11 +1,9 @@ package laniakea import ( - "bytes" "encoding/json" "fmt" - "io" - "net/http" + "log" "os" "sort" "strings" @@ -36,6 +34,7 @@ type Bot struct { plugins []*Plugin middlewares []*Middleware prefixes []string + runners []Runner dbContext *DatabaseContext @@ -67,29 +66,18 @@ func LoadSettingsFromEnv() *BotSettings { } } -type MsgContext struct { - Bot *Bot - Msg *Message - Update *Update - From *User - CallbackMsgId int - FromID int - Prefix string - Text string - Args []string +func LoadPrefixesFromEnv() []string { + prefixesS, exists := os.LookupEnv("PREFIXES") + if !exists { + return []string{"!"} + } + return strings.Split(prefixesS, ";") } - -type DatabaseContext struct { - PostgresSQL *sqlx.DB - MongoDB *mongo.Client - Redis *redis.Client -} - 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), + prefixes: settings.Prefixes, updateTypes: make([]string, 0), runners: make([]Runner, 0), updateQueue: updateQueue, token: settings.Token, } @@ -97,10 +85,10 @@ func NewBot(settings *BotSettings) *Bot { if len(settings.ErrorTemplate) > 0 { bot.errorTemplate = settings.ErrorTemplate } - if len(settings.LoggerBasePath) == 0 { settings.LoggerBasePath = "./" } + level := slog.FATAL if settings.Debug { level = slog.DEBUG @@ -134,14 +122,22 @@ func NewBot(settings *BotSettings) *Bot { } func (b *Bot) Close() { - b.logger.Close() - b.requestLogger.Close() + err := b.logger.Close() + if err != nil { + log.Println(err) + } + err = b.requestLogger.Close() + if err != nil { + log.Println(err) + } } -func (b *Bot) InitDatabaseContext(ctx *DatabaseContext) *Bot { - b.dbContext = ctx - return b +type DatabaseContext struct { + PostgresSQL *sqlx.DB + MongoDB *mongo.Client + Redis *redis.Client } + func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) slog.LoggerWriter) *Bot { w := writer(b.dbContext) b.logger.AddWriter(w) @@ -151,6 +147,10 @@ func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) slog.LoggerWrit return b } +func (b *Bot) DatabaseContext(ctx *DatabaseContext) *Bot { + b.dbContext = ctx + return b +} func (b *Bot) UpdateTypes(t ...string) *Bot { b.updateTypes = make([]string, 0) b.updateTypes = append(b.updateTypes, t...) @@ -164,18 +164,11 @@ 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) Debugln(debug bool) *Bot { +func (b *Bot) Debug(debug bool) *Bot { b.debug = debug return b } @@ -202,6 +195,17 @@ func (b *Bot) AddMiddleware(middleware ...*Middleware) *Bot { } return b } +func (b *Bot) AddRunner(runner Runner) *Bot { + b.runners = append(b.runners, runner) + b.logger.Debugln(fmt.Sprintf("runner with name \"%s\" registered", runner.Name)) + return b +} +func (b *Bot) Logger() *slog.Logger { + return b.logger +} +func (b *Bot) GetDBContext() *DatabaseContext { + return b.dbContext +} func (b *Bot) Run() { if len(b.prefixes) == 0 { @@ -214,15 +218,16 @@ func (b *Bot) Run() { return } - b.logger.Infoln("Bot running. Press CTRL+C to exit.") + b.logger.Infoln("Executing runners...") + b.ExecRunners() + b.logger.Infoln("Bot running. Press CTRL+C to exit.") go func() { for { _, err := b.Updates() if err != nil { b.logger.Errorln(err) } - time.Sleep(time.Millisecond * 10) } }() @@ -239,8 +244,7 @@ func (b *Bot) Run() { continue } ctx := &MsgContext{ - Bot: b, - Update: u, + Bot: b, Update: u, } for _, middleware := range b.middlewares { middleware.Execute(ctx, b.dbContext) @@ -263,25 +267,26 @@ func (b *Bot) Run() { // {"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) { - var text string if update.Message == nil { return } + + var text string if len(update.Message.Text) > 0 { text = update.Message.Text } else { text = update.Message.Caption } - ctx.FromID = update.Message.From.ID - ctx.From = update.Message.From - ctx.Msg = update.Message text = strings.TrimSpace(text) prefix, hasPrefix := b.checkPrefixes(text) if !hasPrefix { return } ctx.Prefix = prefix + ctx.FromID = update.Message.From.ID + ctx.From = update.Message.From + ctx.Msg = update.Message text = strings.TrimSpace(text[len(prefix):]) @@ -296,6 +301,7 @@ func (b *Bot) handleMessage(update *Update, ctx *MsgContext) { ctx.Args = strings.Split(ctx.Text, " ") go plugin.Execute(cmd, ctx, b.dbContext) + return } } } @@ -320,7 +326,7 @@ func (b *Bot) handleCallback(update *Update, ctx *MsgContext) { continue } go plugin.ExecutePayload(data.Command, ctx, b.dbContext) - break + return } } @@ -332,219 +338,3 @@ func (b *Bot) checkPrefixes(text string) (string, bool) { } return "", false } - -type AnswerMessage struct { - MessageID int - Text string - IsMedia bool - Keyboard *InlineKeyboard - ctx *MsgContext -} - -func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard) *AnswerMessage { - params := &EditMessageTextP{ - MessageID: messageId, - ChatID: ctx.Msg.Chat.ID, - Text: text, - ParseMode: ParseMD, - } - if keyboard != nil { - params.ReplyMarkup = keyboard.Get() - } - msg, err := ctx.Bot.EditMessageText(params) - if err != nil { - ctx.Bot.logger.Errorln(err) - return nil - } - return &AnswerMessage{ - MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: false, - } -} -func (m *AnswerMessage) Edit(text string) *AnswerMessage { - return m.ctx.edit(m.MessageID, text, nil) -} -func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage { - if ctx.CallbackMsgId == 0 { - ctx.Bot.logger.Errorln("Can't edit non-callback update message") - return nil - } - - return ctx.edit(ctx.CallbackMsgId, text, keyboard) -} -func (ctx *MsgContext) EditCallbackf(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage { - return ctx.EditCallback(fmt.Sprintf(format, args...), keyboard) -} - -func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeyboard) *AnswerMessage { - params := &EditMessageCaptionP{ - ChatID: ctx.Msg.Chat.ID, - MessageID: messageId, - Caption: text, - ParseMode: ParseMD, - } - if kb != nil { - params.ReplyMarkup = kb.Get() - } - msg, err := ctx.Bot.EditMessageCaption(params) - if err != nil { - ctx.Bot.logger.Errorln(err) - } - return &AnswerMessage{ - MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, - } -} -func (m *AnswerMessage) EditCaption(text string) *AnswerMessage { - return m.ctx.editPhotoText(m.MessageID, text, nil) -} -func (m *AnswerMessage) EditCaptionKeyboard(text string, kb *InlineKeyboard) *AnswerMessage { - return m.ctx.editPhotoText(m.MessageID, text, kb) -} - -func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard) *AnswerMessage { - params := &SendMessageP{ - ChatID: ctx.Msg.Chat.ID, - Text: text, - ParseMode: ParseMD, - } - if keyboard != nil { - params.ReplyMarkup = keyboard.Get() - } - - msg, err := ctx.Bot.SendMessage(params) - if err != nil { - ctx.Bot.logger.Errorln(err) - return nil - } - return &AnswerMessage{ - MessageID: msg.MessageID, ctx: ctx, IsMedia: false, Text: text, - } -} -func (ctx *MsgContext) Answer(text string) *AnswerMessage { - return ctx.answer(text, nil) -} -func (ctx *MsgContext) Answerf(template string, args ...any) *AnswerMessage { - return ctx.answer(fmt.Sprintf(template, args...), nil) -} -func (ctx *MsgContext) Keyboard(text string, kb *InlineKeyboard) *AnswerMessage { - return ctx.answer(text, kb) -} - -func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard) *AnswerMessage { - params := &SendPhotoP{ - ChatID: ctx.Msg.Chat.ID, - Caption: text, - Photo: photoId, - ParseMode: ParseMD, - } - if kb != nil { - params.ReplyMarkup = kb.Get() - } - msg, err := ctx.Bot.SendPhoto(params) - if err != nil { - ctx.Bot.logger.Errorln(err) - } - return &AnswerMessage{ - MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, - } -} -func (ctx *MsgContext) AnswerPhoto(photoId, text string) *AnswerMessage { - return ctx.answerPhoto(photoId, text, nil) -} -func (ctx *MsgContext) AnswerPhotoKeyboard(photoId, text string, kb *InlineKeyboard) *AnswerMessage { - return ctx.answerPhoto(photoId, text, kb) -} - -func (ctx *MsgContext) delete(messageId int) { - _, err := ctx.Bot.DeleteMessage(&DeleteMessageP{ - ChatID: ctx.Msg.Chat.ID, - MessageID: messageId, - }) - if err != nil { - ctx.Bot.logger.Errorln(err) - } -} -func (m *AnswerMessage) Delete() { - m.ctx.delete(m.MessageID) -} -func (ctx *MsgContext) CallbackDelete() { - ctx.delete(ctx.CallbackMsgId) -} - -func (ctx *MsgContext) Error(err error) { - _, sendErr := ctx.Bot.SendMessage(&SendMessageP{ - ChatID: ctx.Msg.Chat.ID, - Text: fmt.Sprintf(ctx.Bot.errorTemplate, EscapeMarkdown(err.Error())), - }) - ctx.Bot.logger.Errorln(err) - - if sendErr != nil { - ctx.Bot.logger.Errorln(sendErr) - } -} - -func (b *Bot) Logger() *slog.Logger { - return b.logger -} - -type ApiResponse struct { - Ok bool `json:"ok"` - Result map[string]any `json:"result,omitempty"` - Description string `json:"description,omitempty"` - ErrorCode int `json:"error_code,omitempty"` -} - -type ApiResponseA struct { - Ok bool `json:"ok"` - Result []any `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 any) (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.Debugln(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 - } - defer r.Body.Close() - data, err := io.ReadAll(r.Body) - if err != nil { - return nil, err - } - b.requestLogger.Debugln(fmt.Sprintf("RES %s %s", methodName, string(data))) - response := new(ApiResponse) - - var result map[string]any - - 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/go.mod b/go.mod index 519aafc..fc1aad0 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.33.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.22.0 // indirect diff --git a/methods.go b/methods.go index cc82b21..f80cfa7 100644 --- a/methods.go +++ b/methods.go @@ -18,29 +18,21 @@ func (b *Bot) Updates() ([]*Update, error) { return nil, err } res := make([]*Update, 0) - for _, u := range data["data"].([]any) { - updateObj := new(Update) - data, err := json.Marshal(u) + err = AnyToStruct(data["data"], &res) + if err != nil { + return res, err + } + + for _, u := range res { + b.updateOffset = u.UpdateID + 1 + err = b.updateQueue.Enqueue(u) if err != nil { return res, err } - err = json.Unmarshal(data, updateObj) - if err != nil { - return res, err - } - //err = MapToStruct(u.(map[string]any), 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) + res = append(res, u) if b.debug && b.requestLogger != nil { - j, err := MapToJson(u.(map[string]interface{})) + j, err := json.Marshal(u) if err != nil { b.logger.Error(err) } diff --git a/msg_context.go b/msg_context.go new file mode 100644 index 0000000..441ee6a --- /dev/null +++ b/msg_context.go @@ -0,0 +1,164 @@ +package laniakea + +import "fmt" + +type MsgContext struct { + Bot *Bot + Msg *Message + Update *Update + From *User + CallbackMsgId int + FromID int + Prefix string + Text string + Args []string +} + +type AnswerMessage struct { + MessageID int + Text string + IsMedia bool + Keyboard *InlineKeyboard + ctx *MsgContext +} + +func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard) *AnswerMessage { + params := &EditMessageTextP{ + MessageID: messageId, + ChatID: ctx.Msg.Chat.ID, + Text: text, + ParseMode: ParseMD, + } + if keyboard != nil { + params.ReplyMarkup = keyboard.Get() + } + msg, err := ctx.Bot.EditMessageText(params) + if err != nil { + ctx.Bot.logger.Errorln(err) + return nil + } + return &AnswerMessage{ + MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: false, + } +} +func (m *AnswerMessage) Edit(text string) *AnswerMessage { + return m.ctx.edit(m.MessageID, text, nil) +} +func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage { + if ctx.CallbackMsgId == 0 { + ctx.Bot.logger.Errorln("Can't edit non-callback update message") + return nil + } + + return ctx.edit(ctx.CallbackMsgId, text, keyboard) +} +func (ctx *MsgContext) EditCallbackf(format string, keyboard *InlineKeyboard, args ...any) *AnswerMessage { + return ctx.EditCallback(fmt.Sprintf(format, args...), keyboard) +} + +func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeyboard) *AnswerMessage { + params := &EditMessageCaptionP{ + ChatID: ctx.Msg.Chat.ID, + MessageID: messageId, + Caption: text, + ParseMode: ParseMD, + } + if kb != nil { + params.ReplyMarkup = kb.Get() + } + msg, err := ctx.Bot.EditMessageCaption(params) + if err != nil { + ctx.Bot.logger.Errorln(err) + } + return &AnswerMessage{ + MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, + } +} +func (m *AnswerMessage) EditCaption(text string) *AnswerMessage { + return m.ctx.editPhotoText(m.MessageID, text, nil) +} +func (m *AnswerMessage) EditCaptionKeyboard(text string, kb *InlineKeyboard) *AnswerMessage { + return m.ctx.editPhotoText(m.MessageID, text, kb) +} + +func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard) *AnswerMessage { + params := &SendMessageP{ + ChatID: ctx.Msg.Chat.ID, + Text: text, + ParseMode: ParseMD, + } + if keyboard != nil { + params.ReplyMarkup = keyboard.Get() + } + + msg, err := ctx.Bot.SendMessage(params) + if err != nil { + ctx.Bot.logger.Errorln(err) + return nil + } + return &AnswerMessage{ + MessageID: msg.MessageID, ctx: ctx, IsMedia: false, Text: text, + } +} +func (ctx *MsgContext) Answer(text string) *AnswerMessage { + return ctx.answer(text, nil) +} +func (ctx *MsgContext) Answerf(template string, args ...any) *AnswerMessage { + return ctx.answer(fmt.Sprintf(template, args...), nil) +} +func (ctx *MsgContext) Keyboard(text string, kb *InlineKeyboard) *AnswerMessage { + return ctx.answer(text, kb) +} + +func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard) *AnswerMessage { + params := &SendPhotoP{ + ChatID: ctx.Msg.Chat.ID, + Caption: text, + Photo: photoId, + ParseMode: ParseMD, + } + if kb != nil { + params.ReplyMarkup = kb.Get() + } + msg, err := ctx.Bot.SendPhoto(params) + if err != nil { + ctx.Bot.logger.Errorln(err) + } + return &AnswerMessage{ + MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, + } +} +func (ctx *MsgContext) AnswerPhoto(photoId, text string) *AnswerMessage { + return ctx.answerPhoto(photoId, text, nil) +} +func (ctx *MsgContext) AnswerPhotoKeyboard(photoId, text string, kb *InlineKeyboard) *AnswerMessage { + return ctx.answerPhoto(photoId, text, kb) +} + +func (ctx *MsgContext) delete(messageId int) { + _, err := ctx.Bot.DeleteMessage(&DeleteMessageP{ + ChatID: ctx.Msg.Chat.ID, + MessageID: messageId, + }) + if err != nil { + ctx.Bot.logger.Errorln(err) + } +} +func (m *AnswerMessage) Delete() { + m.ctx.delete(m.MessageID) +} +func (ctx *MsgContext) CallbackDelete() { + ctx.delete(ctx.CallbackMsgId) +} + +func (ctx *MsgContext) Error(err error) { + _, sendErr := ctx.Bot.SendMessage(&SendMessageP{ + ChatID: ctx.Msg.Chat.ID, + Text: fmt.Sprintf(ctx.Bot.errorTemplate, EscapeMarkdown(err.Error())), + }) + ctx.Bot.logger.Errorln(err) + + if sendErr != nil { + ctx.Bot.logger.Errorln(sendErr) + } +} diff --git a/runners.go b/runners.go new file mode 100644 index 0000000..75daaa2 --- /dev/null +++ b/runners.go @@ -0,0 +1,80 @@ +package laniakea + +import ( + "time" +) + +type RunnerFn func(*Bot) error +type RunnerBuilder struct { + name string + onetime bool + async bool + timeout time.Duration + fn RunnerFn +} +type Runner struct { + Name string + Onetime bool + Async bool + Timeout time.Duration + Fn RunnerFn +} + +func NewRunner(name string, fn RunnerFn) *RunnerBuilder { + return &RunnerBuilder{ + name: name, fn: fn, async: true, + } +} +func (b *RunnerBuilder) Onetime(onetime bool) *RunnerBuilder { + b.onetime = onetime + return b +} +func (b *RunnerBuilder) Async(async bool) *RunnerBuilder { + b.async = async + return b +} +func (b *RunnerBuilder) Timeout(timeout time.Duration) *RunnerBuilder { + b.timeout = timeout + return b +} +func (b *RunnerBuilder) Build() Runner { + return Runner{ + Name: b.name, Onetime: b.onetime, Async: b.async, Fn: b.fn, Timeout: b.timeout, + } +} + +func (b *Bot) ExecRunners() { + for _, runner := range b.runners { + if !runner.Onetime && !runner.Async { + b.logger.Warnf("Runner %s not onetime, but sync\n", runner.Name) + continue + } + if !runner.Onetime && runner.Async && runner.Timeout == (time.Second*0) { + b.logger.Warnf("Background runner \"%s\" should have timeout", runner.Name) + } + + if runner.Async && runner.Onetime { + go func() { + err := runner.Fn(b) + if err != nil { + b.logger.Warnf("Runner %s failed: %s\n", runner.Name, err) + } + }() + } else if !runner.Async && runner.Onetime { + err := runner.Fn(b) + if err != nil { + b.logger.Warnf("Runner %s failed: %s\n", runner.Name, err) + } + } else if !runner.Onetime { + go func() { + for { + err := runner.Fn(b) + if err != nil { + b.logger.Warnf("Runner %s failed: %s\n", runner.Name, err) + } + time.Sleep(runner.Timeout) + } + }() + } + } +} diff --git a/utils.go b/utils.go index 6725da0..b071e13 100644 --- a/utils.go +++ b/utils.go @@ -6,7 +6,8 @@ import ( "strings" ) -func MapToStruct(m map[string]interface{}, s interface{}) error { +// MapToStruct unsafe function +func MapToStruct(m map[string]any, s any) error { data, err := json.Marshal(m) if err != nil { return err @@ -15,7 +16,27 @@ func MapToStruct(m map[string]interface{}, s interface{}) error { return err } -func MapToJson(m map[string]interface{}) (string, error) { +// SliceToStruct unsafe function +func SliceToStruct(sl []any, s any) error { + data, err := json.Marshal(sl) + if err != nil { + return err + } + err = json.Unmarshal(data, s) + return err +} + +// AnyToStruct unsafe function +func AnyToStruct(src, dest any) error { + data, err := json.Marshal(src) + if err != nil { + return err + } + err = json.Unmarshal(data, dest) + return err +} + +func MapToJson(m map[string]any) (string, error) { data, err := json.Marshal(m) return string(data), err } From 012854be412fb4d2803d0114de0cccc1ac8d1731 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 21:47:22 +0300 Subject: [PATCH 21/22] runners and some fixes --- version.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.go b/version.go index cc24bed..aa6cbec 100644 --- a/version.go +++ b/version.go @@ -1,8 +1,8 @@ package laniakea const ( - VersionString = "0.2.5" + VersionString = "0.3.0" VersionMajor = 0 - VersionMinor = 2 - VersionPatch = 5 + VersionMinor = 3 + VersionPatch = 0 ) From ce9952340ff0114aabb0084024f65d10aaffaf2f Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Wed, 28 Jan 2026 21:57:21 +0300 Subject: [PATCH 22/22] deps version bump --- go.mod | 15 +++++++-------- go.sum | 34 ++++++++++++++++------------------ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index fc1aad0..79c4658 100644 --- a/go.mod +++ b/go.mod @@ -6,24 +6,23 @@ require ( git.nix13.pw/scuroneko/slog v1.0.0 github.com/redis/go-redis/v9 v9.17.3 github.com/vinovest/sqlx v1.7.1 - go.mongodb.org/mongo-driver/v2 v2.4.2 + go.mongodb.org/mongo-driver/v2 v2.5.0 ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/color v1.18.0 // indirect - github.com/golang/snappy v1.0.0 // indirect - github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/compress v1.18.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/muir/sqltoken v0.1.0 // indirect + github.com/muir/sqltoken v0.2.1 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sync v0.11.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/text v0.33.0 // indirect ) diff --git a/go.sum b/go.sum index 798ad5b..21af68d 100644 --- a/go.sum +++ b/go.sum @@ -16,12 +16,10 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= -github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= -github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -30,39 +28,39 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/muir/sqltoken v0.1.0 h1:edosEGsOClOZNfgGQNQSgxR9O6LiVefm2rDRqp2InuI= -github.com/muir/sqltoken v0.1.0/go.mod h1:lgOIORnKekMsuc/ZwdPOfwz/PtWLPCke43cEbT3uDuY= +github.com/muir/sqltoken v0.2.1 h1:19KvJrCj9aOMfU921hjnizWPlQmPTe+tb36zupOY2FA= +github.com/muir/sqltoken v0.2.1/go.mod h1:sSlj5M0VqQ4OuedmxwWs1TmzzRXaH3DLf5ukzg6meIo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4= github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vinovest/sqlx v1.7.1 h1:kdq4v0N9kRLpytWGSWOw4aulOGdQPmIoMR6Y+cTBxow= github.com/vinovest/sqlx v1.7.1/go.mod h1:3fAv74r4iDMv2PpFomADb+vex5ukzfYn4GseC9KngD8= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver/v2 v2.4.2 h1:HrJ+Auygxceby9MLp3YITobef5a8Bv4HcPFIkml1U7U= -go.mongodb.org/mongo-driver/v2 v2.4.2/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -77,8 +75,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=