From 7420047491dd7550165e804ba1552628f76a6274 Mon Sep 17 00:00:00 2001 From: Nefrace Date: Sun, 7 Jul 2024 01:52:38 +0300 Subject: [PATCH] init --- .gitignore | 4 ++ go.mod | 22 +++++++ go.sum | 39 +++++++++++ src/botutils.go | 40 ++++++++++++ src/db.go | 162 ++++++++++++++++++++++++++++++++++++++++++++++ src/handlers.go | 64 ++++++++++++++++++ src/main.go | 40 ++++++++++++ src/middleware.go | 63 ++++++++++++++++++ 8 files changed, 434 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 src/botutils.go create mode 100644 src/db.go create mode 100644 src/handlers.go create mode 100644 src/main.go create mode 100644 src/middleware.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8b17d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +bot.db +.env +.vscode \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9f82f68 --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module nefrace.ru/kickbot.ng + +go 1.22.5 + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fogleman/gg v1.3.0 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-telegram/bot v1.5.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + go.etcd.io/bbolt v1.3.10 // indirect + golang.org/x/image v0.18.0 // indirect + golang.org/x/sys v0.15.0 // indirect + modernc.org/libc v1.37.6 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/sqlite v1.28.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4daf75a --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-telegram/bot v1.5.0 h1:q31yJ8iajFG54b17TgSs/Brl2YkWziRjf4Au5pe3xV0= +github.com/go-telegram/bot v1.5.0/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= +modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= +modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= diff --git a/src/botutils.go b/src/botutils.go new file mode 100644 index 0000000..e4168d8 --- /dev/null +++ b/src/botutils.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "fmt" + + "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" +) + +func GetChatFullName(chat models.Chat) string { + if chat.Title != "" { + return chat.Title + } + if chat.FirstName != "" { + if chat.LastName != "" { + return fmt.Sprint(chat.FirstName, " ", chat.LastName) + } + return chat.FirstName + } + return "" +} + +func GetUserFullName(user models.User) string { + if user.FirstName != "" { + if user.LastName != "" { + return fmt.Sprint(user.FirstName, " ", user.LastName) + } + return user.FirstName + } + return "" +} + +func FetchMemberFromChat(ctx context.Context, b *bot.Bot, chatID int64, userID int64) (*models.ChatMember, error) { + return b.GetChatMember(ctx, &bot.GetChatMemberParams{ChatID: chatID, UserID: userID}) +} + +func IsAdmin(member models.ChatMember) bool { + return member.Administrator != nil || member.Owner != nil +} diff --git a/src/db.go b/src/db.go new file mode 100644 index 0000000..a1b8176 --- /dev/null +++ b/src/db.go @@ -0,0 +1,162 @@ +package main + +import ( + "log" + + "github.com/jmoiron/sqlx" +) + +var schema = ` +create table if not exists chats +( +id INTEGER PRIMARY KEY, +name TEXT, +username TEXT, +topic INTEGER, +active INTEGER +); + +create table if not exists activations +( +id INTEGER PRIMARY KEY AUTOINCREMENT, +code TEXT +); + +create table if not exists messagesToDelete +( +id INTEGER PRIMARY KEY AUTOINCREMENT, +message_id INTEGER, +chat_id INTEGER, +delete_date INTEGER +); + +create table if not exists users +( +id INTEGER PRIMARY KEY, +name TEXT, +username TEXT +); + +create table if not exists bans +( +id INTEGER PRIMARY KEY AUTOINCREMENT, +user_id INTEGER, +reason TEXT, +ban_date INTEGER, +unban_date INTEGER, +FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +create table if not exists captchas +( +id INTEGER PRIMARY KEY AUTOINCREMENT, +user_id INTEGER, +chat_id INTEGER, +message_id INTEGER, +correct_answer INTEGER, +FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +) +` + +type ChatSchema struct { + Id int64 `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Username string `json:"username" db:"username"` + Topic int64 `json:"topic" db:"topic"` + Active bool `json:"active" db:"active"` +} + +type MessageToDelete struct { + Id int64 `db:"id"` + MessageId int `db:"message_id"` + ChatId int64 `db:"chat_id"` + DeleteDate int64 `db:"delete_date"` +} + +type User struct { + Id int64 `db:"id"` + Name string `db:"name"` + Username string `db:"username"` +} + +var db *sqlx.DB + +func InitDb() error { + newdb, err := sqlx.Connect("sqlite", "./bot.db?_time_format=sqlite") + if err != nil { + return err + } + db = newdb + return nil +} + +func NewChat(chat ChatSchema) error { + _, err := db.NamedExec(`insert into chats (id, name, username, topic, active) values (:id, :name, :username, :topic, :active)`, chat) + return err +} + +func IsChatExists(id int64) bool { + var exists bool + err := db.Get(&exists, `SELECT exists(SELECT 1 FROM chats WHERE id = $1);`, id) + if err != nil { + log.Println("Can't check existing of chat", id, err) + return false + } + return exists +} + +func IsChatActive(id int64) bool { + var active bool + err := db.Get(&active, `SELECT active from chats where id = $1`, id) + if err != nil { + return false + } + return active +} + +func GetChatById(id int64) (ChatSchema, error) { + c := ChatSchema{} + err := db.Get(&c, `select * from chats where id = $1`, id) + return c, err +} + +func NewActivation(code string) error { + _, err := db.Exec(`insert into activations (code) values ($1)`, code) + return err +} + +func UseActivation(code string) bool { + _, err := db.Exec(`delete from activations where code = $1`, code) + return err == nil +} + +func ActivateChat(id int64) error { + _, err := db.Exec(`update chats set active = 1 where id = $1`, id) + return err +} + +func AddMessageToDelete(msg MessageToDelete) error { + _, err := db.NamedExec(`insert into messagesToDelete (message_id, chat_id, delete_date) values (:message_id, :chat_id, :delete_date)`, msg) + return err +} + +func NewUser(user User) error { + _, err := db.NamedExec(`insert into users (id, name, username) values (:id, :name, :username)`, user) + return err +} + +func IsUserExists(id int64) bool { + var exists bool + err := db.Get(&exists, `SELECT exists(SELECT 1 FROM users WHERE id = $1);`, id) + if err != nil { + log.Println("Can't check existing of chat", id, err) + return false + } + return exists +} + +func GetUserById(id int64) (User, error) { + c := User{} + err := db.Get(&c, `select * from users where id = $1`, id) + return c, err +} diff --git a/src/handlers.go b/src/handlers.go new file mode 100644 index 0000000..5620c43 --- /dev/null +++ b/src/handlers.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "log" + "strings" + "time" + + "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" +) + +func defaultHandler(ctx context.Context, b *bot.Bot, update *models.Update) { + // b.SendMessage(ctx, &bot.SendMessageParams{ + // ChatID: update.Message.Chat.ID, + // Text: "hello!", + // }) + log.Println(*update) +} + +func registerChat(ctx context.Context, b *bot.Bot, update *models.Update) { + msg := update.Message + log.Println("registering", msg.Chat.ID) + m, err := FetchMemberFromChat(ctx, b, msg.Chat.ID, msg.From.ID) + if err == nil { + if !IsAdmin(*m) { + log.Println("register: user is not admin") + return + } + args := strings.Split(msg.Text, " ") + if len(args) == 1 { + log.Println("register: there's no code") + return + } + if !UseActivation(args[1]) { + log.Println("register: wrong code") + return + } + if err := ActivateChat(msg.Chat.ID); err != nil { + log.Println("Error activating chat: ", err) + return + } + b.DeleteMessage(ctx, &bot.DeleteMessageParams{ChatID: msg.Chat.ID, MessageID: msg.ID}) + sent, err := b.SendMessage(ctx, &bot.SendMessageParams{ChatID: msg.Chat.ID, Text: "Чат зарегистрирован", MessageThreadID: msg.MessageThreadID}) + if err == nil { + err := AddMessageToDelete(MessageToDelete{MessageId: sent.ID, ChatId: msg.Chat.ID, DeleteDate: time.Now().Add(1 * time.Minute).Unix()}) + if err != nil { + log.Println("register: failed to add to delete", err) + } + } + } else { + log.Println("register: error", err) + } +} + +func handleNewJoined(ctx context.Context, b *bot.Bot, u *models.Update) { + for _, user := range u.Message.NewChatMembers { + log.Println(user) + } +} + +func banUser(ctx context.Context, b *bot.Bot, u *models.Update) { + +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..cc00375 --- /dev/null +++ b/src/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "os" + "os/signal" + + _ "github.com/glebarez/go-sqlite" + "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + if err := InitDb(); err != nil { + panic(err) + } + defer db.Close() + + db.MustExec(schema) + + opts := []bot.Option{ + bot.WithMiddlewares(logChats, logUsers, checkRegistered), + bot.WithDefaultHandler(defaultHandler), + } + + b, err := bot.New(os.Getenv("TG_TOKEN"), opts...) + if err != nil { + panic(err) + } + + b.RegisterHandler(bot.HandlerTypeMessageText, "/register", bot.MatchTypePrefix, registerChat) + b.RegisterHandler(bot.HandlerTypeMessageText, "/ban", bot.MatchTypePrefix, banUser) + b.RegisterHandlerMatchFunc(func(update *models.Update) bool { + return update.Message != nil && len(update.Message.NewChatMembers) > 0 + }, handleNewJoined) + b.Start(ctx) +} diff --git a/src/middleware.go b/src/middleware.go new file mode 100644 index 0000000..b39c441 --- /dev/null +++ b/src/middleware.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "log" + "strings" + + "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" +) + +func checkRegistered(next bot.HandlerFunc) bot.HandlerFunc { + return func(ctx context.Context, b *bot.Bot, update *models.Update) { + if update.Message == nil { + next(ctx, b, update) + return + } + chat := update.Message.Chat + if chat.Type == "private" { + return + } + if !IsChatActive(chat.ID) && !strings.HasPrefix(update.Message.Text, "/register") { + log.Println("checkRegistered: not registered", chat.ID) + return + } + + next(ctx, b, update) + } +} + +func logChats(next bot.HandlerFunc) bot.HandlerFunc { + return func(ctx context.Context, b *bot.Bot, update *models.Update) { + if update.Message == nil { + next(ctx, b, update) + return + } + chat := update.Message.Chat + chatName := GetChatFullName(chat) + if !IsChatExists(chat.ID) { + db.MustExec(`insert into chats (id, username, name, topic, active) values ($1, $2, $3, $4, $5);`, chat.ID, chat.Username, chatName, 0, false) + } else { + db.MustExec(`update chats set username = $2, name = $3 where id = $1;`, chat.ID, chat.Username, chatName) + } + next(ctx, b, update) + } +} + +func logUsers(next bot.HandlerFunc) bot.HandlerFunc { + return func(ctx context.Context, b *bot.Bot, update *models.Update) { + if update.Message == nil { + next(ctx, b, update) + return + } + user := update.Message.From + userFullName := GetUserFullName(*user) + if !IsUserExists(user.ID) { + db.MustExec(`insert into users (id, username, name) values ($1, $2, $3);`, user.ID, user.Username, userFullName, 0, false) + } else { + db.MustExec(`update users set username = $2, name = $3 where id = $1;`, user.ID, user.Username, userFullName) + } + next(ctx, b, update) + } +}