first commit

This commit is contained in:
ScuroNeko 2024-12-16 00:52:43 +03:00
commit f203e3e056
Signed by: ScuroNeko
GPG Key ID: C9081BDABF0837CA
9 changed files with 595 additions and 0 deletions

195
bot.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}