Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f17b88787 | |||
|
7a6f135487
|
|||
|
2e9e82d43b
|
|||
|
55d4065259
|
|||
| b89f27574f | |||
| 689eb8a5e2 | |||
| 6fd482b58f | |||
| 913fa20e19 | |||
| c71aad0c79 | |||
| 90e2f38c18 | |||
| 0921d306fd | |||
| 6970c37c6b | |||
| 6d6f5738cd | |||
| fef718438a | |||
| 7f248fff62 |
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Laniakea
|
||||||
|
|
||||||
|
A lightweight, easy to use and performance Telegram API wrapper for bot development.
|
||||||
15
api.go
15
api.go
@@ -63,3 +63,18 @@ func (r TelegramRequest[R, P]) Do(bot *Bot) (*R, error) {
|
|||||||
}
|
}
|
||||||
return &response.Result, nil
|
return &response.Result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
req, err := http.NewRequest("GET", u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
return io.ReadAll(res.Body)
|
||||||
|
}
|
||||||
|
|||||||
27
bot.go
27
bot.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.nix13.pw/scuroneko/extypes"
|
||||||
"git.nix13.pw/scuroneko/slog"
|
"git.nix13.pw/scuroneko/slog"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/vinovest/sqlx"
|
"github.com/vinovest/sqlx"
|
||||||
@@ -30,8 +31,8 @@ type Bot struct {
|
|||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
requestLogger *slog.Logger
|
requestLogger *slog.Logger
|
||||||
|
|
||||||
plugins []*Plugin
|
plugins []Plugin
|
||||||
middlewares []*Middleware
|
middlewares []Middleware
|
||||||
prefixes []string
|
prefixes []string
|
||||||
runners []Runner
|
runners []Runner
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ type Bot struct {
|
|||||||
|
|
||||||
updateOffset int
|
updateOffset int
|
||||||
updateTypes []string
|
updateTypes []string
|
||||||
updateQueue *Queue[*Update]
|
updateQueue *extypes.Queue[*Update]
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotSettings struct {
|
type BotSettings struct {
|
||||||
@@ -73,9 +74,9 @@ func LoadPrefixesFromEnv() []string {
|
|||||||
return strings.Split(prefixesS, ";")
|
return strings.Split(prefixesS, ";")
|
||||||
}
|
}
|
||||||
func NewBot(settings *BotSettings) *Bot {
|
func NewBot(settings *BotSettings) *Bot {
|
||||||
updateQueue := CreateQueue[*Update](256)
|
updateQueue := extypes.CreateQueue[*Update](256)
|
||||||
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,
|
||||||
token: settings.Token,
|
token: settings.Token,
|
||||||
@@ -117,6 +118,12 @@ func NewBot(settings *BotSettings) *Bot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u, err := bot.GetMe()
|
||||||
|
if err != nil {
|
||||||
|
bot.logger.Fatal(err)
|
||||||
|
}
|
||||||
|
bot.logger.Infof("Authorized as %s\n", u.FirstName)
|
||||||
|
|
||||||
return bot
|
return bot
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,20 +178,20 @@ func (b *Bot) Debug(debug bool) *Bot {
|
|||||||
b.debug = debug
|
b.debug = debug
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
func (b *Bot) AddPlugins(plugin ...*Plugin) *Bot {
|
func (b *Bot) AddPlugins(plugin ...Plugin) *Bot {
|
||||||
b.plugins = append(b.plugins, plugin...)
|
b.plugins = append(b.plugins, plugin...)
|
||||||
for _, p := range plugin {
|
for _, p := range plugin {
|
||||||
b.logger.Debugln(fmt.Sprintf("plugins with name \"%s\" registered", p.Name))
|
b.logger.Debugln(fmt.Sprintf("plugins with name \"%s\" registered", p.Name))
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
func (b *Bot) AddMiddleware(middleware ...*Middleware) *Bot {
|
func (b *Bot) AddMiddleware(middleware ...Middleware) *Bot {
|
||||||
b.middlewares = append(b.middlewares, middleware...)
|
b.middlewares = append(b.middlewares, middleware...)
|
||||||
for _, m := range middleware {
|
for _, m := range middleware {
|
||||||
b.logger.Debugln(fmt.Sprintf("middleware with name \"%s\" registered", m.Name))
|
b.logger.Debugln(fmt.Sprintf("middleware with name \"%s\" registered", m.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(&b.middlewares, func(i, j int) bool {
|
sort.Slice(b.middlewares, func(i, j int) bool {
|
||||||
first := b.middlewares[i]
|
first := b.middlewares[i]
|
||||||
second := b.middlewares[j]
|
second := b.middlewares[j]
|
||||||
if first.Order == second.Order {
|
if first.Order == second.Order {
|
||||||
@@ -244,9 +251,7 @@ func (b *Bot) Run() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := &MsgContext{
|
ctx := &MsgContext{Bot: b, Update: u}
|
||||||
Bot: b, Update: u,
|
|
||||||
}
|
|
||||||
for _, middleware := range b.middlewares {
|
for _, middleware := range b.middlewares {
|
||||||
middleware.Execute(ctx, b.dbContext)
|
middleware.Execute(ctx, b.dbContext)
|
||||||
}
|
}
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -1,9 +1,10 @@
|
|||||||
module git.nix13.pw/scuroneko/laniakea
|
module git.nix13.pw/scuroneko/laniakea
|
||||||
|
|
||||||
go 1.25
|
go 1.25.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.nix13.pw/scuroneko/slog v1.0.0
|
git.nix13.pw/scuroneko/extypes 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
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0
|
go.mongodb.org/mongo-driver/v2 v2.5.0
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -1,7 +1,9 @@
|
|||||||
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/slog v1.0.0 h1:PI0YePrmCopjrljUfwCtBIEwNYB+PBgDzPcCXbetpcE=
|
git.nix13.pw/scuroneko/extypes v1.0.2 h1:Qz1InLccaB9crXY33oGrSetPHePKfQAUqW/p/iYXmJk=
|
||||||
git.nix13.pw/scuroneko/slog v1.0.0/go.mod h1:3Qm2wzkR5KjwOponMfG7TcGSDjmYaFqRAmLvSPTuWJI=
|
git.nix13.pw/scuroneko/extypes v1.0.2/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/go.mod h1:3Qm2wzkR5KjwOponMfG7TcGSDjmYaFqRAmLvSPTuWJI=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
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/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 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ func (b *Bot) handleMessage(update *Update, ctx *MsgContext) {
|
|||||||
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, " ")
|
||||||
|
|
||||||
|
if !plugin.executeMiddlewares(ctx, b.dbContext) {
|
||||||
|
return
|
||||||
|
}
|
||||||
go plugin.Execute(cmd, ctx, b.dbContext)
|
go plugin.Execute(cmd, ctx, b.dbContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -57,6 +60,7 @@ func (b *Bot) handleCallback(update *Update, ctx *MsgContext) {
|
|||||||
ctx.From = update.CallbackQuery.From
|
ctx.From = update.CallbackQuery.From
|
||||||
ctx.Msg = update.CallbackQuery.Message
|
ctx.Msg = update.CallbackQuery.Message
|
||||||
ctx.CallbackMsgId = update.CallbackQuery.Message.MessageID
|
ctx.CallbackMsgId = update.CallbackQuery.Message.MessageID
|
||||||
|
ctx.CallbackQueryId = update.CallbackQuery.ID
|
||||||
ctx.Args = data.Args
|
ctx.Args = data.Args
|
||||||
|
|
||||||
for _, plugin := range b.plugins {
|
for _, plugin := range b.plugins {
|
||||||
@@ -64,6 +68,10 @@ func (b *Bot) handleCallback(update *Update, ctx *MsgContext) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !plugin.executeMiddlewares(ctx, b.dbContext) {
|
||||||
|
return
|
||||||
|
}
|
||||||
go plugin.ExecutePayload(data.Command, ctx, b.dbContext)
|
go plugin.ExecutePayload(data.Command, ctx, b.dbContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
117
methods.go
117
methods.go
@@ -73,21 +73,36 @@ func (b *Bot) SendMessage(params *SendMessageP) (*Message, error) {
|
|||||||
return req.Do(b)
|
return req.Do(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendPhotoBaseP 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"`
|
||||||
|
Caption string `json:"caption,omitempty"`
|
||||||
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
|
HasSpoiler bool `json:"has_spoiler,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"`
|
||||||
|
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||||
|
}
|
||||||
type SendPhotoP struct {
|
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"`
|
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||||
Photo string `json:"photo"`
|
|
||||||
Caption string `json:"caption,omitempty"`
|
Caption string `json:"caption,omitempty"`
|
||||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media"`
|
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||||
HasSpoiler bool `json:"has_spoiler"`
|
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"`
|
ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"`
|
||||||
|
Photo string `json:"photo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) SendPhoto(params *SendPhotoP) (*Message, error) {
|
func (b *Bot) SendPhoto(params *SendPhotoP) (*Message, error) {
|
||||||
@@ -130,7 +145,99 @@ type DeleteMessageP struct {
|
|||||||
MessageID int `json:"message_id"`
|
MessageID int `json:"message_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) DeleteMessage(params *DeleteMessageP) (*Message, error) {
|
func (b *Bot) DeleteMessage(params *DeleteMessageP) (bool, error) {
|
||||||
req := NewRequest[Message]("deleteMessage", params)
|
req := NewRequest[bool]("deleteMessage", params)
|
||||||
|
ok, err := req.Do(b)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return *ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnswerCallbackQueryP struct {
|
||||||
|
CallbackQueryID string `json:"callback_query_id"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
ShowAlert bool `json:"show_alert,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
CacheTime int `json:"cache_time,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) AnswerCallbackQuery(params *AnswerCallbackQueryP) (bool, error) {
|
||||||
|
req := NewRequest[bool]("answerCallbackQuery", params)
|
||||||
|
ok, err := req.Do(b)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return *ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetFileP struct {
|
||||||
|
FileId string `json:"file_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) GetFile(params *GetFileP) (*File, error) {
|
||||||
|
req := NewRequest[File]("getFile", params)
|
||||||
return req.Do(b)
|
return req.Do(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SendChatActionP struct {
|
||||||
|
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||||
|
ChatID int `json:"chat_id"`
|
||||||
|
MessageThreadID int `json:"message_thread_id,omitempty"`
|
||||||
|
Action ChatActions `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) SendChatAction(params SendChatActionP) (bool, error) {
|
||||||
|
req := NewRequest[bool]("sendChatAction", params)
|
||||||
|
res, err := req.Do(b)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return *res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetMessageReactionP struct {
|
||||||
|
ChatId int `json:"chat_id"`
|
||||||
|
MessageId int `json:"message_id"`
|
||||||
|
IsBig bool `json:"is_big,omitempty"`
|
||||||
|
}
|
||||||
|
type SetMessageReactionEmojiP struct {
|
||||||
|
SetMessageReactionP
|
||||||
|
Reaction []ReactionTypeEmoji `json:"reaction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) SetMessageReactionEmoji(params SetMessageReactionEmojiP) (bool, error) {
|
||||||
|
req := NewRequest[bool]("setMessageReaction", params)
|
||||||
|
res, err := req.Do(b)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return *res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetMessageReactionCustomEmojiP struct {
|
||||||
|
SetMessageReactionP
|
||||||
|
Reaction []ReactionTypeCustomEmoji `json:"reaction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) SetMessageReactionCustom(params SetMessageReactionCustomEmojiP) (bool, error) {
|
||||||
|
req := NewRequest[bool]("setMessageReaction", params)
|
||||||
|
res, err := req.Do(b)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return *res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetMessageReactionPaidP struct {
|
||||||
|
SetMessageReactionP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) SetMessageReactionPaid(params SetMessageReactionPaidP) (bool, error) {
|
||||||
|
req := NewRequest[bool]("setMessageReaction", params)
|
||||||
|
res, err := req.Do(b)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return *res, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ package laniakea
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
type MsgContext struct {
|
type MsgContext struct {
|
||||||
Bot *Bot
|
Bot *Bot
|
||||||
Msg *Message
|
Msg *Message
|
||||||
Update *Update
|
Update *Update
|
||||||
From *User
|
From *User
|
||||||
CallbackMsgId int
|
CallbackMsgId int
|
||||||
FromID int
|
CallbackQueryId string
|
||||||
Prefix string
|
FromID int
|
||||||
Text string
|
Prefix string
|
||||||
Args []string
|
Text string
|
||||||
|
Args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnswerMessage struct {
|
type AnswerMessage struct {
|
||||||
@@ -75,6 +76,10 @@ 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 {
|
||||||
|
m.ctx.Bot.logger.Errorln("Can't edit caption message, message id is zero")
|
||||||
|
return m
|
||||||
|
}
|
||||||
return m.ctx.editPhotoText(m.MessageID, text, nil)
|
return m.ctx.editPhotoText(m.MessageID, text, nil)
|
||||||
}
|
}
|
||||||
func (m *AnswerMessage) EditCaptionKeyboard(text string, kb *InlineKeyboard) *AnswerMessage {
|
func (m *AnswerMessage) EditCaptionKeyboard(text string, kb *InlineKeyboard) *AnswerMessage {
|
||||||
@@ -114,8 +119,8 @@ func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard) *An
|
|||||||
params := &SendPhotoP{
|
params := &SendPhotoP{
|
||||||
ChatID: ctx.Msg.Chat.ID,
|
ChatID: ctx.Msg.Chat.ID,
|
||||||
Caption: text,
|
Caption: text,
|
||||||
Photo: photoId,
|
|
||||||
ParseMode: ParseMD,
|
ParseMode: ParseMD,
|
||||||
|
Photo: photoId,
|
||||||
}
|
}
|
||||||
if kb != nil {
|
if kb != nil {
|
||||||
params.ReplyMarkup = kb.Get()
|
params.ReplyMarkup = kb.Get()
|
||||||
@@ -123,6 +128,9 @@ func (ctx *MsgContext) answerPhoto(photoId, text string, kb *InlineKeyboard) *An
|
|||||||
msg, err := ctx.Bot.SendPhoto(params)
|
msg, err := ctx.Bot.SendPhoto(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Bot.logger.Errorln(err)
|
ctx.Bot.logger.Errorln(err)
|
||||||
|
return &AnswerMessage{
|
||||||
|
ctx: ctx, Text: text, IsMedia: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &AnswerMessage{
|
return &AnswerMessage{
|
||||||
MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true,
|
MessageID: msg.MessageID, ctx: ctx, Text: text, IsMedia: true,
|
||||||
@@ -151,14 +159,50 @@ func (ctx *MsgContext) CallbackDelete() {
|
|||||||
ctx.delete(ctx.CallbackMsgId)
|
ctx.delete(ctx.CallbackMsgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *MsgContext) Error(err error) {
|
func (ctx *MsgContext) answerCallbackQuery(url, text string, showAlert bool) {
|
||||||
_, sendErr := ctx.Bot.SendMessage(&SendMessageP{
|
if len(ctx.CallbackQueryId) == 0 {
|
||||||
ChatID: ctx.Msg.Chat.ID,
|
return
|
||||||
Text: fmt.Sprintf(ctx.Bot.errorTemplate, EscapeMarkdown(err.Error())),
|
}
|
||||||
|
_, err := ctx.Bot.AnswerCallbackQuery(&AnswerCallbackQueryP{
|
||||||
|
CallbackQueryID: ctx.CallbackQueryId,
|
||||||
|
Text: text, ShowAlert: showAlert, URL: url,
|
||||||
})
|
})
|
||||||
ctx.Bot.logger.Errorln(err)
|
if err != nil {
|
||||||
|
ctx.Bot.logger.Errorln(err)
|
||||||
if sendErr != nil {
|
|
||||||
ctx.Bot.logger.Errorln(sendErr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func (ctx *MsgContext) AnswerCbQuery() {
|
||||||
|
ctx.answerCallbackQuery("", "", false)
|
||||||
|
}
|
||||||
|
func (ctx *MsgContext) AnswerCbQueryText(text string) {
|
||||||
|
ctx.answerCallbackQuery("", text, false)
|
||||||
|
}
|
||||||
|
func (ctx *MsgContext) AnswerCbQueryAlert(text string) {
|
||||||
|
ctx.answerCallbackQuery("", text, true)
|
||||||
|
}
|
||||||
|
func (ctx *MsgContext) AnswerCbQueryUrl(u string) {
|
||||||
|
ctx.answerCallbackQuery(u, "", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *MsgContext) SendAction(action ChatActions) {
|
||||||
|
_, err := ctx.Bot.SendChatAction(SendChatActionP{
|
||||||
|
ChatID: ctx.Msg.Chat.ID, Action: action,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Bot.logger.Errorln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *MsgContext) error(err error) {
|
||||||
|
text := fmt.Sprintf(ctx.Bot.errorTemplate, EscapeMarkdown(err.Error()))
|
||||||
|
|
||||||
|
if ctx.CallbackQueryId != "" {
|
||||||
|
ctx.answerCallbackQuery("", text, false)
|
||||||
|
} else {
|
||||||
|
ctx.answer(text, nil)
|
||||||
|
}
|
||||||
|
ctx.Bot.logger.Errorln(err)
|
||||||
|
}
|
||||||
|
func (ctx *MsgContext) Error(err error) {
|
||||||
|
ctx.error(err)
|
||||||
|
}
|
||||||
|
|||||||
131
multipart.go
Normal file
131
multipart.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package laniakea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Encode[T any](w *multipart.Writer, req T) error {
|
||||||
|
v := reflect.ValueOf(req)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("req must be a struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
t := v.Type()
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := v.Field(i)
|
||||||
|
fieldType := t.Field(i)
|
||||||
|
|
||||||
|
formTags := strings.Split(fieldType.Tag.Get("json"), ",")
|
||||||
|
fieldName := ""
|
||||||
|
if len(formTags) == 0 {
|
||||||
|
formTags = strings.Split(fieldType.Tag.Get("json"), ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(formTags) > 0 {
|
||||||
|
fieldName = formTags[0]
|
||||||
|
if fieldName == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if slices.Index(formTags, "omitempty") >= 0 {
|
||||||
|
if field.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldName = strings.ToLower(fieldType.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
fw io.Writer
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if field.String() != "" {
|
||||||
|
fw, err = w.CreateFormField(fieldName)
|
||||||
|
if err == nil {
|
||||||
|
_, err = fw.Write([]byte(field.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
fw, err = w.CreateFormField(fieldName)
|
||||||
|
if err == nil {
|
||||||
|
_, err = fw.Write([]byte(strconv.FormatInt(field.Int(), 10)))
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
fw, err = w.CreateFormField(fieldName)
|
||||||
|
if err == nil {
|
||||||
|
_, err = fw.Write([]byte(strconv.FormatUint(field.Uint(), 10)))
|
||||||
|
}
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
fw, err = w.CreateFormField(fieldName)
|
||||||
|
if err == nil {
|
||||||
|
_, err = fw.Write([]byte(strconv.FormatFloat(field.Float(), 'f', -1, 64)))
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
fw, err = w.CreateFormField(fieldName)
|
||||||
|
if err == nil {
|
||||||
|
_, err = fw.Write([]byte(strconv.FormatBool(field.Bool())))
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Slice:
|
||||||
|
if field.Type().Elem().Kind() == reflect.Uint8 && !field.IsNil() {
|
||||||
|
filename := fieldType.Tag.Get("filename")
|
||||||
|
if filename == "" {
|
||||||
|
filename = fieldName
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := ""
|
||||||
|
filename = filename + ext
|
||||||
|
|
||||||
|
fw, err = w.CreateFormFile(fieldName, filename)
|
||||||
|
if err == nil {
|
||||||
|
_, err = fw.Write(field.Bytes())
|
||||||
|
}
|
||||||
|
} else if !field.IsNil() {
|
||||||
|
// Handle slice of primitive values (as multiple form fields with the same name)
|
||||||
|
for j := 0; j < field.Len(); j++ {
|
||||||
|
elem := field.Index(j)
|
||||||
|
fw, err = w.CreateFormField(fieldName)
|
||||||
|
if err == nil {
|
||||||
|
switch elem.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
_, err = fw.Write([]byte(elem.String()))
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
_, err = fw.Write([]byte(strconv.FormatInt(elem.Int(), 10)))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
_, err = fw.Write([]byte(strconv.FormatUint(elem.Uint(), 10)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
var jsonData []byte
|
||||||
|
jsonData, err = json.Marshal(field.Interface())
|
||||||
|
if err == nil {
|
||||||
|
fw, err = w.CreateFormField(fieldName)
|
||||||
|
if err == nil {
|
||||||
|
_, err = fw.Write(jsonData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
82
plugins.go
82
plugins.go
@@ -1,6 +1,10 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
import "log"
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.nix13.pw/scuroneko/extypes"
|
||||||
|
)
|
||||||
|
|
||||||
type CommandExecutor func(ctx *MsgContext, dbContext *DatabaseContext)
|
type CommandExecutor func(ctx *MsgContext, dbContext *DatabaseContext)
|
||||||
|
|
||||||
@@ -9,6 +13,7 @@ type PluginBuilder struct {
|
|||||||
commands map[string]*CommandExecutor
|
commands map[string]*CommandExecutor
|
||||||
payloads map[string]*CommandExecutor
|
payloads map[string]*CommandExecutor
|
||||||
updateListener *CommandExecutor
|
updateListener *CommandExecutor
|
||||||
|
middlewares extypes.Slice[*PluginMiddleware]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plugin struct {
|
type Plugin struct {
|
||||||
@@ -16,6 +21,7 @@ type Plugin struct {
|
|||||||
Commands map[string]*CommandExecutor
|
Commands map[string]*CommandExecutor
|
||||||
Payloads map[string]*CommandExecutor
|
Payloads map[string]*CommandExecutor
|
||||||
UpdateListener *CommandExecutor
|
UpdateListener *CommandExecutor
|
||||||
|
Middlewares extypes.Slice[*PluginMiddleware]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlugin(name string) *PluginBuilder {
|
func NewPlugin(name string) *PluginBuilder {
|
||||||
@@ -45,17 +51,22 @@ func (p *PluginBuilder) UpdateListener(listener CommandExecutor) *PluginBuilder
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PluginBuilder) Build() *Plugin {
|
func (p *PluginBuilder) Middleware(middleware *PluginMiddleware) *PluginBuilder {
|
||||||
|
p.middlewares = p.middlewares.Push(middleware)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Println("no command or payloads")
|
||||||
}
|
}
|
||||||
plugin := &Plugin{
|
return Plugin{
|
||||||
Name: p.name,
|
Name: p.name,
|
||||||
Commands: p.commands,
|
Commands: p.commands,
|
||||||
Payloads: p.payloads,
|
Payloads: p.payloads,
|
||||||
UpdateListener: p.updateListener,
|
UpdateListener: p.updateListener,
|
||||||
|
Middlewares: p.middlewares,
|
||||||
}
|
}
|
||||||
return plugin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Plugin) Execute(cmd string, ctx *MsgContext, dbContext *DatabaseContext) {
|
func (p *Plugin) Execute(cmd string, ctx *MsgContext, dbContext *DatabaseContext) {
|
||||||
@@ -66,28 +77,37 @@ func (p *Plugin) ExecutePayload(payload string, ctx *MsgContext, dbContext *Data
|
|||||||
(*p.Payloads[payload])(ctx, dbContext)
|
(*p.Payloads[payload])(ctx, dbContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Plugin) executeMiddlewares(ctx *MsgContext, db *DatabaseContext) bool {
|
||||||
|
for _, m := range p.Middlewares {
|
||||||
|
if !m.Execute(ctx, db) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
Name string
|
Name string
|
||||||
Executor *CommandExecutor
|
Executor CommandExecutor
|
||||||
Order int
|
Order int
|
||||||
Async bool
|
Async bool
|
||||||
}
|
}
|
||||||
type MiddlewareBuilder struct {
|
type MiddlewareBuilder struct {
|
||||||
name string
|
name string
|
||||||
executor *CommandExecutor
|
executor CommandExecutor
|
||||||
order int
|
order int
|
||||||
async bool
|
async bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMiddleware(name string) *MiddlewareBuilder {
|
func NewMiddleware(name string, executor CommandExecutor) *MiddlewareBuilder {
|
||||||
return &MiddlewareBuilder{name: name, async: false}
|
return &MiddlewareBuilder{name: name, executor: executor, order: 0, async: false}
|
||||||
}
|
}
|
||||||
func (m *MiddlewareBuilder) SetName(name string) *MiddlewareBuilder {
|
func (m *MiddlewareBuilder) SetName(name string) *MiddlewareBuilder {
|
||||||
m.name = name
|
m.name = name
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
func (m *MiddlewareBuilder) SetExecutor(executor CommandExecutor) *MiddlewareBuilder {
|
func (m *MiddlewareBuilder) SetExecutor(executor CommandExecutor) *MiddlewareBuilder {
|
||||||
m.executor = &executor
|
m.executor = executor
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
func (m *MiddlewareBuilder) SetOrder(order int) *MiddlewareBuilder {
|
func (m *MiddlewareBuilder) SetOrder(order int) *MiddlewareBuilder {
|
||||||
@@ -98,19 +118,51 @@ func (m *MiddlewareBuilder) SetAsync(async bool) *MiddlewareBuilder {
|
|||||||
m.async = async
|
m.async = async
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
func (m *MiddlewareBuilder) Build() *Middleware {
|
func (m *MiddlewareBuilder) Build() Middleware {
|
||||||
return &Middleware{
|
return Middleware{
|
||||||
Name: m.name,
|
Name: m.name,
|
||||||
Executor: m.executor,
|
Executor: m.executor,
|
||||||
Order: m.order,
|
Order: m.order,
|
||||||
Async: m.async,
|
Async: m.async,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func (m *Middleware) Execute(ctx *MsgContext, db *DatabaseContext) {
|
func (m Middleware) Execute(ctx *MsgContext, db *DatabaseContext) {
|
||||||
exec := *m.Executor
|
|
||||||
if m.Async {
|
if m.Async {
|
||||||
go exec(ctx, db)
|
go m.Executor(ctx, db)
|
||||||
} else {
|
} else {
|
||||||
exec(ctx, db)
|
m.Execute(ctx, db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PluginMiddlewareExecutor func(ctx *MsgContext, db *DatabaseContext) bool
|
||||||
|
|
||||||
|
// PluginMiddleware
|
||||||
|
// When async, returned value ignored
|
||||||
|
type PluginMiddleware struct {
|
||||||
|
executor PluginMiddlewareExecutor
|
||||||
|
order int
|
||||||
|
async bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPluginMiddleware(executor PluginMiddlewareExecutor) *PluginMiddleware {
|
||||||
|
return &PluginMiddleware{
|
||||||
|
executor: executor,
|
||||||
|
order: 0,
|
||||||
|
async: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *PluginMiddleware) SetOrder(order int) *PluginMiddleware {
|
||||||
|
m.order = order
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
func (m *PluginMiddleware) SetAsync(async bool) *PluginMiddleware {
|
||||||
|
m.async = async
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
func (m *PluginMiddleware) Execute(ctx *MsgContext, db *DatabaseContext) bool {
|
||||||
|
if m.async {
|
||||||
|
go m.executor(ctx, db)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return m.executor(ctx, db)
|
||||||
|
}
|
||||||
|
|||||||
71
queue.go
71
queue.go
@@ -1,71 +0,0 @@
|
|||||||
package laniakea
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var QueueFullErr = errors.New("queue full")
|
|
||||||
|
|
||||||
type Queue[T any] struct {
|
|
||||||
size uint64
|
|
||||||
mu sync.RWMutex
|
|
||||||
queue []T
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateQueue[T any](size uint64) *Queue[T] {
|
|
||||||
return &Queue[T]{
|
|
||||||
queue: make([]T, 0),
|
|
||||||
size: size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue[T]) Enqueue(el T) error {
|
|
||||||
if q.IsFull() {
|
|
||||||
return QueueFullErr
|
|
||||||
}
|
|
||||||
q.queue = append(q.queue, el)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue[T]) Peak() T {
|
|
||||||
q.mu.RLock()
|
|
||||||
defer q.mu.RUnlock()
|
|
||||||
return q.queue[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue[T]) IsEmpty() bool {
|
|
||||||
return q.Length() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue[T]) IsFull() bool {
|
|
||||||
return q.Length() == q.size
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queue[T]) Raw() []T {
|
|
||||||
return q.queue
|
|
||||||
}
|
|
||||||
28
types.go
28
types.go
@@ -1,5 +1,7 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
|
import "git.nix13.pw/scuroneko/extypes"
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
UpdateID int `json:"update_id"`
|
UpdateID int `json:"update_id"`
|
||||||
Message *Message `json:"message"`
|
Message *Message `json:"message"`
|
||||||
@@ -54,9 +56,9 @@ type Message struct {
|
|||||||
Chat *Chat `json:"chat,omitempty"`
|
Chat *Chat `json:"chat,omitempty"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
|
||||||
Photo []*PhotoSize `json:"photo,omitempty"`
|
Photo extypes.Slice[*PhotoSize] `json:"photo,omitempty"`
|
||||||
Caption string `json:"caption,omitempty"`
|
Caption string `json:"caption,omitempty"`
|
||||||
ReplyToMessage *Message `json:"reply_to_message"`
|
ReplyToMessage *Message `json:"reply_to_message"`
|
||||||
|
|
||||||
ReplyMarkup *MessageReplyMarkup `json:"reply_markup,omitempty"`
|
ReplyMarkup *MessageReplyMarkup `json:"reply_markup,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -172,3 +174,23 @@ type ReactionCount struct {
|
|||||||
Type *ReactionType `json:"type"`
|
Type *ReactionType `json:"type"`
|
||||||
TotalCount int `json:"total_count"`
|
TotalCount int `json:"total_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
FileId string `json:"file_id"`
|
||||||
|
FileUniqueID string `json:"file_unique_id"`
|
||||||
|
FileSize int `json:"file_size,omitempty"`
|
||||||
|
FilePath string `json:"file_path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatActions string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChatActionTyping ChatActions = "typing"
|
||||||
|
ChatActionUploadPhoto ChatActions = "upload_photo"
|
||||||
|
ChatActionUploadVideo ChatActions = "upload_video"
|
||||||
|
ChatActionUploadVoice ChatActions = "upload_voice"
|
||||||
|
ChatActionUploadDocument ChatActions = "upload_document"
|
||||||
|
ChatActionChooseSticker ChatActions = "choose_sticker"
|
||||||
|
ChatActionFindLocation ChatActions = "find_location"
|
||||||
|
ChatActionUploadVideoNone ChatActions = "upload_video_none"
|
||||||
|
)
|
||||||
|
|||||||
136
uploader.go
Normal file
136
uploader.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package laniakea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Uploader struct {
|
||||||
|
bot *Bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUploader(bot *Bot) *Uploader {
|
||||||
|
return &Uploader{bot: bot}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploaderFileType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UploaderPhotoType UploaderFileType = "photo"
|
||||||
|
UploaderVideoType UploaderFileType = "video"
|
||||||
|
UploaderAudioType UploaderFileType = "audio"
|
||||||
|
UploaderDocumentType UploaderFileType = "document"
|
||||||
|
UploaderVoiceType UploaderFileType = "voice"
|
||||||
|
UploaderVideoNoteType UploaderFileType = "video_note"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploaderFile struct {
|
||||||
|
filename string
|
||||||
|
data []byte
|
||||||
|
t UploaderFileType
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUploaderFile(name string, data []byte) UploaderFile {
|
||||||
|
t := uploaderTypeByExt(name)
|
||||||
|
return UploaderFile{filename: name, data: data, t: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetType used when auto-detect failed. I.e. you sending a voice message, but it detects as audio
|
||||||
|
func (f UploaderFile) SetType(t UploaderFileType) UploaderFile {
|
||||||
|
f.t = t
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploaderRequest[R, P any] struct {
|
||||||
|
method string
|
||||||
|
file UploaderFile
|
||||||
|
params P
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUploaderRequest[R, P any](method string, file UploaderFile, params P) UploaderRequest[R, P] {
|
||||||
|
return UploaderRequest[R, P]{method, file, params}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UploaderRequest[R, P]) Do(bot *Bot) (*R, error) {
|
||||||
|
url := fmt.Sprintf("https://api.telegram.org/bot%s/%s", bot.token, u.method)
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
w := multipart.NewWriter(buf)
|
||||||
|
|
||||||
|
fw, err := w.CreateFormFile(string(u.file.t), u.file.filename)
|
||||||
|
if err != nil {
|
||||||
|
w.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = fw.Write(u.file.data)
|
||||||
|
if err != nil {
|
||||||
|
w.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = Encode(w, u.params)
|
||||||
|
if err != nil {
|
||||||
|
w.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", w.FormDataContentType())
|
||||||
|
bot.logger.Debugln("UPLOADER REQ", u.method)
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bot.logger.Debugln("UPLOADER RES", u.method, string(body))
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("[%d] %s", res.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
response := new(ApiResponse[*R])
|
||||||
|
err = json.Unmarshal(body, response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !response.Ok {
|
||||||
|
return nil, fmt.Errorf("[%d] %s", response.ErrorCode, response.Description)
|
||||||
|
}
|
||||||
|
return response.Result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Uploader) UploadPhoto(file UploaderFile, params SendPhotoBaseP) (*Message, error) {
|
||||||
|
req := NewUploaderRequest[Message]("sendPhoto", file, params)
|
||||||
|
return req.Do(u.bot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploaderTypeByExt(filename string) UploaderFileType {
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".webp", ".bmp":
|
||||||
|
return UploaderPhotoType
|
||||||
|
case ".mp4":
|
||||||
|
return UploaderVideoType
|
||||||
|
case ".mp3", ".m4a":
|
||||||
|
return UploaderAudioType
|
||||||
|
case ".ogg":
|
||||||
|
return UploaderVoiceType
|
||||||
|
default:
|
||||||
|
return UploaderDocumentType
|
||||||
|
}
|
||||||
|
}
|
||||||
58
utils.go
58
utils.go
@@ -41,12 +41,12 @@ func MapToJson(m map[string]any) (string, error) {
|
|||||||
return string(data), err
|
return string(data), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func StructToMap(s interface{}) (map[string]interface{}, error) {
|
func StructToMap(s any) (map[string]any, error) {
|
||||||
data, err := json.Marshal(s)
|
data, err := json.Marshal(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
m := make(map[string]interface{})
|
m := make(map[string]any)
|
||||||
err = json.Unmarshal(data, &m)
|
err = json.Unmarshal(data, &m)
|
||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
@@ -73,57 +73,3 @@ func EscapeMarkdownV2(s string) string {
|
|||||||
}
|
}
|
||||||
return s
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VersionString = "0.3.2"
|
VersionString = "0.3.9"
|
||||||
VersionMajor = 0
|
VersionMajor = 0
|
||||||
VersionMinor = 3
|
VersionMinor = 3
|
||||||
VersionPatch = 2
|
VersionPatch = 9
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user