├── .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 |
--------------------------------------------------------------------------------