├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── .gitignore ├── .gitpod.yml ├── renovate.json ├── .dockerignore ├── .env.example ├── README.md ├── models ├── user_captcha.go ├── user_report.go ├── user.go └── report.go ├── config └── config.go ├── .vscode └── settings.json ├── cas_banned_test.go ├── capcay.go ├── cas_banned.go ├── go.mod ├── callbacks ├── callbacks.go └── report.go ├── commands ├── ping.go ├── rules.go ├── admin.go ├── commands.go └── report.go ├── fly.toml ├── go.sum └── main.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://bagidu.id/sucipto'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .env 3 | .goreload 4 | miranda-bot 5 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: go mod download 3 | command: go build -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from .gitignore 2 | **/vendor 3 | **/.env 4 | **/.goreload 5 | **/miranda-bot 6 | fly.toml 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8888 2 | TOKEN=xxxxxxxxxxxxxxxxxx 3 | WEBHOOK_URL=https:/your-app.herokuapp.com/webhook 4 | DATABASE_URL=postgres://postgres:postgres@localhost:5432/miranda?sslmode=disable 5 | GROUP_ID=-234324324324 6 | BOT_USERNAME=BGLIbot 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Miranda Salma Bot 2 | This is **Miranda Salma** Telegram Bot rewriten in Go Language, Previously written in [Node.js using Telegraf.js](https://github.com/bgli/bglibot-js) framework. This bot intended to help manage Telegram Group. 3 | 4 | 5 | -------------------------------------------------------------------------------- /models/user_captcha.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/jinzhu/gorm" 4 | 5 | // UserCaptcha model 6 | type UserCaptcha struct { 7 | gorm.Model 8 | 9 | UserID int64 `gorm:"index"` 10 | Code string `gorm:"size:5"` 11 | MessageID int 12 | } 13 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Configuration ... 4 | type Configuration struct { 5 | Port string 6 | UpdateMode string 7 | Token string 8 | WebhookURL string 9 | DBUrl string 10 | GroupID int64 11 | BotUsername string 12 | } 13 | -------------------------------------------------------------------------------- /models/user_report.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/jinzhu/gorm" 5 | ) 6 | 7 | // UserReport struct 8 | type UserReport struct { 9 | gorm.Model 10 | 11 | User *User 12 | UserID int 13 | Report *Report 14 | ReportID int 15 | Vote int // 1 Vote Up; 0 Vote Down 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sqltools.connections": [ 3 | { 4 | "previewLimit": 50, 5 | "server": "localhost", 6 | "port": 5432, 7 | "driver": "PostgreSQL", 8 | "name": "local", 9 | "database": "postgres", 10 | "username": "postgres", 11 | "password": "postgres" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /cas_banned_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | // User chip 6 | func TestUserIsNotBanned(t *testing.T) { 7 | if checkBanned(1051416075) { 8 | t.Error("User should not banned") 9 | } 10 | } 11 | 12 | // https://cas.chat/query?u=1089155882 13 | func TestUserIsCasBanned(t *testing.T) { 14 | if !checkBanned(1089155882) { 15 | t.Error("User should banned") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/jinzhu/gorm" 4 | 5 | // User model 6 | type User struct { 7 | gorm.Model 8 | 9 | TelegramID int64 `gorm:"unique_index"` 10 | Name string `gorm:"size:255"` 11 | Username string `gorm:"size:255;unique_index"` 12 | Point int `gorm:"default:'10'"` 13 | RoleID int `gorm:"default:'3'"` // 1 Admin 2 Moderator 3 Member 14 | } 15 | -------------------------------------------------------------------------------- /capcay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | const charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 9 | 10 | var seededRand = rand.New( 11 | rand.NewSource(time.Now().UnixNano())) 12 | 13 | func randomStr(length int) string { 14 | b := make([]byte, length) 15 | for i := range b { 16 | b[i] = charset[seededRand.Intn(len(charset))] 17 | } 18 | return string(b) 19 | } 20 | -------------------------------------------------------------------------------- /models/report.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/jinzhu/gorm" 4 | 5 | // Report models 6 | type Report struct { 7 | gorm.Model 8 | 9 | MessageID int `gorm:"unique_index"` // Same message can't be reported more than once 10 | ReporterID int64 11 | VoteUp int `gorm:"default:'0'"` 12 | VoteDown int `gorm:"default:'0'"` 13 | // Users []User `gorm:"many2many:report_users"` 14 | UserReports []*UserReport 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: go mod download 27 | 28 | - name: Build 29 | run: go build -v . 30 | 31 | 32 | -------------------------------------------------------------------------------- /cas_banned.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | func checkBanned(id int64) bool { 12 | resp, err := http.Get(fmt.Sprintf("https://api.cas.chat/check?user_id=%d", id)) 13 | if err != nil { 14 | log.Printf("[cas] unable check user status on CAS banned, status: %d", resp.StatusCode) 15 | return false 16 | } 17 | 18 | defer resp.Body.Close() 19 | bytes, _ := io.ReadAll(resp.Body) 20 | 21 | var cas struct { 22 | OK bool `json:"ok"` 23 | } 24 | 25 | json.Unmarshal(bytes, &cas) 26 | log.Printf("[cas] result from CAS API %v", cas.OK) 27 | return cas.OK 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module miranda-bot 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/getsentry/sentry-go v0.16.0 7 | github.com/go-chi/chi/v5 v5.0.8 8 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 9 | github.com/jinzhu/gorm v1.9.16 10 | github.com/joho/godotenv v1.4.0 11 | ) 12 | 13 | require ( 14 | github.com/denisenkom/go-mssqldb v0.12.2 // indirect 15 | github.com/jinzhu/inflection v1.0.0 // indirect 16 | github.com/jinzhu/now v1.1.5 // indirect 17 | github.com/lib/pq v1.10.7 // indirect 18 | github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect 19 | golang.org/x/sys v0.3.0 // indirect 20 | golang.org/x/text v0.5.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /callbacks/callbacks.go: -------------------------------------------------------------------------------- 1 | package callbacks 2 | 3 | import ( 4 | "log" 5 | "miranda-bot/config" 6 | 7 | "github.com/getsentry/sentry-go" 8 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 9 | "github.com/jinzhu/gorm" 10 | ) 11 | 12 | // Callback handle callback query 13 | type Callback struct { 14 | Bot *tg.BotAPI 15 | DB *gorm.DB 16 | CallbackQuery *tg.CallbackQuery 17 | Config *config.Configuration 18 | } 19 | 20 | // Handle handle callback base on mode 21 | func (cb *Callback) Handle(mode string) { 22 | log.Printf("[callback] handle %s", mode) 23 | 24 | defer sentry.Recover() 25 | 26 | switch mode { 27 | case "report": 28 | cb.Report() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /commands/ping.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 8 | ) 9 | 10 | // Ping send pong 11 | func (c Command) Ping() { 12 | log.Println("[command] Call ping!") 13 | 14 | msg := tg.NewMessage(c.Message.Chat.ID, "Pong ✨") 15 | msg.ParseMode = "markdown" 16 | 17 | r, err := c.Bot.Send(msg) 18 | 19 | if err != nil { 20 | log.Println(err) 21 | 22 | return 23 | } 24 | 25 | // Delete !ping 26 | ping := tg.DeleteMessageConfig{ 27 | ChatID: c.Message.Chat.ID, 28 | MessageID: c.Message.MessageID, 29 | } 30 | c.Bot.Request(ping) 31 | 32 | go func() { 33 | log.Printf("Deleting message %d in 3 seconds...", r.Chat.ID) 34 | time.Sleep(3 * time.Second) 35 | 36 | // Delete Pong after a few second 37 | pong := tg.DeleteMessageConfig{ 38 | ChatID: r.Chat.ID, 39 | MessageID: r.MessageID, 40 | } 41 | c.Bot.Request(pong) 42 | }() 43 | } 44 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for tux on 2022-12-15T05:29:35Z 2 | 3 | app = "tux" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [build] 9 | builder = "paketobuildpacks/builder:base" 10 | buildpacks = ["gcr.io/paketo-buildpacks/go"] 11 | 12 | [env] 13 | PORT = "8080" 14 | 15 | [experimental] 16 | allowed_public_ports = [] 17 | auto_rollback = true 18 | 19 | [[services]] 20 | http_checks = [] 21 | internal_port = 8080 22 | processes = ["app"] 23 | protocol = "tcp" 24 | script_checks = [] 25 | [services.concurrency] 26 | hard_limit = 25 27 | soft_limit = 20 28 | type = "connections" 29 | 30 | [[services.ports]] 31 | force_https = true 32 | handlers = ["http"] 33 | port = 80 34 | 35 | [[services.ports]] 36 | handlers = ["tls", "http"] 37 | port = 443 38 | 39 | [[services.tcp_checks]] 40 | grace_period = "1s" 41 | interval = "15s" 42 | restart_limit = 0 43 | timeout = "2s" 44 | -------------------------------------------------------------------------------- /commands/rules.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 8 | ) 9 | 10 | // Rules send rules 11 | func (c Command) Rules() { 12 | msg := tg.NewMessage(c.Message.Chat.ID, "Peraturan\n\nBaca: Peraturan Grup BGLI") 13 | msg.ParseMode = "HTML" 14 | msg.ReplyToMessageID = c.Message.MessageID 15 | 16 | r, err := c.Bot.Send(msg) 17 | 18 | if err != nil { 19 | log.Println(err) 20 | 21 | return 22 | } 23 | 24 | go func() { 25 | log.Printf("Deleting message %d in 10 seconds...", r.Chat.ID) 26 | time.Sleep(10 * time.Second) 27 | 28 | // Delete !rules 29 | rules := tg.DeleteMessageConfig{ 30 | ChatID: c.Message.Chat.ID, 31 | MessageID: c.Message.MessageID, 32 | } 33 | c.Bot.Request(rules) 34 | 35 | // Delete Rules after a few second 36 | reply := tg.DeleteMessageConfig{ 37 | ChatID: r.Chat.ID, 38 | MessageID: r.MessageID, 39 | } 40 | c.Bot.Request(reply) 41 | }() 42 | 43 | } 44 | -------------------------------------------------------------------------------- /commands/admin.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "miranda-bot/models" 7 | 8 | "github.com/getsentry/sentry-go" 9 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 10 | "github.com/jinzhu/gorm" 11 | ) 12 | 13 | // AdminList ... 14 | func (c Command) AdminList() { 15 | var users []models.User 16 | 17 | if err := c.DB.Where("role_id IN (?)", []int{1, 2}).Order("point desc").Find(&users).Error; err != nil { 18 | if !gorm.IsRecordNotFoundError(err) { 19 | log.Printf("[admin] error queryng db: %s", err.Error()) 20 | sentry.CaptureException(err) 21 | } 22 | } 23 | 24 | var msg string 25 | 26 | msg = "*Daftar admin dan moderator:*\n" 27 | for i, user := range users { 28 | var role string 29 | switch user.RoleID { 30 | case 1: 31 | role = "Admin" 32 | case 2: 33 | role = "Moderator" 34 | } 35 | l := fmt.Sprintf("%v. %s \n🔰 %s 💰 %v\n", i+1, user.Name, role, user.Point) 36 | 37 | msg += l 38 | } 39 | 40 | m := tg.NewMessage(c.Message.Chat.ID, msg) 41 | m.ReplyToMessageID = c.Message.MessageID 42 | m.ParseMode = "markdown" 43 | 44 | _, err := c.Bot.Send(m) 45 | 46 | if err != nil { 47 | sentry.CaptureException(err) 48 | } 49 | 50 | // log.Printf("Users: \n%s", msg) 51 | } 52 | -------------------------------------------------------------------------------- /commands/commands.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "log" 5 | "miranda-bot/config" 6 | 7 | "github.com/getsentry/sentry-go" 8 | 9 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 10 | "github.com/jinzhu/gorm" 11 | ) 12 | 13 | // Command ... 14 | type Command struct { 15 | Bot *tg.BotAPI 16 | Message *tg.Message 17 | DB *gorm.DB 18 | Config *config.Configuration 19 | } 20 | 21 | // Setup ... 22 | func (c *Command) Setup(b *tg.BotAPI, m *tg.Message) { 23 | c.Bot = b 24 | c.Message = m 25 | } 26 | 27 | // Handle command 28 | func (c *Command) Handle(cs string) { 29 | 30 | defer sentry.Recover() 31 | 32 | switch cs { 33 | case "ping", "p": 34 | c.Ping() 35 | case "report", "r", "spam": 36 | if c.IsFromGroup() { 37 | c.Report() 38 | } else { 39 | log.Println("[report] unable call command from outside group") 40 | } 41 | 42 | case "rules": 43 | if c.IsFromGroup() { 44 | c.Rules() 45 | } else { 46 | log.Println("[report] unable call command from outside group") 47 | } 48 | case "adm", "admin": 49 | c.AdminList() 50 | } 51 | 52 | } 53 | 54 | // IsFromGroup ... 55 | func (c Command) IsFromGroup() bool { 56 | message := c.Message 57 | 58 | if message.Chat.ID == c.Config.GroupID { 59 | return true 60 | } 61 | 62 | return false 63 | } 64 | -------------------------------------------------------------------------------- /commands/report.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "miranda-bot/models" 7 | 8 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 9 | ) 10 | 11 | // Report ... 12 | func (c Command) Report() { 13 | 14 | if c.Message.ReplyToMessage != nil { 15 | 16 | // Check user reporter 17 | var reporter models.User 18 | if err := c.DB.Where("telegram_id = ?", c.Message.From.ID).First(&reporter).Error; err != nil { 19 | 20 | log.Printf("Create user reporter: %s", c.Message.From.UserName) 21 | log.Println(err) 22 | 23 | // Create user reporter to db if not exists 24 | reporter = models.User{ 25 | TelegramID: c.Message.From.ID, 26 | Name: fmt.Sprintf("%s %s", c.Message.From.FirstName, c.Message.From.LastName), 27 | Username: c.Message.From.UserName, 28 | } 29 | 30 | c.DB.Create(&reporter) 31 | } else { 32 | log.Printf("[Report] User reporter (%s) already exists with point: %v", reporter.Name, reporter.Point) 33 | } 34 | 35 | // Create Report Record 36 | var report models.Report 37 | if err := c.DB.Where("message_id = ?", c.Message.ReplyToMessage.MessageID).First(&report).Error; err != nil { 38 | report = models.Report{ 39 | MessageID: c.Message.ReplyToMessage.MessageID, 40 | ReporterID: reporter.TelegramID, 41 | } 42 | 43 | c.DB.Create(&report) 44 | } else { 45 | log.Printf("Pesan sudah pernah dilaporkan #%v", report.ID) 46 | // Message already reported 47 | nm := tg.NewMessage(c.Message.Chat.ID, fmt.Sprintf("Pesan sudah pernah dilaporkan dengan ID #%v", report.ID)) 48 | nm.ReplyToMessageID = report.MessageID 49 | c.Bot.Send(nm) 50 | 51 | // Delete !report command 52 | dr := tg.NewDeleteMessage(c.Message.Chat.ID, c.Message.MessageID) 53 | if _, err := c.Bot.Send(dr); err != nil { 54 | log.Println("[report] Error delete report message") 55 | } 56 | 57 | return 58 | } 59 | 60 | // Voting Message Inline Keyboard 61 | cbUp := fmt.Sprintf("report:%v:up", report.MessageID) 62 | cbDown := fmt.Sprintf("report:%v:down", report.MessageID) 63 | keyboard := tg.InlineKeyboardMarkup{ 64 | InlineKeyboard: [][]tg.InlineKeyboardButton{ 65 | { 66 | tg.InlineKeyboardButton{Text: "👍", CallbackData: &cbUp}, 67 | tg.InlineKeyboardButton{Text: "👎", CallbackData: &cbDown}, 68 | }, 69 | }, 70 | } 71 | msg := fmt.Sprintf( 72 | "💢 Apakah ini pesan Spam? \nBantu vote untuk menghapus pesan ini.\n\nReporter: %s (@%s)\nReport ID: #%v", 73 | c.Message.From.FirstName, 74 | c.Message.From.UserName, 75 | report.ID, 76 | ) 77 | ma := tg.NewMessage(c.Message.Chat.ID, msg) 78 | ma.ReplyToMessageID = c.Message.ReplyToMessage.MessageID 79 | ma.ParseMode = "html" 80 | ma.ReplyMarkup = keyboard 81 | 82 | _, err := c.Bot.Send(ma) 83 | if err != nil { 84 | log.Println("Error send message", err) 85 | } 86 | 87 | // Delete !report command 88 | dr := tg.NewDeleteMessage(c.Message.Chat.ID, c.Message.MessageID) 89 | if _, err := c.Bot.Send(dr); err != nil { 90 | log.Println("[report] Error delete report message") 91 | } 92 | 93 | } else { 94 | msg := tg.NewMessage(c.Message.Chat.ID, "Pesan mana yang mau dilaporkan? 😕") 95 | msg.ParseMode = "markdown" 96 | msg.ReplyToMessageID = c.Message.MessageID 97 | 98 | c.Bot.Send(msg) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= 2 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= 3 | github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= 4 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 5 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 9 | github.com/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= 10 | github.com/denisenkom/go-mssqldb v0.12.2/go.mod h1:lnIw1mZukFRZDJYQ0Pb833QS2IaC3l5HkEfra2LJ+sk= 11 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 12 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 13 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 14 | github.com/getsentry/sentry-go v0.16.0 h1:owk+S+5XcgJLlGR/3+3s6N4d+uKwqYvh/eS0AIMjPWo= 15 | github.com/getsentry/sentry-go v0.16.0/go.mod h1:ZXCloQLj0pG7mja5NK6NPf2V4A88YJ4pNlc2mOHwh6Y= 16 | github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= 17 | github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 18 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 19 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 20 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 21 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 22 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 23 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 24 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 25 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 26 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 27 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 28 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 29 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 30 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 31 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 32 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 33 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 34 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 35 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 36 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 37 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 38 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 39 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 40 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 41 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 42 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 43 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 44 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 45 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 46 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 47 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 49 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 50 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 51 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 52 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 53 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 54 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= 55 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 56 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 57 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 58 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 59 | golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 60 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 61 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 66 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 68 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 69 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 70 | golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= 71 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 72 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 75 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 78 | -------------------------------------------------------------------------------- /callbacks/report.go: -------------------------------------------------------------------------------- 1 | package callbacks 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "miranda-bot/models" 7 | "strings" 8 | 9 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 10 | ) 11 | 12 | // Report .... 13 | func (cb *Callback) Report() { 14 | cq := cb.CallbackQuery 15 | data := cq.Data 16 | datas := strings.Split(data, ":") 17 | 18 | // If reported message already deleted, delete report message 19 | if cq.Message.ReplyToMessage == nil { 20 | vm := tg.NewDeleteMessage(cq.Message.Chat.ID, cq.Message.MessageID) 21 | if _, err := cb.Bot.Send(vm); err != nil { 22 | log.Println("[report] Error delete vote message", err) 23 | } else { 24 | log.Println("[report] Vote message deleted!") 25 | } 26 | 27 | return 28 | } 29 | 30 | msgID := datas[1] 31 | 32 | log.Printf( 33 | "User %s vote %s for message %s", 34 | cq.From.FirstName, 35 | datas[2], 36 | msgID, 37 | ) 38 | 39 | // Search User or Create New 40 | var voter models.User 41 | if err := cb.DB.Where("telegram_id = ?", cq.From.ID).First(&voter).Error; err != nil { 42 | 43 | log.Printf("Create user voter: %s", cq.From.UserName) 44 | log.Println(err) 45 | 46 | // Create user voter to db if not exists 47 | voter = models.User{ 48 | TelegramID: cq.From.ID, 49 | Name: fmt.Sprintf("%s %s", cq.From.FirstName, cq.From.LastName), 50 | Username: cq.From.UserName, 51 | } 52 | 53 | cb.DB.Create(&voter) 54 | } else { 55 | log.Printf("[Vote Report] User voter (%s) already exists with point: %v", voter.Name, voter.Point) 56 | } 57 | 58 | // Voting Points / Reputation 59 | var votingPoint = 1 60 | 61 | // Admin & Mod has instant delete privileges 62 | if voter.RoleID != 3 { 63 | votingPoint = 3 64 | } else if voter.Point >= 100 { 65 | votingPoint = 3 66 | } else if voter.Point >= 50 { 67 | votingPoint = 2 68 | } 69 | 70 | tx := cb.DB.Begin() 71 | 72 | // Search Report 73 | var report models.Report 74 | if err := tx.Where("message_id = ?", msgID).Set("gorm:query_option", "FOR UPDATE").First(&report).Error; err != nil { 75 | log.Println("[vote] Report data not found") 76 | tx.Rollback() 77 | 78 | cb.Bot.Request(tg.NewCallback(cq.ID, "Data report tidak ditemukan")) 79 | return 80 | } 81 | 82 | // Check Existing Vote for curent voter 83 | var voteValue int 84 | var voteState string 85 | 86 | switch datas[2] { 87 | case "up": 88 | voteValue = 1 89 | voteState = "👍" 90 | case "down": 91 | voteValue = 0 92 | voteState = "👎" 93 | } 94 | 95 | var ur models.UserReport 96 | if tx.Where("user_id = ? and report_id = ?", voter.ID, report.ID).First(&ur).RecordNotFound() { 97 | 98 | // Save Vote Record 99 | report.UserReports = []*models.UserReport{ 100 | { 101 | User: &voter, 102 | Vote: voteValue, 103 | }, 104 | } 105 | 106 | // Update Vote Count 107 | switch datas[2] { 108 | case "up": 109 | report.VoteUp = report.VoteUp + votingPoint 110 | case "down": 111 | report.VoteDown = report.VoteDown + votingPoint 112 | } 113 | 114 | cb.Bot.Request(tg.NewCallback(cq.ID, fmt.Sprintf("Kamu telah memberikan %s untuk pooling ini", voteState))) 115 | 116 | } else { 117 | // TODO: Update Vote if changed 118 | var existingVote string 119 | switch ur.Vote { 120 | case 1: 121 | existingVote = "👍" 122 | if voteValue == 0 { 123 | report.VoteUp = report.VoteUp - votingPoint 124 | report.VoteDown = report.VoteDown + votingPoint 125 | } 126 | case 0: 127 | existingVote = "👎" 128 | if voteValue == 1 { 129 | report.VoteDown = report.VoteDown - votingPoint 130 | report.VoteUp = report.VoteUp + votingPoint 131 | } 132 | } 133 | 134 | // Change Vote Count 135 | if ur.Vote != voteValue { 136 | cb.Bot.Request(tg.NewCallback(cq.ID, fmt.Sprintf("Kamu merubah vote dari %s menjadi %s untuk pooling ini", existingVote, voteState))) 137 | } else { 138 | cb.Bot.Request(tg.NewCallback(cq.ID, fmt.Sprintf("Kamu sudah memberi vote %s untuk pooling ini", existingVote))) 139 | } 140 | 141 | // Update existing vote 142 | ur.Vote = voteValue 143 | tx.Save(&ur) 144 | 145 | // return 146 | } 147 | 148 | tx.Save(&report) 149 | 150 | tx.Commit() 151 | 152 | // New Keyboard 153 | cbUp := fmt.Sprintf("report:%v:up", report.MessageID) 154 | cbDown := fmt.Sprintf("report:%v:down", report.MessageID) 155 | keyboard := tg.InlineKeyboardMarkup{ 156 | InlineKeyboard: [][]tg.InlineKeyboardButton{ 157 | { 158 | tg.InlineKeyboardButton{Text: fmt.Sprintf("%v 👍", report.VoteUp), CallbackData: &cbUp}, 159 | tg.InlineKeyboardButton{Text: fmt.Sprintf("%v 👎", report.VoteDown), CallbackData: &cbDown}, 160 | }, 161 | }, 162 | } 163 | // Update Keyboard 164 | edit := tg.NewEditMessageReplyMarkup( 165 | cq.Message.Chat.ID, 166 | cq.Message.MessageID, 167 | keyboard, 168 | ) 169 | 170 | cb.Bot.Send(edit) 171 | 172 | // Process Vote 173 | dtx := cb.DB.Begin() 174 | if report.VoteUp >= 3 && report.VoteDown < report.VoteUp { 175 | 176 | log.Println("Vote up >= 3, dan votedown lebih sedikit saatnya hapus pesan...") 177 | 178 | // Delete Reported Message 179 | rm := tg.NewDeleteMessage(cq.Message.ReplyToMessage.Chat.ID, cq.Message.ReplyToMessage.MessageID) 180 | 181 | if _, err := cb.Bot.Send(rm); err != nil { 182 | log.Println("[report] Error delete reported message", err) 183 | } else { 184 | log.Println("[report] Reported message deleted!") 185 | } 186 | 187 | // Delete Vote 188 | vm := tg.NewDeleteMessage(cq.Message.Chat.ID, cq.Message.MessageID) 189 | if _, err := cb.Bot.Send(vm); err != nil { 190 | log.Println("[report] Error delete vote message", err) 191 | } else { 192 | log.Println("[report] Vote message deleted!") 193 | } 194 | 195 | // Reducer Reporter Point 196 | var reporter models.User 197 | dtx.Set("gorm:query_option", "FOR UPDATE").Where("telegram_id = ?", report.ReporterID).First(&reporter) 198 | 199 | reporter.Point = reporter.Point + 3 200 | dtx.Save(&reporter) 201 | 202 | // Update Point Voter 203 | var votes = []models.UserReport{} 204 | dtx.Set("gorm:query_option", "FOR UPDATE").Where("report_id = ?", report.ID).Where("vote = ?", 1).Preload("User").Find(&votes) 205 | 206 | for _, ur := range votes { 207 | u := ur.User 208 | log.Printf("[vote] User %s point %v + 1", u.Name, u.Point) 209 | u.Point = u.Point + 1 210 | dtx.Save(&u) 211 | } 212 | 213 | // Delete Record 214 | dtx.Unscoped().Delete(&report) 215 | dtx.Unscoped().Where("report_id = ? ", report.ID).Delete(models.UserReport{}) 216 | 217 | } else if report.VoteDown >= 3 && report.VoteUp < report.VoteDown { 218 | log.Println("Vote down >= 3, dan voteup lebih sedikit, saatnya punish reporter") 219 | 220 | // Delete Vote 221 | vm := tg.NewDeleteMessage(cq.Message.Chat.ID, cq.Message.MessageID) 222 | if _, err := cb.Bot.Send(vm); err != nil { 223 | log.Println("[report] Error delete vote message", err) 224 | } else { 225 | log.Println("[report] Vote message deleted!") 226 | } 227 | 228 | // Reducer Reporter Point 229 | var reporter models.User 230 | dtx.Set("gorm:query_option", "FOR UPDATE").Where("telegram_id = ?", report.ReporterID).First(&reporter) 231 | 232 | reporter.Point = reporter.Point - 3 233 | dtx.Save(&reporter) 234 | 235 | // Update Point Voter 236 | var votes = []models.UserReport{} 237 | dtx.Set("gorm:query_option", "FOR UPDATE").Where("report_id = ?", report.ID).Where("vote = ?", 0).Preload("User").Find(&votes) 238 | 239 | for _, ur := range votes { 240 | u := ur.User 241 | log.Printf("[vote] User %s point %v + 1", u.Name, u.Point) 242 | u.Point = u.Point + 1 243 | dtx.Save(&u) 244 | } 245 | 246 | // Delete Record 247 | dtx.Unscoped().Delete(&report) 248 | dtx.Unscoped().Where("report_id = ? ", report.ID).Delete(models.UserReport{}) 249 | } 250 | dtx.Commit() 251 | 252 | } 253 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "miranda-bot/callbacks" 9 | "miranda-bot/config" 10 | "net/http" 11 | "os" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | "miranda-bot/commands" 17 | "miranda-bot/models" 18 | 19 | "github.com/getsentry/sentry-go" 20 | "github.com/go-chi/chi/v5" 21 | "github.com/go-chi/chi/v5/middleware" 22 | "github.com/jinzhu/gorm" 23 | _ "github.com/jinzhu/gorm/dialects/postgres" 24 | "github.com/joho/godotenv" 25 | 26 | tg "github.com/go-telegram-bot-api/telegram-bot-api/v5" 27 | ) 28 | 29 | // App main app struct 30 | type App struct { 31 | DB *gorm.DB 32 | Bot *tg.BotAPI 33 | Config *config.Configuration 34 | } 35 | 36 | func main() { 37 | // Load Configuration 38 | err := godotenv.Load() 39 | if err != nil { 40 | log.Println("No .env file, reading from system env") 41 | // panic(err) 42 | } 43 | 44 | // Init Sentry 45 | serr := sentry.Init(sentry.ClientOptions{ 46 | Dsn: "https://f2128fc9c33d4bfea0b33e220166a89e:e8ac6687004a476886ace7e3dcf0dd8e@sentry.io/1419349", 47 | }) 48 | defer sentry.Flush(2 * time.Second) 49 | 50 | if serr != nil { 51 | log.Println("Error initialize sentry") 52 | } 53 | 54 | // Init Configuration 55 | groupID, _ := strconv.ParseInt(os.Getenv("GROUP_ID"), 10, 64) 56 | config := &config.Configuration{ 57 | Port: os.Getenv("PORT"), 58 | Token: os.Getenv("TOKEN"), 59 | WebhookURL: os.Getenv("WEBHOOK_URL"), 60 | DBUrl: os.Getenv("DATABASE_URL"), 61 | GroupID: groupID, 62 | BotUsername: os.Getenv("BOT_USERNAME"), 63 | } 64 | 65 | bot, err := tg.NewBotAPI(config.Token) 66 | 67 | if err != nil { 68 | log.Panic(err) 69 | } 70 | 71 | // Init Database 72 | db, err := gorm.Open("postgres", config.DBUrl) 73 | if err != nil { 74 | log.Panic("Unable connect to database", err) 75 | } 76 | 77 | // Limit open connection 78 | db.DB().SetMaxOpenConns(10) 79 | db.DB().SetMaxIdleConns(2) 80 | 81 | defer db.Close() 82 | 83 | log.Println("Connected to DB") 84 | log.Printf("@%s working on group %v", config.BotUsername, config.GroupID) 85 | db.AutoMigrate( 86 | &models.User{}, 87 | &models.Report{}, 88 | &models.UserReport{}, 89 | &models.UserCaptcha{}, 90 | ) 91 | 92 | app := App{ 93 | DB: db, 94 | Config: config, 95 | Bot: bot, 96 | } 97 | 98 | bot.Debug = false 99 | log.Printf("@%s is wake up.. :)", bot.Self.UserName) 100 | 101 | // Using Webhook 102 | 103 | r := chi.NewRouter() 104 | 105 | // r.Use(middleware.Logger) 106 | r.Use(middleware.Recoverer) 107 | 108 | r.Post("/webhook", func(w http.ResponseWriter, r *http.Request) { 109 | bytes, _ := io.ReadAll(r.Body) 110 | 111 | var update tg.Update 112 | if err := json.Unmarshal(bytes, &update); err != nil { 113 | sentry.CaptureException(err) 114 | log.Println("[parse] Error parsing updates") 115 | } 116 | 117 | go app.handle(update) 118 | }) 119 | 120 | log.Println("Set mode webhook to", config.WebhookURL) 121 | if wh, err := tg.NewWebhook(config.WebhookURL); err != nil { 122 | sentry.CaptureException(err) 123 | 124 | log.Fatal("error") 125 | } else { 126 | _, err := bot.Request(wh) 127 | if err != nil { 128 | sentry.CaptureException(err) 129 | log.Fatal("Error setting webhook URL") 130 | } 131 | } 132 | 133 | info, err := bot.GetWebhookInfo() 134 | if err != nil { 135 | log.Fatal("Error getting webhook info", err) 136 | sentry.CaptureException(err) 137 | } 138 | 139 | if info.LastErrorDate != 0 { 140 | log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) 141 | } 142 | 143 | log.Println("Running on port:", config.Port) 144 | err = http.ListenAndServe(":"+config.Port, r) 145 | if err != nil { 146 | log.Fatal("ListenAndServe: ", err) 147 | } 148 | } 149 | 150 | func (app *App) handle(update tg.Update) { 151 | bot := app.Bot 152 | 153 | if update.CallbackQuery != nil { 154 | 155 | log.Println("[callback] handle callback") 156 | cb := callbacks.Callback{ 157 | Bot: bot, 158 | CallbackQuery: update.CallbackQuery, 159 | DB: app.DB, 160 | Config: app.Config, 161 | } 162 | 163 | cq := update.CallbackQuery.Data 164 | 165 | data := strings.Split(cq, ":") 166 | 167 | cb.Handle(data[0]) 168 | 169 | return 170 | } else if update.Message == nil { 171 | log.Println("[update] tidak ada message di update") 172 | return 173 | } 174 | 175 | log.Printf("[%s:%s] %s", update.Message.From.UserName, update.Message.Chat.Title, update.Message.Text) 176 | 177 | // Captcha Middleware 178 | if update.Message != nil { 179 | uid := update.Message.From.ID 180 | tx := app.DB.Begin() 181 | captchaExists := true 182 | 183 | var captcha models.UserCaptcha 184 | 185 | // Check on DB 186 | if err := tx.Where("user_id = ?", uid).First(&captcha).Error; err != nil { 187 | if !gorm.IsRecordNotFoundError(err) { 188 | log.Println("[captcha] error query code on DB") 189 | sentry.CaptureException(err) 190 | } else { 191 | // No Captcha 192 | captchaExists = false 193 | } 194 | } 195 | 196 | // If captcha match, delete from record 197 | if captchaExists { 198 | m := update.Message.Text 199 | 200 | // User have unresolved captcha, and send captcha code 201 | if len(m) == 5 && captcha.Code == m { 202 | // Delete captcha from DB 203 | if err := tx.Unscoped().Delete(&captcha).Error; err != nil { 204 | log.Println("[captcha] error remove captcha code from DB") 205 | sentry.CaptureException(err) 206 | tx.Rollback() 207 | return 208 | } 209 | 210 | // Verified Message 211 | text := fmt.Sprintf( 212 | "Verifikasi berhasil [%s](tg://user?id=%d) 👍\nSekarang kamu bisa mengirim pesan 🤗", 213 | update.Message.From.FirstName, 214 | update.Message.From.ID, 215 | ) 216 | msg := tg.NewMessage(update.Message.Chat.ID, text) 217 | msg.ParseMode = "markdown" 218 | 219 | log.Printf("[captcha:%d] Captcha resolved", update.Message.From.ID) 220 | 221 | r, err := bot.Send(msg) 222 | if err != nil { 223 | sentry.CaptureException(err) 224 | log.Printf("[captcha:%d] unable to send verified message", update.Message.From.ID) 225 | tx.Rollback() 226 | return 227 | } 228 | 229 | // Delete code 230 | if _, err := bot.Request(tg.DeleteMessageConfig{ 231 | ChatID: update.Message.Chat.ID, 232 | MessageID: update.Message.MessageID, 233 | }); err != nil { 234 | log.Println("[captcha] Error delete code message") 235 | sentry.CaptureException(err) 236 | } 237 | 238 | // Delete Welcome 239 | if captcha.MessageID > 0 { 240 | if _, err := bot.Request(tg.DeleteMessageConfig{ 241 | ChatID: update.Message.Chat.ID, 242 | MessageID: captcha.MessageID, 243 | }); err != nil { 244 | log.Println("[captcha] Error delete welcome message") 245 | sentry.CaptureException(err) 246 | } 247 | } 248 | 249 | // Delete verified message after 3sec 250 | go func() { 251 | log.Printf("[captcha] Deleting message %d in 3 seconds...", r.Chat.ID) 252 | time.Sleep(3 * time.Second) 253 | 254 | // Delete Pong after a few second 255 | pong := tg.DeleteMessageConfig{ 256 | ChatID: r.Chat.ID, 257 | MessageID: r.MessageID, 258 | } 259 | bot.Request(pong) 260 | }() 261 | } else { 262 | // If it has captcha & message not match with code, delete message 263 | vm := tg.NewDeleteMessage(update.Message.Chat.ID, update.Message.MessageID) 264 | if _, err := app.Bot.Send(vm); err != nil { 265 | log.Println("[captcha] Error delete unverified user message", err) 266 | } else { 267 | log.Printf("[captcha:%d] Message deleted from unverified user!", update.Message.From.ID) 268 | } 269 | 270 | tx.Commit() 271 | return 272 | } 273 | } 274 | tx.Commit() 275 | } 276 | 277 | // Channel Filter 278 | if update.Message.From.IsBot { 279 | // Delete message 280 | if _, err := bot.Request(tg.NewDeleteMessage(update.Message.Chat.ID, update.Message.MessageID)); err != nil { 281 | sentry.CaptureException(err) 282 | log.Println("[cleanup] unable delete bot message") 283 | } 284 | 285 | // Warn user to use main account 286 | if update.Message.SenderChat != nil && update.Message.SenderChat.Type == "channel" { 287 | log.Println("[channel] new message from channel") 288 | member := update.Message.SenderChat 289 | 290 | msg := tg.NewMessage( 291 | update.Message.Chat.ID, 292 | fmt.Sprintf( 293 | "Hai @%s\nDemi kenyamanan bersama, mengirim pesan menggunakan akun channel tidak diperbolehkan. Silakan menggunakan akun personal.", 294 | member.UserName, 295 | ), 296 | ) 297 | msg.ParseMode = "markdown" 298 | notice, _ := bot.Send(msg) 299 | 300 | if notice.MessageID > 0 { 301 | // Delete after 3s 302 | go func() { 303 | time.Sleep(10 * time.Second) 304 | log.Println("[softkick] Deleting channel notice message after 10 second...") 305 | bot.Request(tg.NewDeleteMessage(update.Message.Chat.ID, notice.MessageID)) 306 | }() 307 | } 308 | 309 | } 310 | } 311 | 312 | switch { 313 | 314 | // New Member Join 315 | case update.Message.NewChatMembers != nil: 316 | 317 | members := update.Message.NewChatMembers 318 | // Cleanup join message 319 | if _, err := bot.Request(tg.NewDeleteMessage(update.Message.Chat.ID, update.Message.MessageID)); err != nil { 320 | sentry.CaptureException(err) 321 | log.Println("[cleanup] unable delete join message") 322 | } 323 | 324 | // var member tg.User 325 | for _, member := range members { 326 | 327 | if member.UserName == app.Config.BotUsername && update.Message.Chat.ID != app.Config.GroupID { 328 | // Left Chat on unregistered group 329 | _, err := bot.Request(tg.LeaveChatConfig{ 330 | ChatID: update.Message.Chat.ID, 331 | }) 332 | // _, err := bot.LeaveChat(tg.ChatConfig{ 333 | // ChatID: update.Message.Chat.ID, 334 | // }) 335 | 336 | log.Printf("[leavechat] Leave chat from unauthorized group %v", update.Message.Chat.ID) 337 | if err != nil { 338 | log.Printf("[leavechat] Error Leave chat from unauthorized group %v", update.Message.Chat.ID) 339 | } 340 | } else if member.IsBot && member.UserName != bot.Self.UserName { 341 | // Kick other bot 342 | _, err := bot.Request(tg.KickChatMemberConfig{ 343 | ChatMemberConfig: tg.ChatMemberConfig{ 344 | ChatID: update.Message.Chat.ID, 345 | UserID: member.ID, 346 | }, 347 | }) 348 | log.Printf("[kickbot] Kick bot @%s", member.UserName) 349 | if err != nil { 350 | log.Printf("[kickbot] Error kick bot @%s :%v", member.UserName, err) 351 | } 352 | } else { 353 | // Check CAS Banned 354 | if checkBanned(member.ID) { 355 | // Kick Spammer 356 | _, err := bot.Request(tg.KickChatMemberConfig{ 357 | ChatMemberConfig: tg.ChatMemberConfig{ 358 | ChatID: update.Message.Chat.ID, 359 | UserID: member.ID, 360 | }, 361 | }) 362 | log.Printf("[cas] Kick spammer %d", member.ID) 363 | if err != nil { 364 | log.Printf("[cas] Error kick spammer %d :%v", member.ID, err) 365 | } 366 | 367 | // Send Notice 368 | msg := tg.NewMessage( 369 | update.Message.Chat.ID, 370 | fmt.Sprintf( 371 | "Member *%s* (%d) dikeluarkan karena terindikasi Spammer.\n\n[Check](https://cas.chat/query?u=%d)", 372 | member.FirstName, 373 | member.ID, 374 | member.ID, 375 | ), 376 | ) 377 | msg.ParseMode = "markdown" 378 | notice, _ := bot.Send(msg) 379 | 380 | if notice.MessageID > 0 { 381 | // Delete after 3s 382 | go func() { 383 | time.Sleep(5 * time.Second) 384 | log.Println("[cas] Deleting cas notice message after 3 second...") 385 | bot.Request(tg.NewDeleteMessage(update.Message.Chat.ID, notice.MessageID)) 386 | }() 387 | } 388 | return 389 | } 390 | 391 | // Send welcome message except itself 392 | if member.UserName != app.Config.BotUsername { 393 | tx := app.DB.Begin() 394 | 395 | var captcha models.UserCaptcha 396 | code := randomStr(5) 397 | 398 | if err := tx.Where("user_id = ?", member.ID).First(&captcha).Error; err != nil { 399 | // Unexpected error 400 | if !gorm.IsRecordNotFoundError(err) { 401 | log.Println("[captcha] unable to find existing code on db") 402 | tx.Rollback() 403 | 404 | sentry.CaptureException(err) 405 | return 406 | } 407 | 408 | captcha = models.UserCaptcha{ 409 | UserID: member.ID, 410 | Code: code, 411 | } 412 | 413 | if err := tx.Create(&captcha).Error; err != nil { 414 | log.Println("[captcha] Unable to save code on database") 415 | sentry.CaptureException(err) 416 | tx.Rollback() 417 | return 418 | } 419 | } 420 | 421 | text := fmt.Sprintf( 422 | "Selamat datang [%s](tg://user?id=%d) 👋\n\nSilahkan balas dengan pesan `%s` dalam waktu 5 menit untuk memastikan kamu bukan bot.", 423 | member.FirstName, 424 | member.ID, 425 | captcha.Code, 426 | ) 427 | 428 | msg := tg.NewMessage(update.Message.Chat.ID, text) 429 | msg.ParseMode = "markdown" 430 | 431 | log.Println("[join] New chat members", member.FirstName, member.ID) 432 | 433 | welcome, err := bot.Send(msg) 434 | if err != nil { 435 | log.Println("[bot] unable to send message") 436 | sentry.CaptureException(err) 437 | return 438 | } 439 | captcha.MessageID = welcome.MessageID 440 | 441 | if err := tx.Save(&captcha).Error; err != nil { 442 | log.Println("[captcha] unable to update message ID") 443 | sentry.CaptureException(err) 444 | } 445 | 446 | // Commit transaction 447 | tx.Commit() 448 | 449 | // Kick if timeout in 5 min 450 | go func() { 451 | time.Sleep(5 * time.Minute) 452 | kicked := app.kickUnverified(member.ID, update) 453 | // Delete welcome / captcha message 454 | if kicked { 455 | if _, err := bot.Request(tg.NewDeleteMessage(update.Message.Chat.ID, welcome.MessageID)); err != nil { 456 | sentry.CaptureException(err) 457 | log.Println("[timeout] unable to delete welcome message") 458 | } 459 | } 460 | }() 461 | 462 | } 463 | } 464 | } 465 | 466 | case update.Message.Text != "": 467 | // Filter Group command 468 | m := update.Message.Text 469 | 470 | if i := strings.Index(m, "!"); i == 0 { 471 | s := strings.Split(m, " ") 472 | cs := strings.Replace(s[0], "!", "", 1) 473 | log.Printf("[command] %s", cs) 474 | 475 | // Handle Update 476 | c := commands.Command{ 477 | Bot: bot, 478 | Message: update.Message, 479 | DB: app.DB, 480 | Config: app.Config, 481 | } 482 | c.Handle(cs) 483 | } 484 | 485 | case update.Message.Photo != nil: 486 | //TODO: Handle Photo message 487 | log.Println("New Photo Message") 488 | 489 | case update.Message.Sticker != nil: 490 | //TODO: Handle Sticker Message 491 | log.Println("New Sticker Message") 492 | case update.Message.LeftChatMember != nil: 493 | log.Printf("[left] member left: %s", update.Message.LeftChatMember.FirstName) 494 | default: 495 | log.Printf("[update] update handler tidak diketahui %v", update) 496 | } 497 | 498 | } 499 | 500 | func (app *App) kickUnverified(id int64, update tg.Update) bool { 501 | bot := app.Bot 502 | 503 | var captcha models.UserCaptcha 504 | if err := app.DB.Where("user_id = ?", id).First(&captcha).Error; err != nil { 505 | if !gorm.IsRecordNotFoundError(err) { 506 | sentry.CaptureException(err) 507 | } 508 | 509 | // Skip kick if no captcha record on db 510 | log.Printf("[softkick] skip kick %d, captcha already resolved", id) 511 | return false 512 | } 513 | 514 | // Ban chat member, can rejoin after 35 second. 515 | // https://core.telegram.org/bots/api#banchatmember 516 | if _, err := bot.Request(tg.BanChatMemberConfig{ 517 | ChatMemberConfig: tg.ChatMemberConfig{ 518 | ChatID: update.Message.Chat.ID, 519 | UserID: id, 520 | }, 521 | UntilDate: time.Now().Add(35 * time.Second).Unix(), 522 | }); err != nil { 523 | log.Printf("[softkick] Error kick spammer %d :%v", id, err) 524 | } 525 | 526 | // Delete captcha record 527 | if err := app.DB.Delete(models.UserCaptcha{}, "user_id = ?", id).Error; err != nil { 528 | log.Printf("[softkick] delete captcha (%d) so they can join again (if real human)", id) 529 | } 530 | 531 | kicked := "" 532 | // Find member from update 533 | for _, member := range update.Message.NewChatMembers { 534 | if member.ID == id { 535 | kicked = member.FirstName 536 | } 537 | } 538 | 539 | if kicked == "" { 540 | kicked = fmt.Sprintf("%d", id) 541 | } 542 | 543 | // Send Notice 544 | msg := tg.NewMessage( 545 | update.Message.Chat.ID, 546 | fmt.Sprintf( 547 | "🤦🏻 User %s dikeluarkan karena tidak menjawab captcha lebih dari 5 menit.", 548 | kicked, 549 | ), 550 | ) 551 | msg.ParseMode = "markdown" 552 | notice, _ := bot.Send(msg) 553 | 554 | if notice.MessageID > 0 { 555 | // Delete after 3s 556 | go func() { 557 | time.Sleep(3 * time.Second) 558 | log.Println("[softkick] Deleting cas notice message after 3 second...") 559 | bot.Request(tg.NewDeleteMessage(update.Message.Chat.ID, notice.MessageID)) 560 | }() 561 | } 562 | 563 | return true 564 | } 565 | --------------------------------------------------------------------------------