-
Notifications
You must be signed in to change notification settings - Fork 0
/
bot.go
383 lines (350 loc) · 13.6 KB
/
bot.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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package main
import (
"fmt"
"strings"
"strconv"
"os"
"os/signal"
"github.com/bwmarrin/discordgo"
)
/* Formats the result of a dice roll in a pretty, human-readable way.
*/
func formatRollResult(expression string, result int, rolls []DiceRoll) string {
rollResults := ""
for _, r := range rolls {
parts := strings.Split(r.Expression, "d")
max, _ := strconv.Atoi(parts[1])
if max <= 2 {
rollResults += fmt.Sprintf("> 🎲 **%s** %v\n", r.Expression, r.Results)
} else {
resultsDisplay := []string{}
for _, result := range r.Results {
if result == 1 {
resultsDisplay = append(resultsDisplay, fmt.Sprintf("🔻**%d**", result))
} else if result == max {
resultsDisplay = append(resultsDisplay, fmt.Sprintf("🔺**%d**", result))
} else {
resultsDisplay = append(resultsDisplay, fmt.Sprintf("%d", result))
}
}
rollResults += fmt.Sprintf("> 🎲 **%s** %v\n", r.Expression, resultsDisplay)
}
}
return fmt.Sprintf(
"You asked me to roll: `%s`\nYou rolled a **%d**!\n> *ROLL RESULTS*\n%s",
expression,
result,
rollResults,
)
}
/* Sends a message to Discord.
* Used for the bot to respond to slash commands.
*/
func sendDiscordMessage(s* discordgo.Session, i *discordgo.InteractionCreate, message string) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: message,
AllowedMentions: &discordgo.MessageAllowedMentions{
Parse: []discordgo.AllowedMentionType{},
},
},
})
}
/* Sets up and runs a Discord bot to respond to slash commands for rolling dice.
* The following commands are supported:
* - /roll <expression> | rolls the given expression
* - /make-macro <name> <expression> | creates a macro with the given name
* - /roll-macro <name> <arguments> | rolls the macro with the given name using given arguments
* - /list-macros | lists all macros available to the server
* - /view-macro <name> | views the macro with the given name
* - /delete-macro <name> | deletes the macro with the given name
* - /edit-macro <name> <expression> | replaces existing macro with given expression
* - /help-me-roll | displays help/usage information
*/
func RunBot() {
// Set up the discord bot
fmt.Println("Initializing bot...")
token := os.Getenv("DISCORD_TOKEN")
dg, err := discordgo.New("Bot " + token)
if err != nil {
fmt.Println("Error creating Discord session: ", err)
return
}
// Open websocket connection to Discord and begin listening
fmt.Println("Opening websocket connection...")
err = dg.Open()
if err != nil {
fmt.Println("Error opening connection: ", err)
return
}
// Set up commands
fmt.Println("Registering commands...")
commands := []*discordgo.ApplicationCommand{
{
Name: "roll",
Description: "Roll some dice",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "expression",
Description: "Your expression with dice notation",
Required: true,
},
},
},
{
Name: "make-macro",
Description: "Create a macro",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "name",
Description: "The name of the macro",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "expression",
Description: "The macro expression, using A, B, C etc for inputs to the macro",
Required: true,
},
},
},
{
Name: "roll-macro",
Description: "Roll one of your custom macros",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "name",
Description: "The name of the macro you want to roll",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "inputs",
Description: "The inputs to the macro, separated by spaces",
Required: false,
},
},
},
{
Name: "list-macros",
Description: "List all macros available to the server",
},
{
Name: "view-macro",
Description: "View an existing macro",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "name",
Description: "The name of the macro",
Required: true,
},
},
},
{
Name: "delete-macro",
Description: "Delete an existing macro",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "name",
Description: "The name of the macro",
Required: true,
},
},
},
{
Name: "edit-macro",
Description: "Create a macro",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "name",
Description: "The name of the macro",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "expression",
Description: "The macro expression, using A, B, C etc for inputs to the macro",
Required: true,
},
},
},
{
Name: "help-me-roll",
Description: "Shows you how to use the DiceMancer bot",
},
}
commandHandlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"roll": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
argument := i.ApplicationCommandData().Options[0].StringValue()
result, rolls, error := ParseExpression(argument)
if error != nil {
sendDiscordMessage(s, i, fmt.Sprintf("**Uh-oh!** Error occurred parsing: %s \n%s", argument, error))
} else {
sendDiscordMessage(s, i, formatRollResult(argument, result, rolls))
}
},
"make-macro": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
name := i.ApplicationCommandData().Options[0].StringValue()
expression := i.ApplicationCommandData().Options[1].StringValue()
// Check if a macro with this name already exists
existing, _ := FindMacro(i.Interaction.GuildID, name)
if existing != nil {
sendDiscordMessage(s, i, fmt.Sprintf("A macro with the name '%s' already exists.", name))
return
}
// Validate the macro expression
err := ValidateMacro(expression)
if err != nil {
sendDiscordMessage(s, i, fmt.Sprintf("Invalid macro expression: %s", err))
return
}
// Create the new macro
newMacro := Macro{
Guild: i.Interaction.GuildID,
Name: name,
Expression: expression,
}
MakeMacro(&newMacro)
sendDiscordMessage(s, i, fmt.Sprintf("Macro '%s' created!\nMacro expression: %s", name, expression))
},
"roll-macro": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
name := i.ApplicationCommandData().Options[0].StringValue()
arguments := []string{}
if len(i.ApplicationCommandData().Options) == 2 {
arguments = strings.Fields(i.ApplicationCommandData().Options[1].StringValue())
}
macro, _ := FindMacro(i.Interaction.GuildID, name)
if macro != nil {
expression := FillMacro(macro.Expression, arguments)
result, rolls, err := ParseExpression(expression)
if err != nil {
sendDiscordMessage(s, i, fmt.Sprintf("**Uh-oh!** Error occurred parsing: %s \n%s", expression, err))
return
}
sendDiscordMessage(s, i, formatRollResult(expression, result, rolls))
} else {
sendDiscordMessage(s, i, fmt.Sprintf("No macro with the name '%s' was found.", name))
}
},
"list-macros": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
macros, _ := ListMacros(i.Interaction.GuildID)
if macros != nil && len(macros) > 0 {
listMessage := "Macros found: \n"
for _, m := range macros {
listMessage += fmt.Sprintf("**%s**: %s\n", m.Name, m.Expression)
}
sendDiscordMessage(s, i, listMessage)
} else {
sendDiscordMessage(s, i, "No macros found. Create some with the /make-macro command.")
}
},
"view-macro": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
name := i.ApplicationCommandData().Options[0].StringValue()
macro, _ := FindMacro(i.Interaction.GuildID, name)
if macro != nil {
sendDiscordMessage(s, i, fmt.Sprintf("Macro '%s' found: %s", macro.Name, macro.Expression))
} else {
sendDiscordMessage(s, i, fmt.Sprintf("No macro with the name '%s' was found.", name))
}
},
"delete-macro": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
name := i.ApplicationCommandData().Options[0].StringValue()
macro, _ := FindMacro(i.Interaction.GuildID, name)
if macro != nil {
DeleteMacro(macro)
sendDiscordMessage(s, i, fmt.Sprintf("Macro '%s' was deleted.", name))
} else {
sendDiscordMessage(s, i, fmt.Sprintf("No macro with the name '%s' was found.", name))
}
},
"edit-macro": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
name := i.ApplicationCommandData().Options[0].StringValue()
expression := i.ApplicationCommandData().Options[1].StringValue()
macro, _ := FindMacro(i.Interaction.GuildID, name)
if macro != nil {
macro.Expression = expression
EditMacro(macro)
sendDiscordMessage(s, i, fmt.Sprintf("Macro '%s' was updated: %s", name, expression))
} else {
sendDiscordMessage(s, i, fmt.Sprintf("No macro with the name '%s' was found.", name))
}
},
"help-me-roll": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
helpMessage := `**DiceMancer Bot Available Commands**
🎲 Basic Usage 🎲
**/roll** <expression>
- Example usage: `+"`"+`/roll 4d10 + 5`+"`"+`
- You can give it any arithmetic expression with both numbers and dice notation.
- Dice notation must be in the form XdY, where X and Y are integers.
- For advantage and disadvantage, you can write ! or ? after your dice notation to get the highest and lowest roll respectively. For example, 4d10! will get the highest of the four rolls, while 4d10? will get the lowest.
- You can roll up to d200 and up to 20 rolls at once.
🎲 Macros 🎲
A macro is an expression you can re-use again and again. Macros can have inputs, which must be written as uppercase letters starting from A. If the macro only has one input, it must be named A; two, must be named A and B, and so on.
For example, you can have a macro: `+"`"+`4 * (A + B)`+"`"+`
You will be able to roll this macro substituting anything you'd like for the variables A and B.
**/make-macro** <name> <expression>
- This is used to create a macro. For example: `+"`"+`/make-macro my-macro 4 * (A + B)`+"`"+`
- Macros can be named anything, with a maximum of 128 characters.
**/roll-macro** <name> <inputs separated by spaces>
- This is how you roll a macro once it's created. Specify the name of the macro, following by what you want the A, B, C, etc to be separated by spaces. (They can be either numbers or dice notation.)
- For example: `+"`"+`/roll-macro my-macro 10 4d6`+"`"+`
There are several other commands to help you view, edit, and delete macros:
**/list-macros** | Lists all macros available.
**/view-macro** <name> | Displays the macro with the given name.
**/delete-macro** <name> | Deletes the macro with the given name.
**/edit-macro** <name> <expression> | Updates the existing macro.
Macros are tied to the server and macros created by this server can only be used in this server.
Please enjoy using DiceMancer, and feel free to contact the developer <@284867832376721409> if you have further questions or comments.
`
sendDiscordMessage(s, i, helpMessage)
},
}
// Register commands
_, err = dg.ApplicationCommandBulkOverwrite(
dg.State.User.ID, "", commands,
)
if err != nil {
fmt.Printf("Error registering commands: %s", err)
return
}
// Add command handlers
fmt.Println("Adding command handlers...")
dg.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Check if server is allowed to use the bot
serverHasAccess, err := ServerHasAccess(i.Interaction.GuildID)
if err != nil {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Error loading list of allowed servers. Please contact bot admin for support."),
},
})
return
}
if !serverHasAccess {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Your server does not have access to use the bot."),
},
})
return
}
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
})
// Keep running this program until interrupt signal is received
defer dg.Close()
fmt.Println("** Bot is now running. Press Ctrl+C to exit. **")
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
<- stop
}