2 Commits

Author SHA1 Message Date
9d18bef97e v0.4.1; bot api v9.4 2026-02-10 14:14:17 +03:00
60f09e940a v0.4.0 2026-02-05 12:16:25 +03:00
14 changed files with 399 additions and 152 deletions

49
api.go
View File

@@ -7,8 +7,24 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"git.nix13.pw/scuroneko/slog"
) )
type Api struct {
token string
logger *slog.Logger
}
func NewAPI(token string) *Api {
l := slog.CreateLogger().Level(GetLoggerLevel()).Prefix("API")
l.AddWriter(l.CreateJsonStdoutWriter())
return &Api{token, l}
}
func (api *Api) CloseApi() error {
return api.logger.Close()
}
type ApiResponse[R any] struct { type ApiResponse[R any] struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
@@ -24,32 +40,32 @@ type TelegramRequest[R, P any] struct {
func NewRequest[R, P any](method string, params P) TelegramRequest[R, P] { func NewRequest[R, P any](method string, params P) TelegramRequest[R, P] {
return TelegramRequest[R, P]{method: method, params: params} return TelegramRequest[R, P]{method: method, params: params}
} }
func (r TelegramRequest[R, P]) Do(bot *Bot) (*R, error) { func (r TelegramRequest[R, P]) Do(api *Api) (*R, error) {
var buf bytes.Buffer buf := bytes.NewBuffer(nil)
err := json.NewEncoder(&buf).Encode(r.params) err := json.NewEncoder(buf).Encode(r.params)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if bot.requestLogger != nil { u := fmt.Sprintf("https://api.telegram.org/bot%s/%s", api.token, r.method)
bot.requestLogger.Debugln(strings.ReplaceAll(fmt.Sprintf( if api.logger != nil {
"POST https://api.telegram.org/bot%s/%s %s", api.logger.Debugln(strings.ReplaceAll(fmt.Sprintf(
"<TOKEN>", r.method, buf.String(), "POST %s %s", u, buf.String(),
), "\n", "")) ), api.token, "<TOKEN>"))
} }
req, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/%s", bot.token, r.method), "application/json", &buf) res, err := http.Post(u, "application/json", buf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer req.Body.Close() defer res.Body.Close()
data, err := io.ReadAll(req.Body) data, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if bot.requestLogger != nil { if api.logger != nil {
bot.requestLogger.Debugln(fmt.Sprintf("RES %s %s", r.method, string(data))) api.logger.Debugln(fmt.Sprintf("RES %s %s", r.method, string(data)))
} }
response := new(ApiResponse[R]) response := new(ApiResponse[R])
@@ -65,13 +81,8 @@ func (r TelegramRequest[R, P]) Do(bot *Bot) (*R, error) {
} }
func (b *Bot) GetFileByLink(link string) ([]byte, error) { func (b *Bot) GetFileByLink(link string) ([]byte, error) {
c := http.DefaultClient
u := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", b.token, link) u := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", b.token, link)
req, err := http.NewRequest("GET", u, nil) res, err := http.Get(u)
if err != nil {
return nil, err
}
res, err := c.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

14
bot.go
View File

@@ -37,6 +37,9 @@ type Bot struct {
runners []Runner runners []Runner
dbContext *DatabaseContext dbContext *DatabaseContext
api *Api
dbWriterRequested extypes.Slice[*slog.Logger]
updateOffset int updateOffset int
updateTypes []string updateTypes []string
@@ -75,12 +78,14 @@ func LoadPrefixesFromEnv() []string {
} }
func NewBot(settings *BotSettings) *Bot { func NewBot(settings *BotSettings) *Bot {
updateQueue := extypes.CreateQueue[*Update](256) updateQueue := extypes.CreateQueue[*Update](256)
api := NewAPI(settings.Token)
bot := &Bot{ bot := &Bot{
updateOffset: 0, plugins: make([]Plugin, 0), debug: settings.Debug, errorTemplate: "%s", updateOffset: 0, plugins: make([]Plugin, 0), debug: settings.Debug, errorTemplate: "%s",
prefixes: settings.Prefixes, updateTypes: make([]string, 0), runners: make([]Runner, 0), prefixes: settings.Prefixes, updateTypes: make([]string, 0), runners: make([]Runner, 0),
updateQueue: updateQueue, updateQueue: updateQueue, api: api, dbWriterRequested: make([]*slog.Logger, 0),
token: settings.Token, token: settings.Token,
} }
bot.dbWriterRequested = bot.dbWriterRequested.Push(api.logger)
if len(settings.ErrorTemplate) > 0 { if len(settings.ErrorTemplate) > 0 {
bot.errorTemplate = settings.ErrorTemplate bot.errorTemplate = settings.ErrorTemplate
@@ -118,7 +123,7 @@ func NewBot(settings *BotSettings) *Bot {
} }
} }
u, err := bot.GetMe() u, err := api.GetMe()
if err != nil { if err != nil {
bot.logger.Fatal(err) bot.logger.Fatal(err)
} }
@@ -150,6 +155,9 @@ func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) slog.LoggerWrit
if b.requestLogger != nil { if b.requestLogger != nil {
b.requestLogger.AddWriter(w) b.requestLogger.AddWriter(w)
} }
for _, l := range b.dbWriterRequested {
l.AddWriter(w)
}
return b return b
} }
@@ -251,7 +259,7 @@ func (b *Bot) Run() {
continue continue
} }
ctx := &MsgContext{Bot: b, Update: u} ctx := &MsgContext{Bot: b, Update: u, Api: b.api}
for _, middleware := range b.middlewares { for _, middleware := range b.middlewares {
middleware.Execute(ctx, b.dbContext) middleware.Execute(ctx, b.dbContext)
} }

6
go.mod
View File

@@ -1,9 +1,9 @@
module git.nix13.pw/scuroneko/laniakea module git.nix13.pw/scuroneko/laniakea
go 1.25.6 go 1.25
require ( require (
git.nix13.pw/scuroneko/extypes v1.0.2 git.nix13.pw/scuroneko/extypes v1.1.0
git.nix13.pw/scuroneko/slog v1.0.2 git.nix13.pw/scuroneko/slog v1.0.2
github.com/redis/go-redis/v9 v9.17.3 github.com/redis/go-redis/v9 v9.17.3
github.com/vinovest/sqlx v1.7.1 github.com/vinovest/sqlx v1.7.1
@@ -14,7 +14,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muir/sqltoken v0.2.1 // indirect github.com/muir/sqltoken v0.2.1 // indirect

8
go.sum
View File

@@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.nix13.pw/scuroneko/extypes v1.0.2 h1:Qz1InLccaB9crXY33oGrSetPHePKfQAUqW/p/iYXmJk= git.nix13.pw/scuroneko/extypes v1.1.0 h1:kdAraybAqQgVhArVkVfrIi7KVEX8HgTr8mzbIZAAAqg=
git.nix13.pw/scuroneko/extypes v1.0.2/go.mod h1:uZVs8Yo3RrYAG9dMad6qR6lsYY67t+459D9c65QAYAw= git.nix13.pw/scuroneko/extypes v1.1.0/go.mod h1:uZVs8Yo3RrYAG9dMad6qR6lsYY67t+459D9c65QAYAw=
git.nix13.pw/scuroneko/slog v1.0.2 h1:vZyUROygxC2d5FJHUQM/30xFEHY1JT/aweDZXA4rm2g= git.nix13.pw/scuroneko/slog v1.0.2 h1:vZyUROygxC2d5FJHUQM/30xFEHY1JT/aweDZXA4rm2g=
git.nix13.pw/scuroneko/slog v1.0.2/go.mod h1:3Qm2wzkR5KjwOponMfG7TcGSDjmYaFqRAmLvSPTuWJI= git.nix13.pw/scuroneko/slog v1.0.2/go.mod h1:3Qm2wzkR5KjwOponMfG7TcGSDjmYaFqRAmLvSPTuWJI=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -20,8 +20,8 @@ github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=

View File

@@ -35,6 +35,18 @@ func (b *Bot) handleMessage(update *Update, ctx *MsgContext) {
if !strings.HasPrefix(text, cmd) { if !strings.HasPrefix(text, cmd) {
continue continue
} }
requestParts := strings.Split(text, " ")
cmdParts := strings.Split(cmd, " ")
isValid := true
for i, part := range cmdParts {
if part != requestParts[i] {
isValid = false
break
}
}
if !isValid {
continue
}
ctx.Text = strings.TrimSpace(text[len(cmd):]) ctx.Text = strings.TrimSpace(text[len(cmd):])
ctx.Args = strings.Split(ctx.Text, " ") ctx.Args = strings.Split(ctx.Text, " ")

View File

@@ -3,51 +3,97 @@ package laniakea
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.nix13.pw/scuroneko/extypes"
) )
type InlineKbButtonBuilder struct {
text string
iconCustomEmojiID string
style InlineKeyboardButtonStyle
url string
callbackData string
}
func NewInlineKbButton(text string) InlineKbButtonBuilder {
return InlineKbButtonBuilder{text: text}
}
func (b InlineKbButtonBuilder) SetIconCustomEmojiId(id string) InlineKbButtonBuilder {
b.iconCustomEmojiID = id
return b
}
func (b InlineKbButtonBuilder) SetStyle(style InlineKeyboardButtonStyle) InlineKbButtonBuilder {
b.style = style
return b
}
func (b InlineKbButtonBuilder) SetUrl(url string) InlineKbButtonBuilder {
b.url = url
return b
}
func (b InlineKbButtonBuilder) SetCallbackData(data *CallbackData) InlineKbButtonBuilder {
b.callbackData = data.ToJson()
return b
}
func (b InlineKbButtonBuilder) build() InlineKeyboardButton {
return InlineKeyboardButton{
Text: b.text,
URL: b.url,
Style: b.style,
IconCustomEmojiID: b.iconCustomEmojiID,
CallbackData: b.callbackData,
}
}
type InlineKeyboard struct { type InlineKeyboard struct {
CurrentLine []InlineKeyboardButton CurrentLine extypes.Slice[InlineKeyboardButton]
Lines [][]InlineKeyboardButton Lines [][]InlineKeyboardButton
maxRow int maxRow int
} }
func NewInlineKeyboard(maxRow int) *InlineKeyboard { func NewInlineKeyboard(maxRow int) *InlineKeyboard {
return &InlineKeyboard{ return &InlineKeyboard{
CurrentLine: make([]InlineKeyboardButton, 0), CurrentLine: make(extypes.Slice[InlineKeyboardButton], 0),
Lines: make([][]InlineKeyboardButton, 0), Lines: make([][]InlineKeyboardButton, 0),
maxRow: maxRow, maxRow: maxRow,
} }
} }
func (in *InlineKeyboard) append(button InlineKeyboardButton) *InlineKeyboard { func (in *InlineKeyboard) append(button InlineKeyboardButton) *InlineKeyboard {
if len(in.CurrentLine) == in.maxRow { if in.CurrentLine.Len() == in.maxRow {
in.AddLine() in.AddLine()
} }
in.CurrentLine = append(in.CurrentLine, button) in.CurrentLine = in.CurrentLine.Push(button)
return in return in
} }
func (in *InlineKeyboard) AddUrlButton(text, url string) *InlineKeyboard { func (in *InlineKeyboard) AddUrlButton(text, url string) *InlineKeyboard {
return in.append(InlineKeyboardButton{Text: text, URL: url}) return in.append(InlineKeyboardButton{Text: text, URL: url})
} }
func (in *InlineKeyboard) AddCallbackButton(text string, cmd string, args ...any) *InlineKeyboard { func (in *InlineKeyboard) AddCallbackButton(text string, cmd string, args ...any) *InlineKeyboard {
return in.append(InlineKeyboardButton{Text: text, CallbackData: NewCallbackData(cmd, args...).ToJson()}) return in.append(InlineKeyboardButton{
Text: text, CallbackData: NewCallbackData(cmd, args...).ToJson(),
})
}
func (in *InlineKeyboard) AddCustomButton(button InlineKeyboardButton) *InlineKeyboard {
return in.append(button)
}
func (in *InlineKeyboard) AddButton(b InlineKbButtonBuilder) *InlineKeyboard {
return in.append(b.build())
} }
func (in *InlineKeyboard) AddLine() *InlineKeyboard { func (in *InlineKeyboard) AddLine() *InlineKeyboard {
if len(in.CurrentLine) == 0 { if in.CurrentLine.Len() == 0 {
return in return in
} }
in.Lines = append(in.Lines, in.CurrentLine) in.Lines = append(in.Lines, in.CurrentLine)
in.CurrentLine = make([]InlineKeyboardButton, 0) in.CurrentLine = make(extypes.Slice[InlineKeyboardButton], 0)
return in return in
} }
func (in *InlineKeyboard) Get() InlineKeyboardMarkup { func (in *InlineKeyboard) Get() *InlineKeyboardMarkup {
if len(in.CurrentLine) > 0 { if in.CurrentLine.Len() > 0 {
in.Lines = append(in.Lines, in.CurrentLine) in.Lines = append(in.Lines, in.CurrentLine)
} }
return InlineKeyboardMarkup{ return &InlineKeyboardMarkup{InlineKeyboard: in.Lines}
InlineKeyboard: in.Lines,
}
} }
type CallbackData struct { type CallbackData struct {

View File

@@ -2,7 +2,6 @@ package laniakea
import ( import (
"encoding/json" "encoding/json"
"fmt"
) )
type EmptyParams struct{} type EmptyParams struct{}
@@ -23,54 +22,166 @@ func (b *Bot) Updates() ([]*Update, error) {
} }
req := NewRequest[[]*Update]("getUpdates", params) req := NewRequest[[]*Update]("getUpdates", params)
res, err := req.Do(b) res, err := req.Do(b.api)
if err != nil { if err != nil {
return []*Update{}, err return nil, err
} }
updates := *res
for _, u := range updates { for _, u := range *res {
b.updateOffset = u.UpdateID + 1 b.updateOffset = u.UpdateID + 1
err = b.updateQueue.Enqueue(u) err = b.updateQueue.Enqueue(u)
if err != nil { if err != nil {
return updates, err return nil, err
} }
if b.debug && b.requestLogger != nil { if b.requestLogger != nil {
j, err := json.Marshal(u) j, err := json.Marshal(u)
if err != nil { if err != nil {
b.logger.Error(err) b.logger.Error(err)
} }
b.requestLogger.Debugln(fmt.Sprintf("UPDATE %s", j)) b.requestLogger.Debugf("UPDATE %s\n", j)
} }
} }
return updates, err return *res, err
} }
func (b *Bot) GetMe() (*User, error) { func (api *Api) GetMe() (*User, error) {
req := NewRequest[User, EmptyParams]("getMe", NoParams) req := NewRequest[User, EmptyParams]("getMe", NoParams)
return req.Do(b) return req.Do(api)
}
func (api *Api) LogOut() (bool, error) {
req := NewRequest[bool, EmptyParams]("logOut", NoParams)
res, err := req.Do(api)
if err != nil {
return false, err
}
return *res, nil
}
func (api *Api) Close() (bool, error) {
req := NewRequest[bool, EmptyParams]("close", NoParams)
res, err := req.Do(api)
if err != nil {
return false, err
}
return *res, nil
} }
type SendMessageP struct { type SendMessageP struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int `json:"chat_id"` ChatID int `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"` DirectMessageTopicID int `json:"direct_message_topic_id,omitempty"`
Text string `json:"text"` Text string `json:"text"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
Entities []*MessageEntity `json:"entities,omitempty"` Entities []*MessageEntity `json:"entities,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty"` DisableNotifications bool `json:"disable_notifications,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"` ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"` AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"` MessageEffectID string `json:"message_effect_id,omitempty"`
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"` ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
} }
func (b *Bot) SendMessage(params *SendMessageP) (*Message, error) { func (api *Api) SendMessage(params *SendMessageP) (*Message, error) {
req := NewRequest[Message, SendMessageP]("sendMessage", *params) req := NewRequest[Message, SendMessageP]("sendMessage", *params)
return req.Do(b) return req.Do(api)
}
type ForwardMessageP struct {
ChatID int `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessageTopicID int `json:"direct_message_topic_id,omitempty"`
MessageID int `json:"message_id,omitempty"`
FromChatID int `json:"from_chat_id,omitempty"`
VideoStartTimestamp int `json:"video_start_timestamp,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"`
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
}
func (api *Api) ForwardMessage(params ForwardMessageP) (*Message, error) {
req := NewRequest[Message]("forwardMessage", params)
return req.Do(api)
}
type ForwardMessagesP struct {
ChatID int `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessageTopicID int `json:"direct_message_topic_id,omitempty"`
FromChatID int `json:"from_chat_id,omitempty"`
MessageIDs []int `json:"message_ids,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
}
func (api *Api) ForwardMessages(params ForwardMessagesP) ([]int, error) {
req := NewRequest[[]int]("forwardMessages", params)
res, err := req.Do(api)
if err != nil {
return []int{}, err
}
return *res, nil
}
type CopyMessageP struct {
ChatID int `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessageTopicID int `json:"direct_message_topic_id,omitempty"`
FromChatID int `json:"from_chat_id"`
MessageID int `json:"message_id"`
VideoStartTimestamp int `json:"video_start_timestamp,omitempty"`
Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"`
SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
func (api *Api) CopyMessage(params CopyMessageP) (int, error) {
req := NewRequest[int]("copyMessage", params)
res, err := req.Do(api)
if err != nil {
return 0, err
}
return *res, nil
}
type CopyMessagesP struct {
ChatID int `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"`
DirectMessageTopicID int `json:"direct_message_topic_id,omitempty"`
FromChatID int `json:"from_chat_id,omitempty"`
MessageIDs []int `json:"message_ids,omitempty"`
DisableNotification bool `json:"disable_notification,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"`
RemoveCaption bool `json:"remove_caption,omitempty"`
}
func (api *Api) CopyMessages(params CopyMessagesP) ([]int, error) {
req := NewRequest[[]int]("copyMessages", params)
res, err := req.Do(api)
if err != nil {
return []int{}, err
}
return *res, nil
} }
type SendPhotoBaseP struct { type SendPhotoBaseP struct {
@@ -92,22 +203,28 @@ type SendPhotoP struct {
BusinessConnectionID string `json:"business_connection_id,omitempty"` BusinessConnectionID string `json:"business_connection_id,omitempty"`
ChatID int `json:"chat_id"` ChatID int `json:"chat_id"`
MessageThreadID int `json:"message_thread_id,omitempty"` MessageThreadID int `json:"message_thread_id,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"` DirectMessagesTopicID int `json:"direct_messages_topic_id,omitempty"`
Photo string `json:"photo"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"` ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
HasSpoiler bool `json:"has_spoiler,omitempty"` HasSpoiler bool `json:"has_spoiler,omitempty"`
DisableNotifications bool `json:"disable_notifications,omitempty"` DisableNotifications bool `json:"disable_notifications,omitempty"`
ProtectContent bool `json:"protect_content,omitempty"` ProtectContent bool `json:"protect_content,omitempty"`
AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"` AllowPaidBroadcast bool `json:"allow_paid_broadcast,omitempty"`
MessageEffectID string `json:"message_effect_id,omitempty"` MessageEffectID string `json:"message_effect_id,omitempty"`
ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"`
Photo string `json:"photo"` SuggestedPostParameters *SuggestedPostParameters `json:"suggested_post_parameters,omitempty"`
ReplyParameters *ReplyParameters `json:"reply_parameters,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
} }
func (b *Bot) SendPhoto(params *SendPhotoP) (*Message, error) { func (api *Api) SendPhoto(params *SendPhotoP) (*Message, error) {
req := NewRequest[Message]("sendPhoto", params) req := NewRequest[Message]("sendPhoto", params)
return req.Do(b) return req.Do(api)
} }
type EditMessageTextP struct { type EditMessageTextP struct {
@@ -117,12 +234,12 @@ type EditMessageTextP struct {
InlineMessageID string `json:"inline_message_id,omitempty"` InlineMessageID string `json:"inline_message_id,omitempty"`
Text string `json:"text"` Text string `json:"text"`
ParseMode ParseMode `json:"parse_mode,omitempty"` ParseMode ParseMode `json:"parse_mode,omitempty"`
ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
} }
func (b *Bot) EditMessageText(params *EditMessageTextP) (*Message, error) { func (api *Api) EditMessageText(params *EditMessageTextP) (*Message, error) {
req := NewRequest[Message]("editMessageText", params) req := NewRequest[Message]("editMessageText", params)
return req.Do(b) return req.Do(api)
} }
type EditMessageCaptionP struct { type EditMessageCaptionP struct {
@@ -132,12 +249,12 @@ type EditMessageCaptionP struct {
InlineMessageID string `json:"inline_message_id,omitempty"` InlineMessageID string `json:"inline_message_id,omitempty"`
Caption string `json:"caption"` Caption string `json:"caption"`
ParseMode ParseMode `json:"parse_mode,omitempty"` ParseMode ParseMode `json:"parse_mode,omitempty"`
ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
} }
func (b *Bot) EditMessageCaption(params *EditMessageCaptionP) (*Message, error) { func (api *Api) EditMessageCaption(params *EditMessageCaptionP) (*Message, error) {
req := NewRequest[Message]("editMessageCaption", params) req := NewRequest[Message]("editMessageCaption", params)
return req.Do(b) return req.Do(api)
} }
type DeleteMessageP struct { type DeleteMessageP struct {
@@ -145,9 +262,9 @@ type DeleteMessageP struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
} }
func (b *Bot) DeleteMessage(params *DeleteMessageP) (bool, error) { func (api *Api) DeleteMessage(params *DeleteMessageP) (bool, error) {
req := NewRequest[bool]("deleteMessage", params) req := NewRequest[bool]("deleteMessage", params)
ok, err := req.Do(b) ok, err := req.Do(api)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -162,9 +279,9 @@ type AnswerCallbackQueryP struct {
CacheTime int `json:"cache_time,omitempty"` CacheTime int `json:"cache_time,omitempty"`
} }
func (b *Bot) AnswerCallbackQuery(params *AnswerCallbackQueryP) (bool, error) { func (api *Api) AnswerCallbackQuery(params *AnswerCallbackQueryP) (bool, error) {
req := NewRequest[bool]("answerCallbackQuery", params) req := NewRequest[bool]("answerCallbackQuery", params)
ok, err := req.Do(b) ok, err := req.Do(api)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -175,9 +292,9 @@ type GetFileP struct {
FileId string `json:"file_id"` FileId string `json:"file_id"`
} }
func (b *Bot) GetFile(params *GetFileP) (*File, error) { func (api *Api) GetFile(params *GetFileP) (*File, error) {
req := NewRequest[File]("getFile", params) req := NewRequest[File]("getFile", params)
return req.Do(b) return req.Do(api)
} }
type SendChatActionP struct { type SendChatActionP struct {
@@ -187,9 +304,9 @@ type SendChatActionP struct {
Action ChatActions `json:"action"` Action ChatActions `json:"action"`
} }
func (b *Bot) SendChatAction(params SendChatActionP) (bool, error) { func (api *Api) SendChatAction(params SendChatActionP) (bool, error) {
req := NewRequest[bool]("sendChatAction", params) req := NewRequest[bool]("sendChatAction", params)
res, err := req.Do(b) res, err := req.Do(api)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -206,9 +323,9 @@ type SetMessageReactionEmojiP struct {
Reaction []ReactionTypeEmoji `json:"reaction"` Reaction []ReactionTypeEmoji `json:"reaction"`
} }
func (b *Bot) SetMessageReactionEmoji(params SetMessageReactionEmojiP) (bool, error) { func (api *Api) SetMessageReactionEmoji(params SetMessageReactionEmojiP) (bool, error) {
req := NewRequest[bool]("setMessageReaction", params) req := NewRequest[bool]("setMessageReaction", params)
res, err := req.Do(b) res, err := req.Do(api)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -220,9 +337,9 @@ type SetMessageReactionCustomEmojiP struct {
Reaction []ReactionTypeCustomEmoji `json:"reaction"` Reaction []ReactionTypeCustomEmoji `json:"reaction"`
} }
func (b *Bot) SetMessageReactionCustom(params SetMessageReactionCustomEmojiP) (bool, error) { func (api *Api) SetMessageReactionCustom(params SetMessageReactionCustomEmojiP) (bool, error) {
req := NewRequest[bool]("setMessageReaction", params) req := NewRequest[bool]("setMessageReaction", params)
res, err := req.Do(b) res, err := req.Do(api)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -233,9 +350,9 @@ type SetMessageReactionPaidP struct {
SetMessageReactionP SetMessageReactionP
} }
func (b *Bot) SetMessageReactionPaid(params SetMessageReactionPaidP) (bool, error) { func (api *Api) SetMessageReactionPaid(params SetMessageReactionPaidP) (bool, error) {
req := NewRequest[bool]("setMessageReaction", params) req := NewRequest[bool]("setMessageReaction", params)
res, err := req.Do(b) res, err := req.Do(api)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@@ -4,6 +4,8 @@ import "fmt"
type MsgContext struct { type MsgContext struct {
Bot *Bot Bot *Bot
Api *Api
Msg *Message Msg *Message
Update *Update Update *Update
From *User From *User
@@ -33,9 +35,9 @@ func (ctx *MsgContext) edit(messageId int, text string, keyboard *InlineKeyboard
if keyboard != nil { if keyboard != nil {
params.ReplyMarkup = keyboard.Get() params.ReplyMarkup = keyboard.Get()
} }
msg, err := ctx.Bot.EditMessageText(params) msg, err := ctx.Api.EditMessageText(params)
if err != nil { if err != nil {
ctx.Bot.logger.Errorln(err) ctx.Api.logger.Errorln(err)
return nil return nil
} }
return &AnswerMessage{ return &AnswerMessage{
@@ -47,7 +49,7 @@ func (m *AnswerMessage) Edit(text string) *AnswerMessage {
} }
func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage { func (ctx *MsgContext) EditCallback(text string, keyboard *InlineKeyboard) *AnswerMessage {
if ctx.CallbackMsgId == 0 { if ctx.CallbackMsgId == 0 {
ctx.Bot.logger.Errorln("Can't edit non-callback update message") ctx.Api.logger.Errorln("Can't edit non-callback update message")
return nil return nil
} }
@@ -67,9 +69,9 @@ func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeybo
if kb != nil { if kb != nil {
params.ReplyMarkup = kb.Get() params.ReplyMarkup = kb.Get()
} }
msg, err := ctx.Bot.EditMessageCaption(params) msg, err := ctx.Api.EditMessageCaption(params)
if err != nil { if err != nil {
ctx.Bot.logger.Errorln(err) ctx.Api.logger.Errorln(err)
} }
return &AnswerMessage{ return &AnswerMessage{
MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true, MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true,
@@ -77,7 +79,7 @@ func (ctx *MsgContext) editPhotoText(messageId int, text string, kb *InlineKeybo
} }
func (m *AnswerMessage) EditCaption(text string) *AnswerMessage { func (m *AnswerMessage) EditCaption(text string) *AnswerMessage {
if m.MessageID == 0 { if m.MessageID == 0 {
m.ctx.Bot.logger.Errorln("Can't edit caption message, message id is zero") m.ctx.Api.logger.Errorln("Can't edit caption message, message id is zero")
return m return m
} }
return m.ctx.editPhotoText(m.MessageID, text, nil) return m.ctx.editPhotoText(m.MessageID, text, nil)
@@ -96,9 +98,9 @@ func (ctx *MsgContext) answer(text string, keyboard *InlineKeyboard) *AnswerMess
params.ReplyMarkup = keyboard.Get() params.ReplyMarkup = keyboard.Get()
} }
msg, err := ctx.Bot.SendMessage(params) msg, err := ctx.Api.SendMessage(params)
if err != nil { if err != nil {
ctx.Bot.logger.Errorln(err) ctx.Api.logger.Errorln(err)
return nil return nil
} }
return &AnswerMessage{ return &AnswerMessage{
@@ -125,9 +127,9 @@ func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard) *An
if kb != nil { if kb != nil {
params.ReplyMarkup = kb.Get() params.ReplyMarkup = kb.Get()
} }
msg, err := ctx.Bot.SendPhoto(params) msg, err := ctx.Api.SendPhoto(params)
if err != nil { if err != nil {
ctx.Bot.logger.Errorln(err) ctx.Api.logger.Errorln(err)
return &AnswerMessage{ return &AnswerMessage{
ctx: ctx, Text: text, IsMedia: true, ctx: ctx, Text: text, IsMedia: true,
} }
@@ -144,12 +146,12 @@ func (ctx *MsgContext) AnswerPhotoKeyboard(photoId, text string, kb *InlineKeybo
} }
func (ctx *MsgContext) delete(messageId int) { func (ctx *MsgContext) delete(messageId int) {
_, err := ctx.Bot.DeleteMessage(&DeleteMessageP{ _, err := ctx.Api.DeleteMessage(&DeleteMessageP{
ChatID: ctx.Msg.Chat.ID, ChatID: ctx.Msg.Chat.ID,
MessageID: messageId, MessageID: messageId,
}) })
if err != nil { if err != nil {
ctx.Bot.logger.Errorln(err) ctx.Api.logger.Errorln(err)
} }
} }
func (m *AnswerMessage) Delete() { func (m *AnswerMessage) Delete() {
@@ -163,12 +165,12 @@ func (ctx *MsgContext) answerCallbackQuery(url, text string, showAlert bool) {
if len(ctx.CallbackQueryId) == 0 { if len(ctx.CallbackQueryId) == 0 {
return return
} }
_, err := ctx.Bot.AnswerCallbackQuery(&AnswerCallbackQueryP{ _, err := ctx.Api.AnswerCallbackQuery(&AnswerCallbackQueryP{
CallbackQueryID: ctx.CallbackQueryId, CallbackQueryID: ctx.CallbackQueryId,
Text: text, ShowAlert: showAlert, URL: url, Text: text, ShowAlert: showAlert, URL: url,
}) })
if err != nil { if err != nil {
ctx.Bot.logger.Errorln(err) ctx.Api.logger.Errorln(err)
} }
} }
func (ctx *MsgContext) AnswerCbQuery() { func (ctx *MsgContext) AnswerCbQuery() {
@@ -185,11 +187,11 @@ func (ctx *MsgContext) AnswerCbQueryUrl(u string) {
} }
func (ctx *MsgContext) SendAction(action ChatActions) { func (ctx *MsgContext) SendAction(action ChatActions) {
_, err := ctx.Bot.SendChatAction(SendChatActionP{ _, err := ctx.Api.SendChatAction(SendChatActionP{
ChatID: ctx.Msg.Chat.ID, Action: action, ChatID: ctx.Msg.Chat.ID, Action: action,
}) })
if err != nil { if err != nil {
ctx.Bot.logger.Errorln(err) ctx.Api.logger.Errorln(err)
} }
} }

View File

@@ -78,7 +78,6 @@ func Encode[T any](w *multipart.Writer, req T) error {
if err == nil { if err == nil {
_, err = fw.Write([]byte(strconv.FormatBool(field.Bool()))) _, err = fw.Write([]byte(strconv.FormatBool(field.Bool())))
} }
case reflect.Slice: case reflect.Slice:
if field.Type().Elem().Kind() == reflect.Uint8 && !field.IsNil() { if field.Type().Elem().Kind() == reflect.Uint8 && !field.IsNil() {
filename := fieldType.Tag.Get("filename") filename := fieldType.Tag.Get("filename")

View File

@@ -58,7 +58,7 @@ func (p *PluginBuilder) Middleware(middleware *PluginMiddleware) *PluginBuilder
func (p *PluginBuilder) Build() Plugin { func (p *PluginBuilder) Build() Plugin {
if len(p.commands) == 0 && len(p.payloads) == 0 { if len(p.commands) == 0 && len(p.payloads) == 0 {
log.Println("no command or payloads") log.Printf("no command or payloads for %s", p.name)
} }
return Plugin{ return Plugin{
Name: p.name, Name: p.name,

View File

@@ -85,11 +85,13 @@ type MessageEntity struct {
type ReplyParameters struct { type ReplyParameters struct {
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
ChatID int `json:"chat_id,omitempty"` ChatID int `json:"chat_id,omitempty"`
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"` AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
Quote string `json:"quote,omitempty"` Quote string `json:"quote,omitempty"`
QuoteParsingMode string `json:"quote_parsing_mode,omitempty"` QuoteParsingMode string `json:"quote_parsing_mode,omitempty"`
QuoteEntities []*MessageEntity `json:"quote_entities,omitempty"` QuoteEntities []*MessageEntity `json:"quote_entities,omitempty"`
QuotePosition int `json:"quote_postigin,omitempty"` QuotePosition int `json:"quote_position,omitempty"`
ChecklistTaskID int `json:"checklist_task_id,omitempty"`
} }
type PhotoSize struct { type PhotoSize struct {
@@ -108,14 +110,31 @@ type LinkPreviewOptions struct {
ShowAboveText bool `json:"show_above_text,omitempty"` ShowAboveText bool `json:"show_above_text,omitempty"`
} }
type ReplyMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard,omitempty"`
Keyboard [][]int `json:"keyboard,omitempty"`
IsPersistent bool `json:"is_persistent,omitempty"`
ResizeKeyboard bool `json:"resize_keyboard,omitempty"`
OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"`
InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"`
Selective bool `json:"selective,omitempty"`
RemoveKeyboard bool `json:"remove_keyboard,omitempty"`
ForceReply bool `json:"force_reply,omitempty"`
}
type InlineKeyboardMarkup struct { type InlineKeyboardMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard,omitempty"` InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard,omitempty"`
} }
type InlineKeyboardButtonStyle string
type InlineKeyboardButton struct { type InlineKeyboardButton struct {
Text string `json:"text"` Text string `json:"text"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
CallbackData string `json:"callback_data,omitempty"` CallbackData string `json:"callback_data,omitempty"`
Style InlineKeyboardButtonStyle `json:"style,omitempty"`
IconCustomEmojiID string `json:"icon_custom_emoji_id,omitempty"`
} }
type ReplyKeyboardMarkup struct { type ReplyKeyboardMarkup struct {
@@ -194,3 +213,17 @@ const (
ChatActionFindLocation ChatActions = "find_location" ChatActionFindLocation ChatActions = "find_location"
ChatActionUploadVideoNone ChatActions = "upload_video_none" ChatActionUploadVideoNone ChatActions = "upload_video_none"
) )
type SuggestedPostPrice struct {
Currency string `json:"currency"`
Amount int `json:"amount"`
}
type SuggestedPostInfo struct {
State string `json:"state"` //State of the suggested post. Currently, it can be one of “pending”, “approved”, “declined”.
Price SuggestedPostPrice `json:"price"`
SendDate int `json:"send_date"`
}
type SuggestedPostParameters struct {
Price SuggestedPostPrice `json:"price"`
SendDate int `json:"send_date"`
}

View File

@@ -8,14 +8,22 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"path/filepath" "path/filepath"
"git.nix13.pw/scuroneko/slog"
) )
type Uploader struct { type Uploader struct {
bot *Bot api *Api
logger *slog.Logger
} }
func NewUploader(bot *Bot) *Uploader { func NewUploader(api *Api) *Uploader {
return &Uploader{bot: bot} logger := slog.CreateLogger().Level(GetLoggerLevel()).Prefix("UPLOADER")
logger.AddWriter(logger.CreateJsonStdoutWriter())
return &Uploader{api, logger}
}
func (u *Uploader) Close() error {
return u.logger.Close()
} }
type UploaderFileType string type UploaderFileType string
@@ -56,8 +64,8 @@ func NewUploaderRequest[R, P any](method string, file UploaderFile, params P) Up
return UploaderRequest[R, P]{method, file, params} return UploaderRequest[R, P]{method, file, params}
} }
func (u UploaderRequest[R, P]) Do(bot *Bot) (*R, error) { func (u UploaderRequest[R, P]) Do(up *Uploader) (*R, error) {
url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", bot.token, u.method) url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", up.api.token, u.method)
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf) w := multipart.NewWriter(buf)
@@ -87,7 +95,7 @@ func (u UploaderRequest[R, P]) Do(bot *Bot) (*R, error) {
return nil, err return nil, err
} }
req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Content-Type", w.FormDataContentType())
bot.logger.Debugln("UPLOADER REQ", u.method) up.logger.Debugln("UPLOADER REQ", u.method)
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -98,7 +106,7 @@ func (u UploaderRequest[R, P]) Do(bot *Bot) (*R, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
bot.logger.Debugln("UPLOADER RES", u.method, string(body)) up.logger.Debugln("UPLOADER RES", u.method, string(body))
if res.StatusCode != http.StatusOK { if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("[%d] %s", res.StatusCode, string(body)) return nil, fmt.Errorf("[%d] %s", res.StatusCode, string(body))
} }
@@ -116,7 +124,7 @@ func (u UploaderRequest[R, P]) Do(bot *Bot) (*R, error) {
func (u *Uploader) UploadPhoto(file UploaderFile, params SendPhotoBaseP) (*Message, error) { func (u *Uploader) UploadPhoto(file UploaderFile, params SendPhotoBaseP) (*Message, error) {
req := NewUploaderRequest[Message]("sendPhoto", file, params) req := NewUploaderRequest[Message]("sendPhoto", file, params)
return req.Do(u.bot) return req.Do(u)
} }
func uploaderTypeByExt(filename string) UploaderFileType { func uploaderTypeByExt(filename string) UploaderFileType {

View File

@@ -3,9 +3,20 @@ package laniakea
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strings" "strings"
"git.nix13.pw/scuroneko/slog"
) )
func GetLoggerLevel() slog.LogLevel {
level := slog.FATAL
if os.Getenv("DEBUG") == "true" {
level = slog.DEBUG
}
return level
}
// MapToStruct unsafe function // MapToStruct unsafe function
func MapToStruct(m map[string]any, s any) error { func MapToStruct(m map[string]any, s any) error {
data, err := json.Marshal(m) data, err := json.Marshal(m)

View File

@@ -1,8 +1,8 @@
package laniakea package laniakea
const ( const (
VersionString = "0.3.9" VersionString = "0.4.1"
VersionMajor = 0 VersionMajor = 0
VersionMinor = 3 VersionMinor = 4
VersionPatch = 9 VersionPatch = 1
) )