├── .editorconfig
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── anpan.go
├── context.go
├── errors.go
├── example
├── README.md
├── help.go
└── main.go
├── go.mod
├── go.sum
├── handler.go
└── types.go
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.go]
12 | indent_style = unset
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
17 | [go.*]
18 | indent_style = unset
19 | indent_size = unset
20 | insert_final_newline = unset
21 | trim_trailing_whitespace = unset
22 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | name: Build
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Clone repository
12 | uses: actions/checkout@v2
13 |
14 | - name: Set up Go
15 | uses: actions/setup-go@v2
16 | with:
17 | go-version: 1.16
18 |
19 | - name: Build
20 | run: go build -v ./...
21 |
22 | - name: Test
23 | run: go test -v ./...
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | __debug_bin
3 | *.exe
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright 2019-2021 MikeModder/MikeModder007, apfel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://goreportcard.com/report/github.com/MikeModder/anpan)
3 | [](https://pkg.go.dev/github.com/MikeModder/anpan)
4 |
5 | # anpan
6 | **anpan** is a command handler for **[discordgo](https://github.com/bwmarrin/discordgo)**.
7 |
8 | ## Usage
9 | Check the `example` directory for an example for usage of anpan, in addition to an example of a help command.
10 |
11 | # Example
12 | An example bot can be found within the `example` folder.
13 |
--------------------------------------------------------------------------------
/anpan.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2021 MikeModder/MikeModder007, apfel
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 | //
5 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 | package anpan
10 |
11 | import (
12 | "os"
13 | "os/signal"
14 | "syscall"
15 | )
16 |
17 | /* Package anpan:
18 | * A custom command handler for discordgo. (https://github.com/bwmarrin/discordgo)
19 | *
20 | * anpan (c) 2019-2021 MikeModder/MikeModder007, apfel
21 | */
22 |
23 | // New creates a new command handler.
24 | //
25 | // Parameters:
26 | // prefixes - The prefixes to use for the command handler.
27 | // owners - The owners of this application; these are used for Owner-Only commands.
28 | // useState - Whether to use the session's state th fetch data or not. The state will be ignored if the State field of the session used in the message handler is set false.
29 | // ignoreBots - Whether to ignore users marked as bots or not.
30 | // checkPermissions - Whether to check permissions or not.
31 | // useRoutines - Whether to execute commands outside the event's routine.
32 | //
33 | // Notes:
34 | // Refer to MessageHandler to properly activate the command handler.
35 | func New(prefixes []string, owners []string, useState, ignoreBots, respondToPings, checkPermssions bool, prerunFunc PrerunFunc, errorFunc OnErrorFunc, debugFunc DebugFunc) CommandHandler {
36 | return CommandHandler{
37 | checkPermissions: checkPermssions,
38 | debugFunc: debugFunc,
39 | enabled: true,
40 | ignoreBots: ignoreBots,
41 | onErrorFunc: errorFunc,
42 | owners: owners,
43 | prefixes: prefixes,
44 | prerunFunc: prerunFunc,
45 | respondToPings: respondToPings,
46 | useState: useState,
47 | }
48 | }
49 |
50 | // WaitForInterrupt makes your application wait for an interrupt.
51 | // A SIGINT, SIGTERM or a console interrupt will make this function stop.
52 | // Note that the Exit function in the os package will make this function stop, too.
53 | func WaitForInterrupt() {
54 | sc := make(chan os.Signal, 1)
55 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
56 | <-sc
57 | }
58 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2021 MikeModder/MikeModder007, apfel
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 | //
5 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 | package anpan
10 |
11 | /* context.go:
12 | * Contains some utility functions for anpan.Context.
13 | *
14 | * anpan (c) 2019-2021 MikeModder/MikeModder007, apfel
15 | */
16 |
17 | import (
18 | "io"
19 |
20 | "github.com/bwmarrin/discordgo"
21 | )
22 |
23 | // Reply directly replies with a message.
24 | //
25 | // Parameters:
26 | // message - The message content.
27 | func (c *Context) Reply(message string) (*discordgo.Message, error) {
28 | return c.Session.ChannelMessageSend(c.Channel.ID, message)
29 | }
30 |
31 | // ReplyComplex combines Reply, ReplyEmbed and ReplyFile as a way to send a message with, for example, Text and an Embed together.
32 | //
33 | // Parameters:
34 | // message - The message content.
35 | // tts - Whether the client should read the message out or not.
36 | // embed - The embed for this message. Refer to discordgo.MessageEmbed for more info.
37 | // files - The files to send across. These (collectively) cannot pass more than 8 Megabytes. Refer to discordgo.File for information.
38 | func (c *Context) ReplyComplex(message string, tts bool, embed *discordgo.MessageEmbed, files []*discordgo.File) (*discordgo.Message, error) {
39 | return c.Session.ChannelMessageSendComplex(c.Channel.ID, &discordgo.MessageSend{
40 | Content: message,
41 | Embed: embed,
42 | TTS: tts,
43 | Files: files,
44 | })
45 | }
46 |
47 | // ReplyEmbed directly replies with a embed, but not with a message.
48 | //
49 | // Parameters:
50 | // embed - The embed for this message. Refer to discordgo.MessageEmbed for more info.
51 | func (c *Context) ReplyEmbed(embed *discordgo.MessageEmbed) (*discordgo.Message, error) {
52 | return c.Session.ChannelMessageSendEmbed(c.Channel.ID, embed)
53 | }
54 |
55 | // ReplyFile directly replies with a file, but not with a message.
56 | //
57 | // Parameters:
58 | // files - The files to send across. These (collectively) cannot pass more than 8 Megabytes.
59 | func (c *Context) ReplyFile(filename string, file io.Reader) (*discordgo.Message, error) {
60 | return c.Session.ChannelFileSend(c.Channel.ID, filename, file)
61 | }
62 |
--------------------------------------------------------------------------------
/errors.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2021 MikeModder/MikeModder007, apfel
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 | //
5 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 | package anpan
10 |
11 | import "errors"
12 |
13 | /* errors.go:
14 | * Error definitions.
15 | *
16 | * anpan (c) 2019-2021 MikeModder/MikeModder007, apfel
17 | */
18 |
19 | var (
20 | // ErrBotBlocked is thrown when the message handler encounters a bot, but ignoring bots was set to true.
21 | ErrBotBlocked = errors.New("anpan: The given author was a bot and the IgnoreBots setting is true")
22 |
23 | // ErrCommandAlreadyRegistered is thrown when a command by the same name was registered previously.
24 | ErrCommandAlreadyRegistered = errors.New("anpan: Another command was already registered by this name")
25 |
26 | // ErrCommandNotFound is thrown when a message tries to invoke an unknown command, or when an attempt at removing an unregistered command was made.
27 | ErrCommandNotFound = errors.New("anpan: Command not found")
28 |
29 | // ErrDataUnavailable is thrown when data is unavailable, like channels, users or something else.
30 | ErrDataUnavailable = errors.New("anpan: Necessary data couldn't be fetched")
31 |
32 | // ErrDMOnly is thrown when a DM-only command is executed on a guild.
33 | ErrDMOnly = errors.New("anpan: DM-Only command on guild")
34 |
35 | // ErrGuildOnly is thrown when a guild-only command is executed in direct messages.
36 | ErrGuildOnly = errors.New("anpan: Guild-Only command in DMs")
37 |
38 | // ErrOwnerOnly is thrown when an owner-only command is executed.
39 | ErrOwnerOnly = errors.New("anpan: Owner-Only command")
40 |
41 | // ErrSelfInsufficientPermissions is thrown when the bot itself does not have enough permissions.
42 | ErrSelfInsufficientPermissions = errors.New("anpan: Insufficient permissions for the bot")
43 |
44 | // ErrUserInsufficientPermissions is thrown when the user doesn't meet the required permissions.
45 | ErrUserInsufficientPermissions = errors.New("anpan: Insufficient permissions for the user")
46 | )
47 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 | This is an example bot, to give new users an easy way into anpan (and maybe discordgo, too).
3 |
--------------------------------------------------------------------------------
/example/help.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "sort"
6 | "strings"
7 |
8 | "github.com/MikeModder/anpan"
9 | "github.com/bwmarrin/discordgo"
10 | )
11 |
12 | func helpCommand(context anpan.Context, args []string, commands []*anpan.Command, prefixes []string) error {
13 | // This is a check for a command to make sure only appropriate commands are shown.
14 | // TODO: Add a permission check
15 | typeCheck := func(chn discordgo.ChannelType, cmd anpan.CommandType) bool {
16 | switch cmd {
17 | case anpan.CommandTypeEverywhere:
18 | return true
19 |
20 | case anpan.CommandTypePrivate:
21 | if chn == discordgo.ChannelTypeDM {
22 | return true
23 | }
24 |
25 | case anpan.CommandTypeGuild:
26 | if chn == discordgo.ChannelTypeGuildText {
27 | return true
28 | }
29 | }
30 |
31 | return false
32 | }
33 |
34 | // Here we found out that the user inquires for one specific command.
35 | if len(args) >= 1 {
36 | for _, command := range commands {
37 | // If this command isn't it, continue searching.
38 | if args[0] != command.Name {
39 | continue
40 | }
41 |
42 | // If this command is supposed to stay hidden, or if this isn't the correct place, stop the command.
43 | if command.Hidden || !typeCheck(context.Channel.Type, command.Type) {
44 | return nil
45 | }
46 |
47 | // Some useful declarations.
48 | var (
49 | owneronlystring = "No"
50 | typestring = "Anywhere"
51 | )
52 |
53 | // Is this command only accessible by the owners?
54 | if command.OwnerOnly {
55 | owneronlystring = "Yes"
56 | }
57 |
58 | // What type do we actually have?
59 | switch command.Type {
60 | case anpan.CommandTypePrivate:
61 | typestring = "Private"
62 |
63 | case anpan.CommandTypeGuild:
64 | typestring = "Guild-only"
65 | }
66 |
67 | // Time to tell the user about the prefixes.
68 | prefixesBuilder := strings.Builder{}
69 | if len(prefixes) == 1 {
70 | prefixesBuilder.WriteString(fmt.Sprintf("The prefix is %s", prefixes[0]))
71 | } else {
72 | prefixesBuilder.WriteString("The prefixes are ")
73 |
74 | for i, prefix := range prefixes {
75 | if i+1 == len(prefixes) {
76 | prefixesBuilder.WriteString(fmt.Sprintf("and %s", prefix))
77 | } else {
78 | prefixesBuilder.WriteString(fmt.Sprintf("%s, ", prefix))
79 | }
80 | }
81 | }
82 |
83 | // Maybe our command has a few nicknames...
84 | aliases := "**None.**"
85 | if len(command.Aliases) > 0 {
86 | aliases = strings.Join(command.Aliases, "`, `")
87 | aliases = "`" + aliases + "`"
88 | }
89 |
90 | // ..but anyway! Time to return the message.
91 | _, err := context.ReplyEmbed(&discordgo.MessageEmbed{
92 | Title: "Help",
93 | Color: 0x08a4ff,
94 | Description: fmt.Sprintf("**%s**\nAliases: %s\nOwner only: **%s**\nUsable: **%s**", command.Description, aliases, owneronlystring, typestring),
95 | Footer: &discordgo.MessageEmbedFooter{
96 | Text: fmt.Sprintf(" %s.", prefixesBuilder.String()),
97 | },
98 | })
99 |
100 | // We're done.
101 | return err
102 | }
103 |
104 | // We've not found anything :(
105 | _, err := context.Reply("Command `" + args[0] + "` doesn't exist.")
106 | return err
107 | }
108 |
109 | // Well, we now know the user wants to know what commands we actually have.
110 | var (
111 | count int
112 | commandsSorted = make([]*anpan.Command, len(commands))
113 | embed = &discordgo.MessageEmbed{
114 | Title: "Commands",
115 | Color: 0x08a4ff,
116 | }
117 | names = make([]string, len(commands))
118 | )
119 |
120 | // Get all names...
121 | for i, cmd := range commands {
122 | names[i] = cmd.Name
123 | }
124 |
125 | // ...sort them alphabetically...
126 | sort.Strings(names)
127 |
128 | // ...and arrange the commands accordingly.
129 | for i, v := range names {
130 | for _, v2 := range commands {
131 | if v2.Name == v {
132 | commandsSorted[i] = v2
133 | break
134 | }
135 | }
136 |
137 | if commandsSorted[i] == nil {
138 | return fmt.Errorf("sort failure")
139 | }
140 | }
141 |
142 | // Now that we've sorted the commands, we can show them to the user.
143 | for _, cmd := range commandsSorted {
144 | if !cmd.Hidden && typeCheck(context.Channel.Type, cmd.Type) {
145 | embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{
146 | Name: cmd.Name,
147 | Value: cmd.Description,
148 | Inline: count%2 == 0,
149 | })
150 |
151 | count++
152 | }
153 | }
154 |
155 | // We want a footer for additional information.
156 | var footer strings.Builder
157 |
158 | // How many commands do we have?
159 | if count == 1 {
160 | footer.WriteString("There is 1 command.")
161 | } else {
162 | footer.WriteString(fmt.Sprintf("There are %d commands.", count))
163 | }
164 |
165 | footer.WriteString(" | ")
166 |
167 | if len(prefixes) == 1 {
168 | footer.WriteString(fmt.Sprintf("The prefix is %s.", prefixes[0]))
169 | } else {
170 | prefixesBuilder := strings.Builder{}
171 |
172 | for i, prefix := range prefixes {
173 | if i+1 == len(prefixes) {
174 | prefixesBuilder.WriteString(fmt.Sprintf("and %s", prefix))
175 | } else {
176 | prefixesBuilder.WriteString(fmt.Sprintf("%s, ", prefix))
177 | }
178 | }
179 |
180 | footer.WriteString(fmt.Sprintf("The prefixes are %s.", prefixesBuilder.String()))
181 | }
182 |
183 | // Let them know about the prefixes.
184 | embed.Footer = &discordgo.MessageEmbedFooter{Text: footer.String()}
185 |
186 | // Time to give them help.
187 | _, err := context.ReplyEmbed(embed)
188 | return err
189 | }
190 |
--------------------------------------------------------------------------------
/example/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/MikeModder/anpan"
8 | "github.com/bwmarrin/discordgo"
9 | )
10 |
11 | func exampleOnErrorFunc(context anpan.Context, command *anpan.Command, content []string, err error) {
12 | if err == anpan.ErrCommandNotFound {
13 | return
14 | }
15 |
16 | fmt.Printf("An error occurred for command \"%s\": \"%s\".\n", command.Name, err.Error())
17 | }
18 |
19 | func examplePrerunFunc(context anpan.Context, command *anpan.Command, content []string) bool {
20 | fmt.Printf("Command \"%s\" is being run by \"%s#%s\" (ID: %s).\n", command.Name, context.User.Username, context.User.Discriminator, context.User.ID)
21 | return true
22 | }
23 |
24 | func main() {
25 | fmt.Println("Example bot for anpan.\nVersion 1.2.0.\nInitializing...")
26 |
27 | // Here we create an appropriate client.
28 | client, err := discordgo.New("Bot ")
29 | if err != nil {
30 | fmt.Printf("Creating a session failed: \"%s\".\n", err.Error())
31 | return
32 | }
33 |
34 | // In here we create a handler with the supplied data...
35 | handler := anpan.New([]string{"e!"}, []string{"your id", "another one"}, client.StateEnabled, true, true, true, examplePrerunFunc, exampleOnErrorFunc, nil)
36 | client.AddHandler(handler.MessageHandler)
37 |
38 | // ...then we register a command...
39 | handler.AddCommand("ping", "Check the bot's ping.", []string{"pong"}, false, false, discordgo.PermissionSendMessages, discordgo.PermissionSendMessages, anpan.CommandTypeEverywhere, pingCommand)
40 |
41 | // ...and a help command.
42 | handler.SetHelpCommand("help", []string{}, discordgo.PermissionSendMessages, discordgo.PermissionSendMessages, helpCommand)
43 |
44 | // Now, time to connect...
45 | if err = client.Open(); err != nil {
46 | fmt.Printf("Opening the session failed: \"%s\".\n", err.Error())
47 | return
48 | }
49 |
50 | // ...and wait until we need to exit.
51 | anpan.WaitForInterrupt()
52 |
53 | // Now we close the client, assuming it's still open.
54 | fmt.Println("Shutting down.")
55 | if err := client.Close(); err != nil {
56 | fmt.Printf("Closing the session failed. \"%s\". Ignoring.\n", err.Error())
57 | }
58 |
59 | // And we're done!
60 | }
61 |
62 | func pingCommand(ctx anpan.Context, _ []string) error {
63 | // We need to know what time it is now.
64 | timestamp := time.Now()
65 |
66 | msg, err := ctx.Reply("Pong!")
67 | if err != nil {
68 | return err
69 | }
70 |
71 | // Now we can compare it to the current time to see how much time went away during the process of sending a message.
72 | _, err = ctx.Session.ChannelMessageEdit(ctx.Message.ChannelID, msg.ID, fmt.Sprintf("Pong! Ping took **%dms**!", time.Since(timestamp).Milliseconds()))
73 | return err
74 | }
75 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/MikeModder/anpan
2 |
3 | go 1.16
4 |
5 | require github.com/bwmarrin/discordgo v0.23.2
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
2 | github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
3 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
4 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
5 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
6 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
7 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
8 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
9 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
10 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
12 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
13 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14 | golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3 h1:T6tyxxvHMj2L1R2kZg0uNMpS8ZhB9lRa9XRGTCSA65w=
15 | golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
17 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
18 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
19 |
--------------------------------------------------------------------------------
/handler.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2021 MikeModder/MikeModder007, apfel
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 | //
5 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 | package anpan
10 |
11 | /* handler.go:
12 | * Contains the main code of the command handler.
13 | *
14 | * anpan (c) 2019-2021 MikeModder/MikeModder007, apfel
15 | */
16 |
17 | import (
18 | "errors"
19 | "fmt"
20 | "strings"
21 |
22 | "github.com/bwmarrin/discordgo"
23 | )
24 |
25 | // GetCheckPermissions returns whether the message handler checks the permissions or not.
26 | func (c *CommandHandler) GetCheckPermissions(enable bool) bool {
27 | return c.checkPermissions
28 | }
29 |
30 | // SetCheckPermissions sets whether to check for permissions or not.
31 | func (c *CommandHandler) SetCheckPermissions(enable bool) {
32 | c.checkPermissions = enable
33 | }
34 |
35 | // GetEnable returns whether the message handler is actually enabled or not.
36 | func (c *CommandHandler) GetEnable() bool {
37 | return c.enabled
38 | }
39 |
40 | // SetEnable sets whether the message handler shall doing its job.
41 | func (c *CommandHandler) SetEnable(enable bool) {
42 | c.enabled = enable
43 | }
44 |
45 | // GetIgnoreBots returns whether the bot ignores other users marked as bots or not.
46 | func (c *CommandHandler) GetIgnoreBots() bool {
47 | return c.ignoreBots
48 | }
49 |
50 | // SetIgnoreBots sets whether to ignore other bots or not.
51 | func (c *CommandHandler) SetIgnoreBots(enable bool) {
52 | c.ignoreBots = enable
53 | }
54 |
55 | // GetUseState returns whether the command handler uses the cached State of the session or not.
56 | func (c *CommandHandler) GetUseState() bool {
57 | return c.useState
58 | }
59 |
60 | // SetUseState sets whether the command handler should use the cached State of the session or not.
61 | func (c *CommandHandler) SetUseState(enable bool) {
62 | c.useState = enable
63 | }
64 |
65 | // AddPrefix adds a prefix to the handler.
66 | func (c *CommandHandler) AddPrefix(prefix string) {
67 | c.prefixes = append(c.prefixes, prefix)
68 | }
69 |
70 | // RemovePrefix removes the prefix from the handler, if it exists.
71 | func (c *CommandHandler) RemovePrefix(prefix string) {
72 | for i, v := range c.prefixes {
73 | if v == prefix {
74 | copy(c.prefixes[i:], c.prefixes[i+1:])
75 | c.prefixes[len(c.prefixes)-1] = ""
76 | c.prefixes = c.prefixes[:len(c.prefixes)-1]
77 |
78 | break
79 | }
80 | }
81 | }
82 |
83 | // GetAllPrefixes returns the current prefixes slice.
84 | func (c *CommandHandler) GetAllPrefixes() []string {
85 | return c.prefixes
86 | }
87 |
88 | // SetAllPrefixes overwrites all prefixes within the prefixes slice.
89 | func (c *CommandHandler) SetAllPrefixes(prefixes []string) {
90 | c.prefixes = prefixes
91 | }
92 |
93 | // ClearDebugFunc clears the Debug function
94 | // Refer to DebugFunc for more information.
95 | func (c *CommandHandler) ClearDebugFunc() {
96 | c.debugFunc = nil
97 | }
98 |
99 | // GetDebugFunc returns the current debugging function.
100 | // Refer to DebugFunc for more information.
101 | func (c *CommandHandler) GetDebugFunc() DebugFunc {
102 | return c.debugFunc
103 | }
104 |
105 | // SetDebugFunc sets the given debug function as the debugging function for the command handler.
106 | func (c *CommandHandler) SetDebugFunc(df DebugFunc) {
107 | c.debugFunc = df
108 | }
109 |
110 | // ClearOnErrorFunc removes the current OnError function.
111 | // Refer to OnErrorFunc for more details.
112 | func (c *CommandHandler) ClearOnErrorFunc() {
113 | c.onErrorFunc = nil
114 | }
115 |
116 | // GetOnErrorFunc returns the current OnError function.
117 | // Refer to OnErrorFunc for more details.
118 | func (c *CommandHandler) GetOnErrorFunc() OnErrorFunc {
119 | return c.onErrorFunc
120 | }
121 |
122 | // SetOnErrorFunc sets the supplied OnErrorFunc as the one to use.
123 | // Refer to OnErrorFunc for more details.
124 | func (c *CommandHandler) SetOnErrorFunc(oef OnErrorFunc) {
125 | c.onErrorFunc = oef
126 | }
127 |
128 | // ClearPrerunFunc removes the current PrerunFunc.
129 | // Refer to PrerunFunc for more info.
130 | func (c *CommandHandler) ClearPrerunFunc() {
131 | c.prerunFunc = func(_ Context, _ *Command, _ []string) bool {
132 | return true
133 | }
134 | }
135 |
136 | // GetPrerunFunc returns the current PrerunFunc.
137 | // Refer to PrerunFunc for more info.
138 | func (c *CommandHandler) GetPrerunFunc() PrerunFunc {
139 | return c.prerunFunc
140 | }
141 |
142 | // SetPrerunFunc sets the supplied PrerunFunc as the one to use.
143 | // Refer to PrerunFunc for more info.
144 | func (c *CommandHandler) SetPrerunFunc(prf PrerunFunc) {
145 | c.prerunFunc = prf
146 | }
147 |
148 | // AddCommand adds a command to the Commands map.
149 | //
150 | // Parameters:
151 | // name - The name of the this command.
152 | // description - The description for this command.
153 | // aliases - Additional aliases used for this command.
154 | // owneronly - Whether only owners can access this command or not.
155 | // hidden - Whether a help command should hide this command or not.
156 | // selfperms - The necessary permissions for this command. Set this to "0" if any level is fine.
157 | // userperms - The necessary permissions for the user to meet to use this command. Set this to "0" if any level is fine.
158 | // cmdtype - The appropriate command type for this command. Use this to limit commands to direct messages or guilds. Refer to CommandType for help.
159 | // function - The command itself. Refer to CommandFunc for help.
160 | //
161 | // Errors:
162 | // ErrCommandAlreadyRegistered -> There's already a (help) command with this name.
163 | func (c *CommandHandler) AddCommand(name, desc string, aliases []string, owneronly, hidden bool, selfperms, userperms int64, cmdtype CommandType, run CommandFunc) error {
164 | for _, v := range c.commands {
165 | if v.Name == name {
166 | return ErrCommandAlreadyRegistered
167 | }
168 | }
169 |
170 | if c.helpCommand != nil && c.helpCommand.Name == name {
171 | return ErrCommandAlreadyRegistered
172 | }
173 |
174 | c.commands = append(c.commands, &Command{
175 | Aliases: aliases,
176 | Description: desc,
177 | Hidden: hidden,
178 | Name: name,
179 | OwnerOnly: owneronly,
180 | SelfPermissions: selfperms,
181 | UserPermissions: userperms,
182 | Run: run,
183 | Type: cmdtype,
184 | })
185 |
186 | return nil
187 | }
188 |
189 | // RemoveCommand removes the supplied command from the command array by using its name.
190 | //
191 | // Errors:
192 | // ErrCommandNotFound -> The given name doesn't belong to any command.
193 | func (c *CommandHandler) RemoveCommand(name string) error {
194 | for i, v := range c.commands {
195 | if v.Name == name {
196 | copy(c.commands[i:], c.commands[i+1:])
197 | c.commands[len(c.commands)-1] = nil
198 | c.commands = c.commands[:len(c.commands)-1]
199 |
200 | return nil
201 | }
202 | }
203 |
204 | return ErrCommandNotFound
205 | }
206 |
207 | // GetHelpCommand returns the current set help command.
208 | // Refer to HelpCommandFunc for help.
209 | func (c *CommandHandler) GetHelpCommand() *HelpCommand {
210 | return c.helpCommand
211 | }
212 |
213 | // SetHelpCommand sets the help command.
214 | //
215 | // Parameters:
216 | // name - The name of the help command; this should be "help" under normal circumstances.
217 | // aliases - Additional aliases used for the help command.
218 | // selfperms - The necessary permissions for this help command. Set this to "0" if any level is fine.
219 | // userperms - The necessary permissions for the user to meet to use this help command. Set this to "0" if any level is fine.
220 | // function - The help command itself. Refer to HelpCommandFunc for help.
221 | //
222 | // Notes:
223 | // The command handler always checks for the help command first.
224 | //
225 | // Errors:
226 | // ErrCommandAlreadyRegistered -> There's already another command that has been registered with the same name.
227 | func (c *CommandHandler) SetHelpCommand(name string, aliases []string, selfperms, userperms int64, function HelpCommandFunc) error {
228 | for _, v := range c.commands {
229 | if v.Name == name {
230 | return ErrCommandAlreadyRegistered
231 | }
232 | }
233 |
234 | c.helpCommand = &HelpCommand{
235 | Aliases: aliases,
236 | Name: name,
237 | SelfPermissions: selfperms,
238 | UserPermissions: userperms,
239 | Run: function,
240 | }
241 |
242 | return nil
243 | }
244 |
245 | // ClearHelpCommand clears the current help command.
246 | func (c *CommandHandler) ClearHelpCommand() {
247 | c.helpCommand = nil
248 | }
249 |
250 | // AddOwner adds a user ID as an owner.
251 | func (c *CommandHandler) AddOwner(id string) {
252 | c.owners = append(c.owners, id)
253 | }
254 |
255 | // RemoveOwner removes a user ID from the owner list.
256 | func (c *CommandHandler) RemoveOwner(id string) {
257 | for i, v := range c.owners {
258 | if v == id {
259 | c.owners = append(c.owners[:i], c.owners[i+1:]...)
260 | break
261 | }
262 | }
263 | }
264 |
265 | // SetOwners overwrites the current owner list with the given one.
266 | func (c *CommandHandler) SetOwners(ids []string) {
267 | c.owners = ids
268 | }
269 |
270 | // GetOwners returns the current owner list.
271 | func (c *CommandHandler) GetOwners() []string {
272 | return c.owners
273 | }
274 |
275 | // IsOwner checks whether the given ID is set as an owner.
276 | func (c *CommandHandler) IsOwner(id string) bool {
277 | for _, o := range c.owners {
278 | if id == o {
279 | return true
280 | }
281 | }
282 |
283 | return false
284 | }
285 |
286 | func (c *CommandHandler) debugLog(format string, a ...interface{}) {
287 | if c.debugFunc != nil {
288 | c.debugFunc(fmt.Sprintf(format, a...))
289 | }
290 | }
291 |
292 | func (c *CommandHandler) throwError(context Context, command *Command, args []string, err error) {
293 | if c.onErrorFunc != nil {
294 | c.onErrorFunc(context, command, args, err)
295 | }
296 | }
297 |
298 | func permissionCheck(session *discordgo.Session, member *discordgo.Member, guild *discordgo.Guild, channel *discordgo.Channel, necessaryPermissions int64, useState bool) error {
299 | if necessaryPermissions == 0 {
300 | return nil
301 | }
302 |
303 | var permissions int64
304 |
305 | if member.User.ID == guild.OwnerID {
306 | return nil
307 | }
308 |
309 | permissions |= guild.Roles[0].Permissions // everyone role
310 |
311 | for _, roleID := range member.Roles {
312 | var (
313 | role *discordgo.Role
314 | err error
315 | )
316 |
317 | if session.StateEnabled && useState {
318 | role, err = session.State.Role(guild.ID, roleID)
319 | if err != nil {
320 | return err
321 | }
322 | } else {
323 | roles, err := session.GuildRoles(guild.ID)
324 | if err != nil {
325 | return err
326 | }
327 |
328 | for _, v := range roles {
329 | if v.ID == roleID {
330 | role = v
331 | break
332 | }
333 | }
334 |
335 | if role == nil {
336 | return ErrDataUnavailable
337 | }
338 | }
339 |
340 | if role.Permissions&discordgo.PermissionAdministrator == discordgo.PermissionAdministrator {
341 | return nil
342 | }
343 |
344 | permissions |= role.Permissions
345 | }
346 |
347 | for _, overwrite := range channel.PermissionOverwrites {
348 | if overwrite.ID == member.User.ID {
349 | permissions |= overwrite.Allow
350 | permissions = permissions &^ overwrite.Deny
351 | }
352 |
353 | for _, roleID := range member.Roles {
354 | if overwrite.ID == roleID {
355 | permissions |= overwrite.Allow
356 | permissions = permissions &^ overwrite.Deny
357 | }
358 | }
359 | }
360 |
361 | if permissions&necessaryPermissions != necessaryPermissions {
362 | return errors.New("anpan: Insufficient permissions")
363 | }
364 |
365 | return nil
366 | }
367 |
368 | // MessageHandler handles incoming messages and runs commands.
369 | // Pass this to your Session's AddHandler function.
370 | func (c *CommandHandler) MessageHandler(s *discordgo.Session, event *discordgo.MessageCreate) {
371 | if !c.enabled || event.Author.ID == s.State.User.ID {
372 | return
373 | }
374 |
375 | c.debugLog("Received message (%s) by user \"%s\" (%s): \"%s\"", event.Message.ID, event.Author.String(), event.Author.ID, event.Message.Content)
376 |
377 | var (
378 | has bool
379 | prefix string
380 |
381 | command *Command
382 | help *HelpCommand
383 |
384 | content = []string{}
385 |
386 | context Context
387 |
388 | channel *discordgo.Channel
389 | guild *discordgo.Guild
390 | member *discordgo.Member
391 | selfMember *discordgo.Member
392 |
393 | err error
394 | )
395 |
396 | context.Handler = c
397 | context.Message = event.Message
398 | context.Session = s
399 | context.User = event.Author
400 |
401 | for i := 0; i < len(c.prefixes); i++ {
402 | prefix = c.prefixes[i]
403 |
404 | if strings.HasPrefix(event.Message.Content, prefix) {
405 | has = true
406 | content = strings.Split(strings.TrimPrefix(event.Message.Content, prefix), " ")
407 | break
408 | }
409 | }
410 |
411 | if !has && c.respondToPings {
412 | mention := ""
413 | if c.useState {
414 | mention = "<@!" + s.State.User.ID + ">" // discordgo doesn't seem to work right with Mention() here
415 | } else {
416 | user, err := s.User("@me")
417 | if err != nil {
418 | c.debugLog("Failed to fetch the current application: \"%s\"", err.Error())
419 | c.throwError(context, &Command{
420 | Name: c.helpCommand.Name,
421 | SelfPermissions: c.helpCommand.SelfPermissions,
422 | UserPermissions: c.helpCommand.UserPermissions,
423 | Type: CommandTypeEverywhere,
424 | }, nil, ErrDataUnavailable)
425 | return
426 | }
427 |
428 | mention = "<@!" + user.ID + ">"
429 | }
430 |
431 | if strings.HasPrefix(event.Message.Content, mention) {
432 | has = true
433 | content = strings.Split(strings.TrimPrefix(event.Message.Content, mention), " ")[1:] // TODO: why this is needed is beyond me at the moment, figure this out
434 | }
435 | }
436 |
437 | if !has {
438 | c.debugLog("Message %s doesn't contain any of the prefixes", event.Message.ID)
439 | return
440 | }
441 |
442 | if len(content) == 0 || content[0] == "" {
443 | c.debugLog("Message %s was empty", event.Message.ID)
444 | return
445 | }
446 |
447 | c.debugLog("Parsed message \"%s\": \"%s\"", event.Message.ID, content)
448 |
449 | if content[0] == c.helpCommand.Name {
450 | help = c.helpCommand
451 | }
452 |
453 | if help == nil {
454 | for _, v := range c.helpCommand.Aliases {
455 | if content[0] == v {
456 | help = c.helpCommand
457 | }
458 | }
459 | }
460 |
461 | if help != nil {
462 | if channel, err = s.Channel(event.ChannelID); err != nil {
463 | c.debugLog("Failed to fetch the current channel: \"%s\"", err.Error())
464 | c.throwError(context, &Command{
465 | Name: c.helpCommand.Name,
466 | SelfPermissions: c.helpCommand.SelfPermissions,
467 | UserPermissions: c.helpCommand.UserPermissions,
468 | Type: CommandTypeEverywhere,
469 | }, content[1:], ErrDataUnavailable)
470 | return
471 | }
472 |
473 | context.Channel = channel
474 |
475 | if channel.Type == discordgo.ChannelTypeDM {
476 | if c.prerunFunc != nil && !c.prerunFunc(context, &Command{
477 | Name: c.helpCommand.Name,
478 | SelfPermissions: c.helpCommand.SelfPermissions,
479 | UserPermissions: c.helpCommand.UserPermissions,
480 | Type: CommandTypeEverywhere,
481 | }, content[1:]) {
482 | return
483 | }
484 |
485 | if err = help.Run(context, content[1:], c.commands, c.prefixes); err != nil {
486 | c.throwError(context, &Command{
487 | Name: c.helpCommand.Name,
488 | SelfPermissions: c.helpCommand.SelfPermissions,
489 | UserPermissions: c.helpCommand.UserPermissions,
490 | Type: CommandTypeEverywhere,
491 | }, content[1:], err)
492 | }
493 |
494 | return
495 | }
496 |
497 | if guild, err = s.Guild(event.GuildID); err != nil {
498 | c.debugLog("Failed to fetch the current guild: \"%s\"", err.Error())
499 | c.throwError(context, &Command{
500 | Name: c.helpCommand.Name,
501 | SelfPermissions: c.helpCommand.SelfPermissions,
502 | UserPermissions: c.helpCommand.UserPermissions,
503 | Type: CommandTypeEverywhere,
504 | }, content[1:], ErrDataUnavailable)
505 | return
506 | }
507 |
508 | if member, err = s.GuildMember(event.GuildID, event.Author.ID); err != nil {
509 | c.debugLog("Failed to fetch the user as a guild member: \"%s\"", err.Error())
510 | c.throwError(context, &Command{
511 | Name: c.helpCommand.Name,
512 | SelfPermissions: c.helpCommand.SelfPermissions,
513 | UserPermissions: c.helpCommand.UserPermissions,
514 | Type: CommandTypeEverywhere,
515 | }, content[1:], ErrDataUnavailable)
516 | return
517 | }
518 |
519 | context.Guild = guild
520 | context.Member = member
521 |
522 | if selfMember, err = s.GuildMember(event.GuildID, s.State.User.ID); err != nil {
523 | c.debugLog("Failed to fetch the bot as a guild member: \"%s\"", err.Error())
524 | c.throwError(context, command, content[1:], ErrDataUnavailable)
525 | return
526 | }
527 |
528 | if c.checkPermissions {
529 | if err = permissionCheck(s, member, guild, channel, help.UserPermissions, c.useState); err != nil {
530 | c.debugLog("Insufficient permissions (user): \"%s\"", err.Error())
531 | c.throwError(context, command, content[1:], ErrUserInsufficientPermissions)
532 | return
533 | }
534 |
535 | if err = permissionCheck(s, selfMember, guild, channel, help.SelfPermissions, c.useState); err != nil {
536 | c.debugLog("Insufficient permissions (bot): \"%s\"", err.Error())
537 | c.throwError(context, command, content[1:], ErrUserInsufficientPermissions)
538 | return
539 | }
540 | }
541 |
542 | if c.prerunFunc != nil && !c.prerunFunc(context, &Command{
543 | Name: c.helpCommand.Name,
544 | SelfPermissions: c.helpCommand.SelfPermissions,
545 | UserPermissions: c.helpCommand.UserPermissions,
546 | Type: CommandTypeEverywhere,
547 | }, content[1:]) {
548 | return
549 | }
550 |
551 | if err = help.Run(context, content[1:], c.commands, c.prefixes); err != nil {
552 | c.throwError(context, command, content[1:], err)
553 | }
554 |
555 | return
556 | }
557 |
558 | for _, v := range c.commands {
559 | if content[0] == v.Name {
560 | command = v
561 | break
562 | }
563 |
564 | for _, alias := range v.Aliases {
565 | if content[0] == alias {
566 | command = v
567 | break
568 | }
569 | }
570 | }
571 |
572 | if command == nil {
573 | c.throwError(context, nil, content[1:], ErrCommandNotFound)
574 | return
575 | }
576 |
577 | if command.OwnerOnly && !c.IsOwner(event.Author.ID) {
578 | c.debugLog("The user tried to run an owner-only command")
579 | c.throwError(context, command, content[1:], ErrOwnerOnly)
580 | return
581 | }
582 |
583 | if channel, err = s.Channel(event.ChannelID); err != nil {
584 | c.debugLog("Failed to fetch the current channel: \"%s\"", err.Error())
585 | c.throwError(context, command, content[1:], ErrDataUnavailable)
586 | return
587 | }
588 |
589 | context.Channel = channel
590 |
591 | if channel.Type == discordgo.ChannelTypeDM {
592 | if command.Type == CommandTypeGuild {
593 | c.debugLog("The user tried to execute a guild-only command in the DMs")
594 | c.throwError(context, command, content[1:], ErrGuildOnly)
595 | return
596 | }
597 |
598 | if c.prerunFunc != nil && !c.prerunFunc(context, command, content[1:]) {
599 | return
600 | }
601 |
602 | if err = command.Run(context, content[1:]); err != nil {
603 | c.throwError(context, command, content[1:], err)
604 | }
605 |
606 | return
607 | }
608 |
609 | if guild, err = s.Guild(event.GuildID); err != nil {
610 | c.debugLog("Failed to fetch the current guild: \"%s\"", err.Error())
611 | c.throwError(context, command, content[1:], ErrDataUnavailable)
612 | return
613 | }
614 |
615 | if member, err = s.GuildMember(event.GuildID, event.Author.ID); err != nil {
616 | c.debugLog("Failed to fetch the user as a guild member: \"%s\"", err.Error())
617 | c.throwError(context, command, content[1:], ErrDataUnavailable)
618 | return
619 | }
620 |
621 | if command.Type == CommandTypePrivate && guild != nil {
622 | c.debugLog("The user tried to execute a DM-only command outside the DMs")
623 | c.throwError(context, command, content[1:], ErrDMOnly)
624 | return
625 | }
626 |
627 | if command.Type == CommandTypeGuild && guild == nil {
628 | c.debugLog("The user tried to execute a guild-only command outside a guild")
629 | c.throwError(context, command, content[1:], ErrGuildOnly)
630 | return
631 | }
632 |
633 | context.Guild = guild
634 | context.Member = member
635 |
636 | if selfMember, err = s.GuildMember(event.GuildID, s.State.User.ID); err != nil {
637 | c.debugLog("Failed to fetch the bot as a guild member: \"%s\"", err.Error())
638 | c.throwError(context, command, content[1:], ErrDataUnavailable)
639 | return
640 | }
641 |
642 | if c.checkPermissions {
643 | if err = permissionCheck(s, member, guild, channel, command.UserPermissions, c.useState); err != nil {
644 | c.debugLog("Insufficient permissions (user): \"%s\"", err.Error())
645 | c.throwError(context, command, content[1:], ErrUserInsufficientPermissions)
646 | return
647 | }
648 |
649 | if err = permissionCheck(s, selfMember, guild, channel, command.SelfPermissions, c.useState); err != nil {
650 | c.debugLog("Insufficient permissions (bot): \"%s\"", err.Error())
651 | c.throwError(context, command, content[1:], ErrUserInsufficientPermissions)
652 | return
653 | }
654 | }
655 |
656 | if c.prerunFunc != nil && !c.prerunFunc(context, command, content[1:]) {
657 | return
658 | }
659 |
660 | if err = command.Run(context, content[1:]); err != nil {
661 | c.throwError(context, command, content[1:], err)
662 | }
663 | }
664 |
--------------------------------------------------------------------------------
/types.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019-2021 MikeModder/MikeModder007, apfel
2 | //
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 | //
5 | // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 | //
7 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
9 | package anpan
10 |
11 | /* types.go:
12 | * Declares all types used for and within anpan.
13 | *
14 | * anpan (c) 2019-2021 MikeModder/MikeModder007, apfel
15 | */
16 |
17 | import "github.com/bwmarrin/discordgo"
18 |
19 | // CommandFunc defines a normal command's function.
20 | //
21 | // Parameters:
22 | // Context -> The context supplied by the command handler. Refer to Context for help.
23 | // []string -> The arguments sent along with the command, basically the rest of the message after the command and the prefix. Note that this is split by spaces.
24 | type CommandFunc func(Context, []string) error
25 |
26 | // DebugFunc is used for debugging output.
27 | //
28 | // Parameters:
29 | // string -> The returned message.
30 | type DebugFunc func(string)
31 |
32 | // HelpCommandFunc defines a help command's function.
33 | // Context -> The context supplied by the command handler. Refer to Context for help.
34 | // []string -> The arguments sent along with the command, basically the rest of the message after the command and the prefix. Note that this is split by spaces. This can be used to show help for a specific command.
35 | // []*Command -> The command slice, containing all commands.
36 | // []string -> The prefixes used by the command handler.
37 | type HelpCommandFunc func(Context, []string, []*Command, []string) error
38 |
39 | // PrerunFunc is the type for the function that can be run before command execution.
40 | // It is executed after any permission checks and the likes but run before the command itself.
41 | // If all goes well, return true. otherwise, false.
42 | //
43 | // Parameters:
44 | // Context -> The supplied content.
45 | // *Command -> The command that is about to be executed.
46 | // []string -> The arguments sent along with the command, basically the rest of the message after the command and the prefix. Note that this is split by spaces. This can be used to show help for a specific command.
47 | //
48 | // Notes:
49 | // This is executed before the actual command, unless the guild object is not nil, then it's run before the permission check.
50 | type PrerunFunc func(Context, *Command, []string) bool
51 |
52 | // OnErrorFunc is the type for the function that can be run.
53 | //
54 | // Parameters:
55 | // Context -> The context supplied by the command handler. Refer to Context for help.
56 | // *Command -> The command in question.
57 | // []string -> The arguments sent along with the command, basically the rest of the message after the command and the prefix. Note that this is split by spaces. This can be used to show help for a specific command.
58 | // error -> The returned error.
59 | type OnErrorFunc func(Context, *Command, []string, error)
60 |
61 | // CommandType defines where commands can be used.
62 | type CommandType int
63 |
64 | // HelpCommand defines a help command.
65 | // Refer to SetHelpCommand for help.
66 | type HelpCommand struct {
67 | Aliases []string
68 | Name string
69 | SelfPermissions int64
70 | UserPermissions int64
71 | Run HelpCommandFunc
72 | }
73 |
74 | // CommandHandler contains all the data needed for the handler to function.
75 | // Anything inside here must be controlled with the appropriate Get/Set/Remove function.
76 | type CommandHandler struct {
77 | enabled bool
78 | checkPermissions bool
79 | ignoreBots bool
80 | respondToPings bool
81 | useState bool
82 |
83 | owners []string
84 | prefixes []string
85 |
86 | commands []*Command
87 | helpCommand *HelpCommand
88 |
89 | debugFunc DebugFunc
90 | onErrorFunc OnErrorFunc
91 | prerunFunc PrerunFunc
92 | }
93 |
94 | // Command represents a command.
95 | // Refer to AddCommand for help.
96 | type Command struct {
97 | Aliases []string
98 | Description string
99 | Name string
100 |
101 | Hidden bool
102 | OwnerOnly bool
103 |
104 | SelfPermissions int64
105 | UserPermissions int64
106 |
107 | Run CommandFunc
108 |
109 | Type CommandType
110 | }
111 |
112 | // Context holds the data required for command execution.
113 | type Context struct {
114 | // Handler represents the handler on which this command was registered on.
115 | Handler *CommandHandler
116 |
117 | // Channel defines the channel in which the command has been executed.
118 | Channel *discordgo.Channel
119 |
120 | // Guild defines the guild in which the command has been executed.
121 | // Note that this may be nil under certain circumstances.
122 | Guild *discordgo.Guild
123 |
124 | // Member defines the member in the guild in which the command has been executed.
125 | // Note that, if guild is nil, this will be nil too.
126 | Member *discordgo.Member
127 |
128 | // Message defines the message that has executed this command.
129 | Message *discordgo.Message
130 |
131 | // Session defines the session that this command handler run on top of.
132 | Session *discordgo.Session
133 |
134 | // User defines the user that has executed the command.
135 | User *discordgo.User
136 | }
137 |
138 | const (
139 | // CommandTypeEverywhere defines a command that can be executed anywhere.
140 | CommandTypeEverywhere CommandType = iota
141 |
142 | // CommandTypeGuild defines a command that cannot be executed in direct messages.
143 | CommandTypeGuild
144 |
145 | // CommandTypePrivate defines a command that cannot be executed in a guild.
146 | CommandTypePrivate
147 | )
148 |
--------------------------------------------------------------------------------