-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
318 lines (277 loc) · 10.1 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
package main
import (
"bytes"
"encoding/gob"
"fmt"
"log"
"os"
"strings"
"text/template"
"time"
"github.com/go-redis/redis"
tb "gopkg.in/tucnak/telebot.v2"
"strconv"
"regexp"
)
/*
STATE GUIDE
KEY FORMAT == type:instance:attribute
beru:chats <SET> : chats beru has been invited to
chat:%chatID:admins <SET> : admins for this chat that can access beru admin commands
chat:%chatID:owner <int> : super user/owner of chat, user that invited beru, can modify
admin set
chat:%chatID:commands <MAP> : map of command names to static replies
chat:%chatID:title <string> : name of chat
chat:%chatID:usersJoinedCount <int> : number of users joined since beru started tracking
chat:%chatID:usersJoinedLimit <int> : number of users joined before beru posts welcome message
chat:%chatID:usersJoinedMessage <string> : welcome message to post
chat:%chatID:price <MAP> : details for the price command
.slug <string> : the slug identifier on CMC for the token, found in the url
.conversion <string> : the fiat or crypto ticker symbol to act as a secondary price
.
user:%userID:activechat <string> : the chat to which the commands will affect
user:%userID:activePath <Path> : the user dialogue Path that has been started, but not fully traversed
user:%userID:chats <SET> : quick lookup to see what chats user is admin/owner of
user:%user:info <tb.User> : user object for looking up user details
*/
var R *redis.Client
var B *tb.Bot
const ErrorResponse string = "Something went wrong and I wasn't able to fulfill that request"
var helpGuide = `
Nice you meet you, my name is Beru!
I can help you create and manage Telegram chats.
You can control me by sending these commands:
*Chat Owner Only*
/addadmin - allows another user to change the chat rules
/removeadmin - removes a user's ability to change chat rules
/viewadmins - displays list of users with admin privileges
*Beru Level Functionality*
/switchchat - changes which chat beru is managing when a user is an owner/admin of multiple chats
/addchat - shortcut to invite link to add beru to your chat
/removechat - choose between currently managed chats to remove
*Custom Chat Commands*
/addcommand - adds a custom command and response
/removecommand - removes a custom command
/viewcommands - prints a list of custom commands
*Chat Features*
/setwelcome - greets every # users with a welcome message on chat join
/togglejoinmsg - toggles deletion of the notification posted when users join (supergroups only)
/addwhitelistedbot - adds a bot (by username) to be allowed to join a chat
/removewhitelistedbot - removes a bots ability to join a chat
/setpricecommand - allow beru to notify chats of a token's price
/setnewusermediarestriction - will delete all media posts by users newer then the time specified
`
func main() {
gob.Register(Path{})
gob.Register(Prompt{})
gob.Register(tb.User{})
gob.Register(tb.Chat{})
R = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 15,
})
var err error
B, err = tb.NewBot(tb.Settings{
Token: os.Getenv("TELEBOT_SECRET"),
Poller: &tb.LongPoller{Timeout: 10 * time.Second},
})
if err != nil {
panic(err)
return
}
for k, v := range BuiltinCommandRegistry {
B.Handle(k, v)
}
// Command: /start <PAYLOAD>
B.Handle("/start", func(m *tb.Message) {
if !m.Private() {
return
}
// if the user isnt an admin of any chats
key := fmt.Sprintf("user:%d:chats", m.Sender.ID)
if nChats := R.SCard(key).Val(); nChats == 0 {
B.Send(m.Sender, "I need to be invited to a chat before I can be useful")
addChat([]*tb.Message{m})
} else {
listFunctionGroups(m)
}
})
// Command: /start <PAYLOAD>
B.Handle("/help", func(m *tb.Message) {
if !m.Private() {
return
}
B.Send(m.Sender, helpGuide, tb.ParseMode(tb.ModeMarkdown))
})
// deletes message if posted while the restriction flag still exists
removeMsgIfDisallowed := func(m *tb.Message) {
restrictionUserKey := fmt.Sprintf("chat:%d:userRestricted:%d", m.Chat.ID, m.Sender.ID)
exists := R.Exists(restrictionUserKey).Val()
if exists == 1 {
B.Delete(m)
}
}
B.Handle(tb.OnPhoto, func(m *tb.Message) {
removeMsgIfDisallowed(m)
})
B.Handle(tb.OnVideo, func(m *tb.Message) {
removeMsgIfDisallowed(m)
})
B.Handle(tb.OnText, func(m *tb.Message) {
matched, _ := regexp.Match(`^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$`, []byte(m.Text))
if matched {
removeMsgIfDisallowed(m)
}
if p := getUsersActivePath(m.Sender.ID); p != nil {
step(m, p)
}
// check if command
if strings.HasPrefix(m.Text, "/") {
commandName := strings.Split(m.Text, " ")[0]
var chat int
var dest tb.Recipient
// if chatting with beru, respond to user, else chat
if m.Private() {
chat, _, _ = getUsersActiveChat(m.Sender.ID)
dest = m.Sender
} else {
chat = int(m.Chat.ID)
dest = m.Chat
}
key := fmt.Sprintf("chat:%d:commands", chat)
if commandText, err := R.HGet(key, commandName).Result(); err != redis.Nil {
t, _ := template.New("command").Parse(commandText)
by := bytes.Buffer{}
if err := t.Execute(&by, m); err != nil {
LogE.Printf("failed to render template for command %s in chat %d", commandName, chat)
B.Send(dest, ErrorResponse)
} else {
B.Send(dest, by.String())
}
}
}
})
B.Handle("/admins", func(m *tb.Message) {
if m.Private() {
return
}
if members, err := B.AdminsOf(m.Chat); err != nil {
LogE.Printf("error fetching admins for chat %s", m.Chat.ID)
B.Send(m.Sender, ErrorResponse)
} else {
var usernameList = []string{}
for _, u := range members {
usernameList = append(usernameList, "@"+u.User.Username)
}
B.Send(m.Chat, fmt.Sprintf("Hey %s %s, the admins for this channel are: %s",
m.Sender.FirstName, m.Sender.LastName, strings.Join(usernameList, ", ")))
}
})
B.Handle("/price", func(m *tb.Message) {
if m.Private() {
return
}
key := fmt.Sprintf("chat:%d:price", m.Chat.ID)
slug, err := R.HGet(key, "slug").Result()
conversion, err := R.HGet(key, "conversion").Result()
msgFormat, err := R.HGet(key, "msgFormat").Result()
if err != nil {
B.Send(m.Chat, ErrorResponse)
LogE.Print(err)
return
}
token := getTokenInfo(slug)
price, converted, pct_price, pct_conversion := getTokenPrice(token.ID, conversion)
// these are all the possible tags a user can use in the message formatter
replacer := strings.NewReplacer(
"{{price}}", strconv.FormatFloat(price, 'f', 5, 64),
"{{ticker}}", token.Symbol,
"{{name}}", token.Name,
"{{slug}}", slug,
"{{conversion}}", strconv.FormatFloat(converted, 'f', 8, 64),
"{{price_pct_change}}", fmt.Sprintf("%+.1f%%", pct_price),
"{{conversion_pct_change}}", fmt.Sprintf("%+.1f%%", pct_conversion),
)
replaced := replacer.Replace(msgFormat)
B.Send(m.Chat, replaced)
})
B.Handle(tb.OnUserJoined, func(m *tb.Message) {
// add user to media restriction timer
restrictionTimeKey := fmt.Sprintf("chat:%d:userRestrictionTime", m.Chat.ID)
restrictionTime, err := R.Get(restrictionTimeKey).Int64()
if err != nil {
restrictionTime = 1
}
// set the user restriction flag with a time to live of whatever was specified in the channel config
restrictionUserKey := fmt.Sprintf("chat:%d:userRestricted:%d", m.Chat.ID, m.Sender.ID)
ttl := time.Duration(restrictionTime * 1e9)
err = R.Set(restrictionUserKey, 0, ttl).Err()
// kick bot if not whitelisted
k := fmt.Sprintf("chat:%d:botWhitelist", m.Chat.ID)
for _, u := range m.UsersJoined {
// we only need to lookup users that are bots
if !strings.HasSuffix(strings.ToLower(u.Username), "bot") {
continue
}
// for all bots, ban if not member or if whitelist was never set up
isMember, err := R.SIsMember(k, u.Username).Result()
if err != nil || !isMember {
B.Ban(m.Chat, &tb.ChatMember{User: &u, RestrictedUntil: tb.Forever()})
B.Send(m.Chat, "fuck ur bot")
}
}
// post welcome message if available
countKey := fmt.Sprintf("chat:%d:usersJoinedCount", m.Chat.ID)
limitKey := fmt.Sprintf("chat:%d:usersJoinedLimit", m.Chat.ID)
messageKey := fmt.Sprintf("chat:%d:usersJoinedMessage", m.Chat.ID)
usersJoined, _ := R.Incr(countKey).Result()
usersLimit, _ := R.Get(limitKey).Int64()
if usersJoined%usersLimit == 0 {
joinedMsg, _ := R.Get(messageKey).Result()
fmtMsg := strings.Replace(joinedMsg, "$username", m.Sender.Username, -1)
B.Send(m.Chat, fmtMsg)
}
// delete join notification if setting is set to on
deleteKey := fmt.Sprintf("chat:%d:deleteJoinNotification", m.Chat.ID)
delete, err := R.Get(deleteKey).Int64()
if err == nil && delete != 0 {
B.Delete(m)
}
})
B.Handle(tb.OnAddedToGroup, func(m *tb.Message) {
// add chat to list of chats beru has been added to
R.SAdd("beru:chats", m.Chat.ID)
// enables a quick check that user can admin chat
R.SAdd(fmt.Sprintf("user:%d:chats", m.Sender.ID), m.Chat.ID)
// save the full user info if we need it later
R.Set(fmt.Sprintf("user:%d:info", m.Sender.ID), EncodeUser(m.Sender), 0)
// add inviter to active chats admin list
R.SAdd(fmt.Sprintf("chat:%d:activeAdmins", m.Chat.ID), m.Sender.ID)
// since this is the inviter, add this user as the owner of the chat
R.Set(fmt.Sprintf("chat:%d:owner", m.Chat.ID), m.Sender.ID, 0)
// save the chat title so we can display it to the user
R.Set(fmt.Sprintf("chat:%d:title", m.Chat.ID), m.Chat.Title, 0)
// save the full chat info if we need it later
R.Set(fmt.Sprintf("chat:%d:info", m.Chat.ID), EncodeChat(m.Chat), 0)
// set user join notification deletion to off
R.Set(fmt.Sprintf("chat:%d:deleteJoinNotification", m.Chat.ID), 0, 0)
// add all chat admins to list so we can prompt user with potential
// options when adding and removing admins
if err := updateChatAdmins(int(m.Chat.ID)); err != nil {
B.Send(m.Sender, ErrorResponse)
}
LogI.Printf("beru joined chat %s (%d) invited by %s (%d)",
m.Chat.Title, m.Chat.ID, m.Sender.Username, m.Sender.ID)
B.Send(m.Sender, fmt.Sprintf("beru joined chat %s", m.Chat.Title))
setUsersActiveChat(m.Sender.ID, m.Chat.ID)
})
B.Start()
interrupt := make(chan os.Signal, 1)
select {
case <-interrupt:
log.Println("interrupt")
B.Stop()
return
}
}