├── .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 | ![Actions results](https://github.com/MikeModder/anpan/actions/workflows/main.yml/badge.svg) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/MikeModder/anpan)](https://goreportcard.com/report/github.com/MikeModder/anpan) 3 | [![GoDoc](https://godoc.org/github.com/MikeModder/anpan?status.svg)](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 | --------------------------------------------------------------------------------