├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.md ├── build.sh ├── contrib └── bot.service ├── docker-compose.yml ├── locale ├── en-US.ini └── ru-RU.ini ├── main.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | silencebot 3 | bin 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang 2 | 3 | 4 | # Copy the local package files to the container's workspace. 5 | ADD . /go/src/github.com/aprosvetova/silencebot 6 | 7 | # Build the silencebot inside the container. 8 | RUN go get -v github.com/aprosvetova/silencebot && go install github.com/aprosvetova/silencebot 9 | 10 | # Run the silencebot by default when the container starts. 11 | ENTRYPOINT /go/bin/silencebot -t 123456789:XXXxXxxXxxx0xxxXX00XXXX0XXxXXxxXxxx 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silence Bot for Telegram supergroups 2 | 3 | Silence Bot allows you to calm down all chat participants by muting them temporarily. 4 | 5 | Just add the bot to your supergroup, give it message deletion and user restriction rights and you're all set. 6 | If you give it a right to pin messages, the bot will pin "Silence mod enabled" messages. 7 | 8 | >**Use /silence to enable silent mode.** 9 | > 10 | >All non-admin messages will be deleted in silent mode, any user who tries to send a message will get a temporary read-only restriction. 11 | 12 | >**Use /silence again to disable silent mode.** 13 | > 14 | >All users will be unrestricted automatically and be able to chat. 15 | 16 | >**Use /switchlang to switch between Russian and English** 17 | 18 | **Please note that the bot requires running Redis instance to store data.** 19 | 20 | I'm very new to Go, so I'll be happy if you make some pull requests. 21 | 22 | ## Building for all platforms 23 | Make sure you have Go installed and just run 24 | ``` 25 | ./build.sh 26 | ``` 27 | 28 | ## Building manually 29 | Install dependencies 30 | ``` 31 | go get ./... 32 | ``` 33 | And then build 34 | ``` 35 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -tags netgo -ldflags '-s -w -extldflags "-static"' -o silencebot 36 | ``` 37 | 38 | ## Running 39 | Use `./silencebot -t ` to start the bot. 40 | 41 | By default it connects to localhost:6379 Redis instance without password and selects db 0. 42 | You can customize this behavior, check `./silencebot -h` for all arguments. 43 | 44 | ## Running as a background service 45 | 46 | There are two ways as for now: Docker compose and systemd service 47 | 48 | **Don't forget to replace token!** 49 | 50 | Docker compose is ready to use, but not recommended for stable environments as long as redis is running inside Docker. 51 | 52 | [systemd service example](contrib/bot.service) (recommended) 53 | 54 | ## TODO 55 | 56 | - [ ] Embedded service autoinstall 57 | - [ ] Minimal hidden admin commands (`/stats`, `/health`, `/uptime` etc.) 58 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | platforms=("GOOS=linux GOARCH=amd64" "GOOS=linux GOARCH=386" "GOOS=windows GOARCH=amd64" "GOOS=windows GOARCH=386") 3 | names=("linux-x64" "linux-i386" "win-x64.exe" "win-i386.exe") 4 | 5 | echo "Resolving dependencies" 6 | 7 | go get ./... 8 | 9 | for i in "${!platforms[@]}"; do 10 | echo "Building ${names[$i]}" 11 | eval "CGO_ENABLED=0 ${platforms[$i]} go build -a -tags netgo -ldflags '-s -w -extldflags \"-static\"' -o bin/silencebot-${names[$i]}" 12 | done 13 | 14 | echo "Done" -------------------------------------------------------------------------------- /contrib/bot.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Silence Bot 3 | After=syslog.target 4 | 5 | [Service] 6 | LimitNOFILE=1048576 7 | ExecStart=/srv/silence -t 123456789:XXXxXxxXxxx0xxxXX00XXXX0XXxXXxxXxxx 8 | WorkingDirectory=/tmp 9 | Restart=always 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | redis: 5 | image: "redis:alpine" 6 | restart: always 7 | silencebot: 8 | build: . 9 | restart: always 10 | environment: 11 | DB_ADDR: 'redis:6379' 12 | links: 13 | - redis 14 | 15 | -------------------------------------------------------------------------------- /locale/en-US.ini: -------------------------------------------------------------------------------- 1 | welcome_message = *Hey there!*\nI will help you to calm down your users.\n\nI need "Delete Messages" and "Ban Users" rights to work correctly.\n\nUse /silence to enable/disable silent mode.\n\nIf you give me "Pin Messages" right, I'll pin the "Silent mode enabled" post (don't worry, I'll restore the original pinned message if you have one, after you disable silent mode)\n\nBy the way, you can switch me to Russian with /switchlang 2 | language_switched = Now I speak English 3 | bot_not_admin = I'm not an admin :( 4 | silence_enabled = *Silence mode enabled. Tshhh!* 5 | silence_disabled = *Silence mode disabled. Make flood great again.* -------------------------------------------------------------------------------- /locale/ru-RU.ini: -------------------------------------------------------------------------------- 1 | welcome_message = *Приветики!*\nЯ помогу успокоить бурю в вашем чате.\n\nМне нужны права "Delete Messages" и "Ban Users", чтобы функционировать.\n\nКоманда /silence включает/выключает режим тишины.\n\nЕсли дать мне право "Pin Messages", я буду закреплять сообщение о начале режима тишины до его окончания (не переживайте, если у вас было закреплённое сообщение, оно вернётся обратно)\n\nКстати, я могу говорить и по-английски: /switchlang 2 | language_switched = Теперь я говорю по-русски 3 | bot_not_admin = Я не админ :( 4 | silence_enabled = *В чате активирован режим тишины!* 5 | silence_disabled = *Режим тишины отключен. Можете общаться дальше.* -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "time" 7 | 8 | alog "github.com/apex/log" 9 | "github.com/apex/log/handlers/cli" 10 | 11 | "github.com/Unknwon/i18n" 12 | "github.com/go-redis/redis" 13 | "github.com/spf13/pflag" 14 | tb "gopkg.in/tucnak/telebot.v2" 15 | "strings" 16 | ) 17 | 18 | var b *tb.Bot 19 | var db *redis.Client 20 | 21 | var log = &alog.Logger{ 22 | Level: alog.DebugLevel, 23 | Handler: cli.New(os.Stderr), 24 | } 25 | 26 | func main() { 27 | i18n.SetMessage("en-US", "locale/en-US.ini") 28 | i18n.SetMessage("ru-RU", "locale/ru-RU.ini") 29 | 30 | redisHost := pflag.StringP("rhost", "h", "localhost:6379", "redis host and port") 31 | redisPwd := pflag.StringP("rpwd", "p", "", "redis password") 32 | redisDb := pflag.IntP("rdb", "n", 0, "redis DB number (default 0)") 33 | tgToken := pflag.StringP("token", "t", "", "telegram bot token (required)") 34 | pflag.Parse() 35 | 36 | if *tgToken == "" { 37 | log.Fatal("telegram bot token required") 38 | } 39 | 40 | db = redis.NewClient(&redis.Options{ 41 | Addr: *redisHost, 42 | Password: *redisPwd, 43 | DB: *redisDb, 44 | }) 45 | 46 | if err := db.DbSize().Err(); err != nil { 47 | log.WithError(err).Fatal("redis error") 48 | } 49 | 50 | poller := tb.NewMiddlewarePoller(&tb.LongPoller{Timeout: 10 * time.Second}, func(upd *tb.Update) bool { 51 | if upd.Message != nil && upd.Message.Chat.Type != tb.ChatSuperGroup { 52 | return false 53 | } 54 | return true 55 | }) 56 | 57 | var err error 58 | b, err = tb.NewBot(tb.Settings{ 59 | Token: *tgToken, 60 | Poller: poller, 61 | }) 62 | 63 | if err != nil { 64 | log.WithError(err).Fatal("can't init bot instance") 65 | } 66 | 67 | b.Handle("/silence", silenceCommand) 68 | b.Handle("/switchlang", switchLangCommand) 69 | 70 | //uh really? why can't I just handle all Message updates? 71 | b.Handle(tb.OnText, checkMessage) 72 | b.Handle(tb.OnAudio, checkMessage) 73 | b.Handle(tb.OnContact, checkMessage) 74 | b.Handle(tb.OnDocument, checkMessage) 75 | b.Handle(tb.OnLocation, checkMessage) 76 | b.Handle(tb.OnPhoto, checkMessage) 77 | b.Handle(tb.OnSticker, checkMessage) 78 | b.Handle(tb.OnVenue, checkMessage) 79 | b.Handle(tb.OnVideo, checkMessage) 80 | b.Handle(tb.OnVideoNote, checkMessage) 81 | b.Handle(tb.OnVoice, checkMessage) 82 | 83 | b.Handle(tb.OnPinned, savePinnedMessage) 84 | 85 | b.Handle(tb.OnAddedToGroup, showWelcomeMessage) 86 | 87 | b.Start() 88 | } 89 | 90 | func savePinnedMessage(m *tb.Message) { 91 | db.Set(getPinnedMessageKey(m.PinnedMessage.Chat), m.PinnedMessage.ID, 0) 92 | } 93 | 94 | func restorePinnedMessage(c *tb.Chat) { 95 | msgID, err := db.Get(getPinnedMessageKey(c)).Int64() 96 | if err != nil || msgID == 0 { 97 | b.Unpin(c) 98 | return 99 | } 100 | b.Pin(&tb.Message{ 101 | Chat: c, 102 | ID: int(msgID), 103 | }, tb.Silent) 104 | } 105 | 106 | func checkMessage(m *tb.Message) { 107 | if isSilent(m.Chat) { 108 | if isAdmin(m.Chat, m.Sender) { 109 | return 110 | } 111 | b.Delete(m) 112 | restrictUser(m.Chat, m.Sender) 113 | } 114 | } 115 | 116 | func showWelcomeMessage(m *tb.Message) { 117 | b.Send(m.Chat, strings.Replace(i18n.Tr(getLang(m.Chat), "welcome_message"), "\\n", "\n", -1), tb.ModeMarkdown) 118 | } 119 | 120 | func switchLangCommand(m *tb.Message) { 121 | if !isAdmin(m.Chat, m.Sender) { 122 | b.Delete(m) 123 | return 124 | } 125 | currentLang := getLang(m.Chat) 126 | newLang := "ru-RU" 127 | if currentLang == "ru-RU" { 128 | newLang = "en-US" 129 | } 130 | setLang(m.Chat, newLang) 131 | b.Reply(m, i18n.Tr(newLang, "language_switched")) 132 | } 133 | 134 | func silenceCommand(m *tb.Message) { 135 | if !isAdmin(m.Chat, m.Sender) { 136 | b.Delete(m) 137 | return 138 | } 139 | if !isAdmin(m.Chat, b.Me) { 140 | log.WithField("chatID", m.Chat.ID).Debug("bot has no admin rights") 141 | b.Reply(m, i18n.Tr(getLang(m.Chat), "bot_not_admin")) 142 | db.Del(getAdminsKey(m.Chat)) 143 | return 144 | } 145 | 146 | if isSilent(m.Chat) { 147 | setSilent(m.Chat, false) 148 | log.WithField("chatID", m.Chat.ID).Debug("disabling silent mode") 149 | go unrestrictAll(m.Chat) 150 | b.Send(m.Chat, i18n.Tr(getLang(m.Chat), "silence_disabled"), tb.ModeMarkdown) 151 | restorePinnedMessage(m.Chat) 152 | } else { 153 | setSilent(m.Chat, true) 154 | log.WithField("chatID", m.Chat.ID).Debug("enabled silent mode") 155 | msg, err := b.Send(m.Chat, i18n.Tr(getLang(m.Chat), "silence_enabled"), tb.ModeMarkdown) 156 | if err == nil { 157 | b.Pin(msg, tb.Silent) 158 | } 159 | } 160 | } 161 | 162 | func isAdmin(chat *tb.Chat, user *tb.User) bool { 163 | key := getAdminsKey(chat) 164 | if db.Exists(key).Val() == 0 { 165 | members, err := b.AdminsOf(chat) 166 | if err != nil { 167 | return false 168 | } 169 | var admins []interface{} 170 | found := false 171 | for _, member := range members { 172 | if member.CanDeleteMessages || member.Role == tb.Creator { 173 | admins = append(admins, member.User.ID) 174 | if member.User.ID == user.ID { 175 | found = true 176 | } 177 | } 178 | } 179 | db.SAdd(key, admins...) 180 | db.Expire(key, 10*time.Minute) 181 | return found 182 | } else { 183 | return db.SIsMember(key, user.ID).Val() 184 | } 185 | } 186 | 187 | func setSilent(chat *tb.Chat, silent bool) { 188 | key := getSilentKey(chat) 189 | if silent { 190 | db.Set(key, 1, 0) 191 | } else { 192 | db.Del(key) 193 | } 194 | } 195 | 196 | func isSilent(chat *tb.Chat) bool { 197 | return db.Exists(getSilentKey(chat)).Val() == 1 198 | } 199 | 200 | func restrictUser(chat *tb.Chat, user *tb.User) { 201 | b.Restrict(chat, &tb.ChatMember{User: user, RestrictedUntil: time.Now().Add(5 * time.Minute).Unix()}) 202 | db.SAdd(getRestrictedKey(chat), user.ID) 203 | } 204 | 205 | func unrestrictAll(chat *tb.Chat) { 206 | key := getRestrictedKey(chat) 207 | users := db.SMembers(key).Val() 208 | db.Del(key) 209 | for _, user := range users { 210 | userID, err := strconv.Atoi(user) 211 | if err != nil { 212 | continue 213 | } 214 | member := &tb.ChatMember{User: &tb.User{ID: userID}, RestrictedUntil: tb.Forever(), Rights: tb.Rights{CanSendMessages: true}} 215 | b.Promote(chat, member) 216 | time.Sleep(100 * time.Millisecond) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | tb "gopkg.in/tucnak/telebot.v2" 7 | ) 8 | 9 | func getAdminsKey(chat *tb.Chat) string { 10 | return fmt.Sprintf("admins.%d", chat.ID) 11 | } 12 | 13 | func getSilentKey(chat *tb.Chat) string { 14 | return fmt.Sprintf("silent.%d", chat.ID) 15 | } 16 | 17 | func getRestrictedKey(chat *tb.Chat) string { 18 | return fmt.Sprintf("restricted.%d", chat.ID) 19 | } 20 | 21 | func getPinnedMessageKey(chat *tb.Chat) string { 22 | return fmt.Sprintf("pinned.%d", chat.ID) 23 | } 24 | 25 | func getLangKey(chat *tb.Chat) string { 26 | return fmt.Sprintf("lang.%d", chat.ID) 27 | } 28 | 29 | func getLang(chat *tb.Chat) string { 30 | lang := db.Get(getLangKey(chat)).Val() 31 | if lang == "" { 32 | return "en-US" 33 | } 34 | return lang 35 | } 36 | 37 | func setLang(chat *tb.Chat, lang string) { 38 | db.Set(getLangKey(chat), lang, 0) 39 | } --------------------------------------------------------------------------------