Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 012854be41 | |||
| eaa5dbaf10 |
68
api.go
Normal file
68
api.go
Normal file
@@ -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",
|
||||||
|
"<TOKEN>",
|
||||||
|
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")
|
||||||
|
}
|
||||||
310
bot.go
310
bot.go
@@ -1,11 +1,9 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"log"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -36,6 +34,7 @@ type Bot struct {
|
|||||||
plugins []*Plugin
|
plugins []*Plugin
|
||||||
middlewares []*Middleware
|
middlewares []*Middleware
|
||||||
prefixes []string
|
prefixes []string
|
||||||
|
runners []Runner
|
||||||
|
|
||||||
dbContext *DatabaseContext
|
dbContext *DatabaseContext
|
||||||
|
|
||||||
@@ -67,29 +66,18 @@ func LoadSettingsFromEnv() *BotSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MsgContext struct {
|
func LoadPrefixesFromEnv() []string {
|
||||||
Bot *Bot
|
prefixesS, exists := os.LookupEnv("PREFIXES")
|
||||||
Msg *Message
|
if !exists {
|
||||||
Update *Update
|
return []string{"!"}
|
||||||
From *User
|
}
|
||||||
CallbackMsgId int
|
return strings.Split(prefixesS, ";")
|
||||||
FromID int
|
|
||||||
Prefix string
|
|
||||||
Text string
|
|
||||||
Args []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseContext struct {
|
|
||||||
PostgresSQL *sqlx.DB
|
|
||||||
MongoDB *mongo.Client
|
|
||||||
Redis *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBot(settings *BotSettings) *Bot {
|
func NewBot(settings *BotSettings) *Bot {
|
||||||
updateQueue := CreateQueue[*Update](256)
|
updateQueue := 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),
|
prefixes: settings.Prefixes, updateTypes: make([]string, 0), runners: make([]Runner, 0),
|
||||||
updateQueue: updateQueue,
|
updateQueue: updateQueue,
|
||||||
token: settings.Token,
|
token: settings.Token,
|
||||||
}
|
}
|
||||||
@@ -97,10 +85,10 @@ func NewBot(settings *BotSettings) *Bot {
|
|||||||
if len(settings.ErrorTemplate) > 0 {
|
if len(settings.ErrorTemplate) > 0 {
|
||||||
bot.errorTemplate = settings.ErrorTemplate
|
bot.errorTemplate = settings.ErrorTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(settings.LoggerBasePath) == 0 {
|
if len(settings.LoggerBasePath) == 0 {
|
||||||
settings.LoggerBasePath = "./"
|
settings.LoggerBasePath = "./"
|
||||||
}
|
}
|
||||||
|
|
||||||
level := slog.FATAL
|
level := slog.FATAL
|
||||||
if settings.Debug {
|
if settings.Debug {
|
||||||
level = slog.DEBUG
|
level = slog.DEBUG
|
||||||
@@ -134,14 +122,22 @@ func NewBot(settings *BotSettings) *Bot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) Close() {
|
func (b *Bot) Close() {
|
||||||
b.logger.Close()
|
err := b.logger.Close()
|
||||||
b.requestLogger.Close()
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
err = b.requestLogger.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) InitDatabaseContext(ctx *DatabaseContext) *Bot {
|
type DatabaseContext struct {
|
||||||
b.dbContext = ctx
|
PostgresSQL *sqlx.DB
|
||||||
return b
|
MongoDB *mongo.Client
|
||||||
|
Redis *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) slog.LoggerWriter) *Bot {
|
func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) slog.LoggerWriter) *Bot {
|
||||||
w := writer(b.dbContext)
|
w := writer(b.dbContext)
|
||||||
b.logger.AddWriter(w)
|
b.logger.AddWriter(w)
|
||||||
@@ -151,6 +147,10 @@ func (b *Bot) AddDatabaseLogger(writer func(db *DatabaseContext) slog.LoggerWrit
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bot) DatabaseContext(ctx *DatabaseContext) *Bot {
|
||||||
|
b.dbContext = ctx
|
||||||
|
return b
|
||||||
|
}
|
||||||
func (b *Bot) UpdateTypes(t ...string) *Bot {
|
func (b *Bot) UpdateTypes(t ...string) *Bot {
|
||||||
b.updateTypes = make([]string, 0)
|
b.updateTypes = make([]string, 0)
|
||||||
b.updateTypes = append(b.updateTypes, t...)
|
b.updateTypes = append(b.updateTypes, t...)
|
||||||
@@ -164,18 +164,11 @@ func (b *Bot) AddPrefixes(prefixes ...string) *Bot {
|
|||||||
b.prefixes = append(b.prefixes, prefixes...)
|
b.prefixes = append(b.prefixes, prefixes...)
|
||||||
return b
|
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 {
|
func (b *Bot) ErrorTemplate(s string) *Bot {
|
||||||
b.errorTemplate = s
|
b.errorTemplate = s
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
func (b *Bot) Debugln(debug bool) *Bot {
|
func (b *Bot) Debug(debug bool) *Bot {
|
||||||
b.debug = debug
|
b.debug = debug
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@@ -202,6 +195,17 @@ func (b *Bot) AddMiddleware(middleware ...*Middleware) *Bot {
|
|||||||
}
|
}
|
||||||
return b
|
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() {
|
func (b *Bot) Run() {
|
||||||
if len(b.prefixes) == 0 {
|
if len(b.prefixes) == 0 {
|
||||||
@@ -214,15 +218,16 @@ func (b *Bot) Run() {
|
|||||||
return
|
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() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
_, err := b.Updates()
|
_, err := b.Updates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.logger.Errorln(err)
|
b.logger.Errorln(err)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Millisecond * 10)
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -239,8 +244,7 @@ func (b *Bot) Run() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ctx := &MsgContext{
|
ctx := &MsgContext{
|
||||||
Bot: b,
|
Bot: b, Update: u,
|
||||||
Update: u,
|
|
||||||
}
|
}
|
||||||
for _, middleware := range b.middlewares {
|
for _, middleware := range b.middlewares {
|
||||||
middleware.Execute(ctx, b.dbContext)
|
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}
|
// {"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) {
|
func (b *Bot) handleMessage(update *Update, ctx *MsgContext) {
|
||||||
var text string
|
|
||||||
if update.Message == nil {
|
if update.Message == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var text string
|
||||||
if len(update.Message.Text) > 0 {
|
if len(update.Message.Text) > 0 {
|
||||||
text = update.Message.Text
|
text = update.Message.Text
|
||||||
} else {
|
} else {
|
||||||
text = update.Message.Caption
|
text = update.Message.Caption
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.FromID = update.Message.From.ID
|
|
||||||
ctx.From = update.Message.From
|
|
||||||
ctx.Msg = update.Message
|
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
prefix, hasPrefix := b.checkPrefixes(text)
|
prefix, hasPrefix := b.checkPrefixes(text)
|
||||||
if !hasPrefix {
|
if !hasPrefix {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Prefix = prefix
|
ctx.Prefix = prefix
|
||||||
|
ctx.FromID = update.Message.From.ID
|
||||||
|
ctx.From = update.Message.From
|
||||||
|
ctx.Msg = update.Message
|
||||||
|
|
||||||
text = strings.TrimSpace(text[len(prefix):])
|
text = strings.TrimSpace(text[len(prefix):])
|
||||||
|
|
||||||
@@ -296,6 +301,7 @@ func (b *Bot) handleMessage(update *Update, ctx *MsgContext) {
|
|||||||
ctx.Args = strings.Split(ctx.Text, " ")
|
ctx.Args = strings.Split(ctx.Text, " ")
|
||||||
|
|
||||||
go plugin.Execute(cmd, ctx, b.dbContext)
|
go plugin.Execute(cmd, ctx, b.dbContext)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,7 +326,7 @@ func (b *Bot) handleCallback(update *Update, ctx *MsgContext) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go plugin.ExecutePayload(data.Command, ctx, b.dbContext)
|
go plugin.ExecutePayload(data.Command, ctx, b.dbContext)
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,219 +338,3 @@ func (b *Bot) checkPrefixes(text string) (string, bool) {
|
|||||||
}
|
}
|
||||||
return "", false
|
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",
|
|
||||||
"<TOKEN>",
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -22,7 +22,7 @@ require (
|
|||||||
github.com/xdg-go/scram v1.1.2 // indirect
|
github.com/xdg-go/scram v1.1.2 // indirect
|
||||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // 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/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
|||||||
22
methods.go
22
methods.go
@@ -18,29 +18,21 @@ func (b *Bot) Updates() ([]*Update, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
res := make([]*Update, 0)
|
res := make([]*Update, 0)
|
||||||
for _, u := range data["data"].([]any) {
|
err = AnyToStruct(data["data"], &res)
|
||||||
updateObj := new(Update)
|
|
||||||
data, err := json.Marshal(u)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(data, updateObj)
|
|
||||||
|
for _, u := range res {
|
||||||
|
b.updateOffset = u.UpdateID + 1
|
||||||
|
err = b.updateQueue.Enqueue(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
//err = MapToStruct(u.(map[string]any), updateObj)
|
res = append(res, u)
|
||||||
//if err != nil {
|
|
||||||
// return res, err
|
|
||||||
//}
|
|
||||||
b.updateOffset = updateObj.UpdateID + 1
|
|
||||||
err = b.updateQueue.Enqueue(updateObj)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
res = append(res, updateObj)
|
|
||||||
|
|
||||||
if b.debug && b.requestLogger != nil {
|
if b.debug && b.requestLogger != nil {
|
||||||
j, err := MapToJson(u.(map[string]interface{}))
|
j, err := json.Marshal(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.logger.Error(err)
|
b.logger.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
164
msg_context.go
Normal file
164
msg_context.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
80
runners.go
Normal file
80
runners.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
utils.go
25
utils.go
@@ -6,7 +6,8 @@ import (
|
|||||||
"strings"
|
"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)
|
data, err := json.Marshal(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -15,7 +16,27 @@ func MapToStruct(m map[string]interface{}, s interface{}) error {
|
|||||||
return err
|
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)
|
data, err := json.Marshal(m)
|
||||||
return string(data), err
|
return string(data), err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package laniakea
|
package laniakea
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VersionString = "0.2.5"
|
VersionString = "0.3.0"
|
||||||
VersionMajor = 0
|
VersionMajor = 0
|
||||||
VersionMinor = 2
|
VersionMinor = 3
|
||||||
VersionPatch = 5
|
VersionPatch = 0
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user