first commit
This commit is contained in:
commit
f203e3e056
195
bot.go
Normal file
195
bot.go
Normal file
@ -0,0 +1,195 @@
|
||||
package laniakea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"scurotgbot/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
token string
|
||||
logger *utils.Logger
|
||||
debug bool
|
||||
|
||||
plugins []*Plugin
|
||||
prefixes []string
|
||||
|
||||
updateOffset int
|
||||
updateQueue *utils.Queue[*Update]
|
||||
}
|
||||
|
||||
type MsgContext struct {
|
||||
Bot *Bot
|
||||
Msg *Message
|
||||
FromID int
|
||||
Prefix string
|
||||
Text string
|
||||
}
|
||||
|
||||
func NewBot(token string) *Bot {
|
||||
logger := utils.Create()
|
||||
logger.SetLevel(utils.DEBUG)
|
||||
updateQueue := utils.CreateQueue[*Update](256)
|
||||
bot := &Bot{
|
||||
updateOffset: 0, plugins: make([]*Plugin, 0), debug: false,
|
||||
prefixes: make([]string, 0),
|
||||
updateQueue: updateQueue,
|
||||
token: token, logger: logger,
|
||||
}
|
||||
|
||||
return bot
|
||||
}
|
||||
|
||||
func (b *Bot) AddPrefixes(prefixes ...string) {
|
||||
b.prefixes = append(b.prefixes, prefixes...)
|
||||
}
|
||||
|
||||
func (b *Bot) Run() {
|
||||
if len(b.prefixes) == 0 {
|
||||
b.logger.Fatal("no prefixes defined")
|
||||
return
|
||||
}
|
||||
|
||||
if len(b.plugins) == 0 {
|
||||
b.logger.Fatal("no plugins defined")
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, err := b.GetUpdates()
|
||||
if err != nil {
|
||||
b.logger.Error(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
queue := b.GetQueue()
|
||||
if queue.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
u := queue.Dequeue()
|
||||
b.handleMessage(u)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) handleMessage(update *Update) {
|
||||
text := strings.TrimSpace(update.Message.Text)
|
||||
prefix, hasPrefix := b.checkPrefixes(text)
|
||||
if !hasPrefix {
|
||||
return
|
||||
}
|
||||
|
||||
text = strings.TrimSpace(text[len(prefix):])
|
||||
|
||||
for _, plugin := range b.plugins {
|
||||
for cmd, _ := range plugin.Commands {
|
||||
if !strings.HasPrefix(text, cmd) {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx := &MsgContext{
|
||||
Bot: b,
|
||||
FromID: update.Message.From.ID,
|
||||
Msg: update.Message,
|
||||
Prefix: prefix,
|
||||
Text: text[len(cmd):],
|
||||
}
|
||||
|
||||
plugin.Execute(cmd, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) checkPrefixes(text string) (string, bool) {
|
||||
for _, prefix := range b.prefixes {
|
||||
if strings.HasPrefix(text, prefix) {
|
||||
return prefix, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (b *Bot) AddPlugins(plugin ...*Plugin) {
|
||||
b.plugins = append(b.plugins, plugin...)
|
||||
}
|
||||
|
||||
func (b *Bot) SetDebug(debug bool) {
|
||||
b.debug = debug
|
||||
}
|
||||
|
||||
func (b *Bot) GetQueue() *utils.Queue[*Update] {
|
||||
return b.updateQueue
|
||||
}
|
||||
|
||||
func (ctx *MsgContext) Answer(text string) {
|
||||
_, err := ctx.Bot.SendMessage(&SendMessageP{
|
||||
ChatID: ctx.Msg.Chat.ID,
|
||||
Text: text,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Bot.logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
type ApiResponse struct {
|
||||
Ok bool `json:"ok"`
|
||||
Result map[string]interface{} `json:"result,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
}
|
||||
|
||||
type ApiResponseA struct {
|
||||
Ok bool `json:"ok"`
|
||||
Result []interface{} `json:"result,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ErrorCode int `json:"error_code,omitempty"`
|
||||
}
|
||||
|
||||
// request is a low-level call to api.
|
||||
func (b *Bot) request(methodName string, params map[string]interface{}) (map[string]interface{}, error) {
|
||||
var buf bytes.Buffer
|
||||
err := json.NewEncoder(&buf).Encode(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b.debug {
|
||||
b.logger.Debug(fmt.Sprintf("POST https://api.telegram.org/bot%s/%s %s", "<TOKEN>", methodName, string(buf.Bytes())))
|
||||
}
|
||||
r, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/%s", b.token, methodName), "application/json", &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := new(ApiResponse)
|
||||
|
||||
var result map[string]interface{}
|
||||
|
||||
err = json.Unmarshal(data, &response)
|
||||
if err != nil {
|
||||
responseArray := new(ApiResponseA)
|
||||
err = json.Unmarshal(data, responseArray)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = map[string]interface{}{
|
||||
"data": responseArray.Result,
|
||||
}
|
||||
} else {
|
||||
result = response.Result
|
||||
}
|
||||
if !response.Ok {
|
||||
return nil, fmt.Errorf("[%d] %s", response.ErrorCode, response.Description)
|
||||
}
|
||||
return result, err
|
||||
}
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
||||
module git.nix13.pw/ScuroNeko/Laniakea
|
||||
|
||||
go 1.23.4
|
||||
|
||||
require github.com/fatih/color v1.18.0
|
||||
|
||||
require (
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
)
|
11
go.sum
Normal file
11
go.sum
Normal file
@ -0,0 +1,11 @@
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
152
logger.go
Normal file
152
logger.go
Normal file
@ -0,0 +1,152 @@
|
||||
package laniakea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
prefix string
|
||||
level LogLevel
|
||||
printTraceback bool
|
||||
printTime bool
|
||||
}
|
||||
|
||||
type LogLevel struct {
|
||||
n uint8
|
||||
t string
|
||||
c color.Attribute
|
||||
}
|
||||
|
||||
type MethodTraceback struct {
|
||||
Package string
|
||||
Method string
|
||||
fullPath string
|
||||
signature string
|
||||
filename string
|
||||
line int
|
||||
}
|
||||
|
||||
var (
|
||||
INFO LogLevel = LogLevel{n: 0, t: "info", c: color.FgWhite}
|
||||
WARN LogLevel = LogLevel{n: 1, t: "warn", c: color.FgHiYellow}
|
||||
ERROR LogLevel = LogLevel{n: 2, t: "error", c: color.FgHiRed}
|
||||
FATAL LogLevel = LogLevel{n: 3, t: "fatal", c: color.FgRed}
|
||||
DEBUG LogLevel = LogLevel{n: 4, t: "debug", c: color.FgGreen}
|
||||
)
|
||||
|
||||
var (
|
||||
GET LogLevel = LogLevel{n: 0, t: "get", c: color.FgWhite}
|
||||
POST LogLevel = LogLevel{n: 0, t: "post", c: color.FgBlue}
|
||||
PUT LogLevel = LogLevel{n: 0, t: "put", c: color.FgYellow}
|
||||
DELETE LogLevel = LogLevel{n: 0, t: "delete", c: color.FgHiRed}
|
||||
)
|
||||
|
||||
var methodLevelMap = map[string]LogLevel{
|
||||
"get": GET, "post": POST, "put": PUT, "delete": DELETE,
|
||||
}
|
||||
|
||||
func Create() *Logger {
|
||||
return &Logger{
|
||||
prefix: "LOG",
|
||||
level: FATAL,
|
||||
printTraceback: false,
|
||||
printTime: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) GetHTTPLogger(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
level := methodLevelMap[r.Method]
|
||||
color.New(level.c).Println(l.buildString(methodLevelMap[strings.ToLower(r.Method)], "Hello"))
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Logger) SetPrefix(prefix string) {
|
||||
l.prefix = prefix
|
||||
}
|
||||
func (l *Logger) SetLevel(level LogLevel) {
|
||||
l.level = level
|
||||
}
|
||||
func (l *Logger) SetPrintTraceback(b bool) {
|
||||
l.printTraceback = b
|
||||
}
|
||||
|
||||
func (l *Logger) Info(m interface{}) {
|
||||
l.print(INFO, m)
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(m interface{}) {
|
||||
l.print(WARN, m)
|
||||
}
|
||||
|
||||
func (l *Logger) Error(m interface{}) {
|
||||
l.print(ERROR, m)
|
||||
}
|
||||
|
||||
func (l *Logger) Fatal(m interface{}) {
|
||||
l.print(FATAL, m)
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(m interface{}) {
|
||||
l.print(DEBUG, m)
|
||||
}
|
||||
|
||||
func (l *Logger) formatTime(t time.Time) string {
|
||||
return fmt.Sprintf("%02d.%02d.%02d %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second())
|
||||
}
|
||||
|
||||
func (l *Logger) getTraceback() *MethodTraceback {
|
||||
caller, _, _, _ := runtime.Caller(4)
|
||||
details := runtime.FuncForPC(caller)
|
||||
signature := details.Name()
|
||||
path, line := details.FileLine(caller)
|
||||
splitPath := strings.Split(path, "/")
|
||||
|
||||
splitSignature := strings.Split(signature, ".")
|
||||
pkg, method := splitSignature[0], splitSignature[len(splitSignature)-1]
|
||||
|
||||
tb := &MethodTraceback{
|
||||
filename: splitPath[len(splitPath)-1],
|
||||
fullPath: path,
|
||||
line: line,
|
||||
signature: signature,
|
||||
Package: pkg,
|
||||
Method: method,
|
||||
}
|
||||
|
||||
return tb
|
||||
}
|
||||
func (l *Logger) formatTraceback(mt *MethodTraceback) string {
|
||||
return fmt.Sprintf("%s:%s:%d", mt.filename, mt.Method, mt.line)
|
||||
}
|
||||
|
||||
func (l *Logger) buildString(level LogLevel, m interface{}) string {
|
||||
args := []string{
|
||||
fmt.Sprintf("[%s]", l.prefix),
|
||||
fmt.Sprintf("[%s]", strings.ToUpper(level.t)),
|
||||
}
|
||||
|
||||
if l.printTraceback {
|
||||
args = append(args, fmt.Sprintf("[%s]", l.formatTraceback(l.getTraceback())))
|
||||
}
|
||||
|
||||
if l.printTime {
|
||||
args = append(args, fmt.Sprintf("[%s]", l.formatTime(time.Now())))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %v", strings.Join(args, " "), m)
|
||||
}
|
||||
|
||||
func (l *Logger) print(level LogLevel, m interface{}) {
|
||||
if l.level.n < level.n {
|
||||
return
|
||||
}
|
||||
color.New(level.c).Print(l.buildString(level, m))
|
||||
}
|
58
methods.go
Normal file
58
methods.go
Normal file
@ -0,0 +1,58 @@
|
||||
package laniakea
|
||||
|
||||
import "fmt"
|
||||
|
||||
var NO_PARAMS = make(map[string]interface{})
|
||||
|
||||
func (b *Bot) GetUpdates() ([]*Update, error) {
|
||||
params := make(map[string]interface{})
|
||||
params["offset"] = b.updateOffset
|
||||
params["timeout"] = 30
|
||||
|
||||
data, err := b.request("getUpdates", params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make([]*Update, 0)
|
||||
for _, u := range data["data"].([]interface{}) {
|
||||
updateObj := new(Update)
|
||||
err = MapToStruct(u.(map[string]interface{}), updateObj)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
b.updateOffset = updateObj.UpdateID + 1
|
||||
err = b.updateQueue.Enqueue(updateObj)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res = append(res, updateObj)
|
||||
|
||||
if b.debug {
|
||||
j, err := MapToJson(u.(map[string]interface{}))
|
||||
if err != nil {
|
||||
b.logger.Error(err)
|
||||
}
|
||||
b.logger.Debug(fmt.Sprintf("New update: %s\n", j))
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
type SendMessageP struct {
|
||||
ChatID int `json:"chat_id"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (b *Bot) SendMessage(params *SendMessageP) (*Message, error) {
|
||||
dataP, err := StructToMap(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := b.request("sendMessage", dataP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
message := new(Message)
|
||||
err = MapToStruct(data, message)
|
||||
return message, err
|
||||
}
|
48
plugins.go
Normal file
48
plugins.go
Normal file
@ -0,0 +1,48 @@
|
||||
package laniakea
|
||||
|
||||
type CommandExecutor func(ctx *MsgContext)
|
||||
|
||||
type PluginBuilder struct {
|
||||
name string
|
||||
commands map[string]*CommandExecutor
|
||||
payload string
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
Name string
|
||||
Commands map[string]*CommandExecutor
|
||||
Payload string
|
||||
}
|
||||
|
||||
func NewPlugin(name string) *PluginBuilder {
|
||||
return &PluginBuilder{
|
||||
name: name,
|
||||
commands: make(map[string]*CommandExecutor),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginBuilder) Command(cmd string, f CommandExecutor) *PluginBuilder {
|
||||
p.commands[cmd] = &f
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PluginBuilder) Payload(payload string) *PluginBuilder {
|
||||
p.payload = payload
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PluginBuilder) Build() *Plugin {
|
||||
if len(p.commands) == 0 {
|
||||
return nil
|
||||
}
|
||||
plugin := &Plugin{
|
||||
Name: p.name,
|
||||
Commands: p.commands,
|
||||
Payload: p.payload,
|
||||
}
|
||||
return plugin
|
||||
}
|
||||
|
||||
func (p *Plugin) Execute(cmd string, ctx *MsgContext) {
|
||||
(*p.Commands[cmd])(ctx)
|
||||
}
|
53
queue.go
Normal file
53
queue.go
Normal file
@ -0,0 +1,53 @@
|
||||
package laniakea
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Queue[T any] struct {
|
||||
queue []T
|
||||
size uint64
|
||||
}
|
||||
|
||||
func CreateQueue[T any](size uint64) *Queue[T] {
|
||||
return &Queue[T]{
|
||||
queue: make([]T, 0),
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Queue[T]) Enqueue(el T) error {
|
||||
if q.IsFull() {
|
||||
return fmt.Errorf("queue full")
|
||||
}
|
||||
q.queue = append(q.queue, el)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Queue[T]) Peak() T {
|
||||
return q.queue[0]
|
||||
}
|
||||
|
||||
func (q *Queue[T]) IsEmpty() bool {
|
||||
return len(q.queue) == 0
|
||||
}
|
||||
|
||||
func (q *Queue[T]) IsFull() bool {
|
||||
return q.Length() == q.size
|
||||
}
|
||||
|
||||
func (q *Queue[T]) Length() uint64 {
|
||||
return uint64(len(q.queue))
|
||||
}
|
||||
|
||||
func (q *Queue[T]) Dequeue() T {
|
||||
el := q.queue[0]
|
||||
if q.Length() == 1 {
|
||||
q.queue = make([]T, 0)
|
||||
return el
|
||||
}
|
||||
q.queue = q.queue[1:]
|
||||
return el
|
||||
}
|
||||
|
||||
func (q *Queue[T]) Raw() []T {
|
||||
return q.queue
|
||||
}
|
40
types.go
Normal file
40
types.go
Normal file
@ -0,0 +1,40 @@
|
||||
package laniakea
|
||||
|
||||
type Update struct {
|
||||
UpdateID int `json:"update_id"`
|
||||
Message *Message `json:"message"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
IsBot bool `json:"is_bot"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
LanguageCode string `json:"language_code,omitempty"`
|
||||
IsPremium bool `json:"is_premium,omitempty"`
|
||||
AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"`
|
||||
CanJoinGroups bool `json:"can_join_groups,omitempty"`
|
||||
CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"`
|
||||
SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"`
|
||||
CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"`
|
||||
HasMainWebApp bool `json:"has_main_web_app,omitempty"`
|
||||
}
|
||||
|
||||
type Chat struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
IsForum bool `json:"is_forum,omitempty"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
MessageID int `json:"message_id"`
|
||||
MessageThreadID int `json:"message_thread_id,omitempty"`
|
||||
From *User `json:"from,omitempty"`
|
||||
Chat *Chat `json:"chat,omitempty"`
|
||||
Text string `json:"text"`
|
||||
}
|
27
utils.go
Normal file
27
utils.go
Normal file
@ -0,0 +1,27 @@
|
||||
package laniakea
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func MapToStruct(m map[string]interface{}, s interface{}) error {
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(data, s)
|
||||
return err
|
||||
}
|
||||
|
||||
func MapToJson(m map[string]interface{}) (string, error) {
|
||||
data, err := json.Marshal(m)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func StructToMap(s interface{}) (map[string]interface{}, error) {
|
||||
data, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(data, &m)
|
||||
return m, err
|
||||
}
|
Loading…
Reference in New Issue
Block a user