├── .gitignore ├── middleware.go ├── go.mod ├── go.sum ├── LICENSE ├── context.go ├── objectsMap.go ├── rateLimiter.go ├── utils.go ├── examples ├── arguments.go └── basic.go ├── README.md ├── command.go ├── router.go ├── help.go └── arguments.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | cmd/ 3 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | // Middleware defines how a middleware looks like 4 | type Middleware func(following ExecutionHandler) ExecutionHandler 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lus/dgc 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.20.3 7 | github.com/karrick/tparse/v2 v2.8.1 8 | github.com/zekroTJA/timedmap v0.0.0-20200518230343-de9b879d109a 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bwmarrin/discordgo v0.20.3 h1:AxjcHGbyBFSC0a3Zx5nDQwbOjU7xai5dXjRnZ0YB7nU= 2 | github.com/bwmarrin/discordgo v0.20.3/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= 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/karrick/tparse/v2 v2.8.1 h1:KmuwSutle9SVDQceHWr1IixyMZ0RSctDP/OB+OhqMNs= 6 | github.com/karrick/tparse/v2 v2.8.1/go.mod h1:OzmKMqNal7LYYHaO/Ie1f/wXmLWAaGKwJmxUFNQCVxg= 7 | github.com/zekroTJA/timedmap v0.0.0-20200518230343-de9b879d109a h1:S8UfZYz3TbkwO8bZ1UE3miiEjh/KSi4tZYwX1A/omC4= 8 | github.com/zekroTJA/timedmap v0.0.0-20200518230343-de9b879d109a/go.mod h1:ktlw5aYhoXQvOvWFL9SzltGXn1bQgJXxZzHJK4wQvsI= 9 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= 10 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lukas SP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import "github.com/bwmarrin/discordgo" 4 | 5 | // Ctx represents the context for a command event 6 | type Ctx struct { 7 | Session *discordgo.Session 8 | Event *discordgo.MessageCreate 9 | Arguments *Arguments 10 | CustomObjects *ObjectsMap 11 | Router *Router 12 | Command *Command 13 | } 14 | 15 | // ExecutionHandler represents a handler for a context execution 16 | type ExecutionHandler func(*Ctx) 17 | 18 | // RespondText responds with the given text message 19 | func (ctx *Ctx) RespondText(text string) error { 20 | _, err := ctx.Session.ChannelMessageSend(ctx.Event.ChannelID, text) 21 | return err 22 | } 23 | 24 | // RespondEmbed responds with the given embed message 25 | func (ctx *Ctx) RespondEmbed(embed *discordgo.MessageEmbed) error { 26 | _, err := ctx.Session.ChannelMessageSendEmbed(ctx.Event.ChannelID, embed) 27 | return err 28 | } 29 | 30 | // RespondTextEmbed responds with the given text and embed message 31 | func (ctx *Ctx) RespondTextEmbed(text string, embed *discordgo.MessageEmbed) error { 32 | _, err := ctx.Session.ChannelMessageSendComplex(ctx.Event.ChannelID, &discordgo.MessageSend{ 33 | Content: text, 34 | Embed: embed, 35 | }) 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /objectsMap.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import "sync" 4 | 5 | // ObjectsMap wraps a map[string]interface 6 | // to provide thread safe access endpoints. 7 | type ObjectsMap struct { 8 | mutex sync.RWMutex 9 | innerMap map[string]interface{} 10 | } 11 | 12 | // newObjectsMap initializes a new 13 | // ObjectsMap instance 14 | func newObjectsMap() *ObjectsMap { 15 | return &ObjectsMap{ 16 | innerMap: make(map[string]interface{}), 17 | } 18 | } 19 | 20 | // Get tries to get a value from the map. 21 | // If a value was found, the value and true 22 | // is returned. Else, nil and false is 23 | // returned. 24 | func (om *ObjectsMap) Get(key string) (interface{}, bool) { 25 | om.mutex.RLock() 26 | defer om.mutex.RUnlock() 27 | 28 | v, ok := om.innerMap[key] 29 | return v, ok 30 | } 31 | 32 | // MustGet wraps Get but only returns the 33 | // value, if found, or nil otherwise. 34 | func (om *ObjectsMap) MustGet(key string) interface{} { 35 | v, ok := om.Get(key) 36 | if !ok { 37 | return nil 38 | } 39 | return v 40 | } 41 | 42 | // Set sets a value to the map by key. 43 | func (om *ObjectsMap) Set(key string, val interface{}) { 44 | om.mutex.Lock() 45 | defer om.mutex.Unlock() 46 | 47 | om.innerMap[key] = val 48 | } 49 | 50 | // Delete removes a key-value pair from the map. 51 | func (om *ObjectsMap) Delete(key string) { 52 | om.mutex.Lock() 53 | defer om.mutex.Unlock() 54 | 55 | delete(om.innerMap, key) 56 | } 57 | -------------------------------------------------------------------------------- /rateLimiter.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/zekroTJA/timedmap" 7 | ) 8 | 9 | // RateLimiter represents a general rate limiter 10 | type RateLimiter interface { 11 | NotifyExecution(*Ctx) bool 12 | } 13 | 14 | // DefaultRateLimiter represents an internal rate limiter 15 | type DefaultRateLimiter struct { 16 | Cooldown time.Duration 17 | RateLimitedHandler ExecutionHandler 18 | executions *timedmap.TimedMap 19 | } 20 | 21 | // NewRateLimiter creates a new rate limiter 22 | func NewRateLimiter(cooldown, cleanupInterval time.Duration, onRateLimited ExecutionHandler) RateLimiter { 23 | return &DefaultRateLimiter{ 24 | Cooldown: cooldown, 25 | RateLimitedHandler: onRateLimited, 26 | executions: timedmap.New(cleanupInterval), 27 | } 28 | } 29 | 30 | // NotifyExecution notifies the rate limiter about a new execution and returns whether or not the execution is allowed 31 | func (rateLimiter *DefaultRateLimiter) NotifyExecution(ctx *Ctx) bool { 32 | if rateLimiter.executions.Contains(ctx.Event.Author.ID) { 33 | if rateLimiter.RateLimitedHandler != nil { 34 | nextExecution, err := rateLimiter.executions.GetExpires(ctx.Event.Author.ID) 35 | if err != nil { 36 | ctx.CustomObjects.Set("dgc_nextExecution", nextExecution) 37 | } 38 | rateLimiter.RateLimitedHandler(ctx) 39 | } 40 | return false 41 | } 42 | rateLimiter.executions.Set(ctx.Event.Author.ID, time.Now().UnixNano()/1e6, rateLimiter.Cooldown) 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import "strings" 4 | 5 | // stringHasPrefix checks whether or not the string contains one of the given prefixes and returns the string without the prefix 6 | func stringHasPrefix(str string, prefixes []string, ignoreCase bool) (bool, string) { 7 | for _, prefix := range prefixes { 8 | stringToCheck := str 9 | if ignoreCase { 10 | stringToCheck = strings.ToLower(stringToCheck) 11 | prefix = strings.ToLower(prefix) 12 | } 13 | if strings.HasPrefix(stringToCheck, prefix) { 14 | return true, string(str[len(prefix):]) 15 | } 16 | } 17 | return false, str 18 | } 19 | 20 | // stringTrimPreSuffix returns the string without the defined pre- and suffix 21 | func stringTrimPreSuffix(str string, preSuffix string) string { 22 | if !(strings.HasPrefix(str, preSuffix) && strings.HasSuffix(str, preSuffix)) { 23 | return str 24 | } 25 | return strings.TrimPrefix(strings.TrimSuffix(str, preSuffix), preSuffix) 26 | } 27 | 28 | // equals provides a simple method to check whether or not 2 strings are equal 29 | func equals(str1, str2 string, ignoreCase bool) bool { 30 | if !ignoreCase { 31 | return str1 == str2 32 | } 33 | return strings.ToLower(str1) == strings.ToLower(str2) 34 | } 35 | 36 | // stringArrayContains checks whether or not the given string array contains the given string 37 | func stringArrayContains(array []string, str string, ignoreCase bool) bool { 38 | if ignoreCase { 39 | str = strings.ToLower(str) 40 | } 41 | for _, value := range array { 42 | if ignoreCase { 43 | value = strings.ToLower(value) 44 | } 45 | if value == str { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | -------------------------------------------------------------------------------- /examples/arguments.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lus/dgc" 7 | ) 8 | 9 | // This example shows how to use the integrated argument parser 10 | 11 | func someCommandHandler(ctx *dgc.Ctx) { 12 | // First of all, get the arguments object 13 | arguments := ctx.Arguments 14 | 15 | // Print the amount of arguments into the console 16 | amount := arguments.Amount() 17 | fmt.Println("Amount:", amount) 18 | 19 | // Print the raw argument string into the console 20 | raw := arguments.Raw() 21 | fmt.Println("Raw:", raw) 22 | 23 | // Parse it into a codeblock struct 24 | codeblock := arguments.AsCodeblock() 25 | if codeblock == nil { 26 | // Arguments aren't a codeblock 27 | } 28 | fmt.Println("Codeblock Language:", codeblock.Language) 29 | fmt.Println("Codeblock Content:", codeblock.Content) 30 | 31 | // Get the first argument 32 | argument := arguments.Get(0) 33 | 34 | // Parse it into an integer 35 | // HINT: You can also use the argument.AsInt64 method 36 | integer, err := argument.AsInt() 37 | if err != nil { 38 | // Argument is no integer 39 | } 40 | fmt.Println("Int:", integer) 41 | 42 | // Parse it into a boolean 43 | boolean, err := argument.AsBool() 44 | if err != nil { 45 | // Argument is no bool 46 | } 47 | fmt.Println("Bool:", boolean) 48 | 49 | // Parse it into a user ID if it is an user mention 50 | // HINT: You can also use the argument.AsRoleMentionID and argument.AsChannelMentionID methods 51 | userID := argument.AsUserMentionID() 52 | if userID == "" { 53 | // Argument is no user mention 54 | } 55 | fmt.Println("User ID:", userID) 56 | 57 | // Parse it into a duration 58 | dur, err := argument.AsDuration() 59 | if err != nil { 60 | // Argument is no duration 61 | } 62 | fmt.Println("Duration:", dur.String()) 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/Lukaesebrot/dgc) 2 | 3 | # dgc 4 | 5 | A DiscordGo command router with tons of useful features 6 | If you find any bugs or if you have a feature request, please tell me using an issue. 7 | 8 | ## Deprecation notice 9 | 10 | After thinking a lot about how to continue this project, I decided I won't. 11 | The main reason for this is that Discord will make the message content intent privileged in 2022 and thus will force every bot developer to use slash commands. 12 | I don't feel motivated to continue a project which will become pretty much redundant in the next months, especially because a major rewrite of some features would be mandatory for me as I started the project at the beginning of my Go learning period and I evolved a lot since then. 13 | I think I could use the time much better for new, cooler projects. 14 | 15 | ## Basic example 16 | 17 | This just shows a very basic example: 18 | ```go 19 | func main() { 20 | // Discord bot logic here 21 | session, _ := ... 22 | 23 | // Create a new command router 24 | router := dgc.Create(&dgc.Router{ 25 | Prefixes: []string{"!"}, 26 | }) 27 | 28 | // Register a simple ping command 29 | router.RegisterCmd(&dgc.Command{ 30 | Name: "ping", 31 | Description: "Responds with 'pong!'", 32 | Usage: "ping", 33 | Example: "ping", 34 | IgnoreCase: true, 35 | Handler: func(ctx *dgc.Ctx) { 36 | ctx.RespondText("Pong!") 37 | }, 38 | }) 39 | 40 | // Initialize the router 41 | router.Initialize(session) 42 | } 43 | ``` 44 | 45 | ## Usage 46 | 47 | You can find examples for the more complex usage and all the integrated features in the `examples/*.go` files. 48 | 49 | ## Arguments 50 | 51 | To find out how you can use the integrated argument parser, just look into the `examples/arguments.go` file. 52 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Command represents a simple command 8 | type Command struct { 9 | Name string 10 | Aliases []string 11 | Description string 12 | Usage string 13 | Example string 14 | Flags []string 15 | IgnoreCase bool 16 | SubCommands []*Command 17 | RateLimiter RateLimiter 18 | Handler ExecutionHandler 19 | } 20 | 21 | // GetSubCmd returns the sub command with the given name if it exists 22 | func (command *Command) GetSubCmd(name string) *Command { 23 | // Loop through all commands to find the correct one 24 | for _, subCommand := range command.SubCommands { 25 | // Define the slice to check 26 | toCheck := make([]string, len(subCommand.Aliases)+1) 27 | toCheck = append(toCheck, subCommand.Name) 28 | toCheck = append(toCheck, subCommand.Aliases...) 29 | 30 | // Check the prefix of the string 31 | if stringArrayContains(toCheck, name, subCommand.IgnoreCase) { 32 | return subCommand 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | // NotifyRateLimiter notifies the rate limiter about a new execution and returns false if the user is being rate limited 39 | func (command *Command) NotifyRateLimiter(ctx *Ctx) bool { 40 | if command.RateLimiter == nil { 41 | return true 42 | } 43 | return command.RateLimiter.NotifyExecution(ctx) 44 | } 45 | 46 | // trigger triggers the given command 47 | func (command *Command) trigger(ctx *Ctx) { 48 | // Check if the first argument matches a sub command 49 | if len(ctx.Arguments.arguments) > 0 { 50 | argument := ctx.Arguments.Get(0).Raw() 51 | subCommand := command.GetSubCmd(argument) 52 | if subCommand != nil { 53 | // Define the arguments for the sub command 54 | arguments := ParseArguments("") 55 | if ctx.Arguments.Amount() > 1 { 56 | arguments = ParseArguments(strings.Join(strings.Split(ctx.Arguments.Raw(), " ")[1:], " ")) 57 | } 58 | 59 | // Trigger the sub command 60 | subCommand.trigger(&Ctx{ 61 | Session: ctx.Session, 62 | Event: ctx.Event, 63 | Arguments: arguments, 64 | CustomObjects: ctx.CustomObjects, 65 | Router: ctx.Router, 66 | Command: subCommand, 67 | }) 68 | return 69 | } 70 | } 71 | 72 | // Prepare all middlewares 73 | nextHandler := command.Handler 74 | for _, middleware := range ctx.Router.Middlewares { 75 | nextHandler = middleware(nextHandler) 76 | } 77 | 78 | // Run all middlewares 79 | nextHandler(ctx) 80 | } 81 | -------------------------------------------------------------------------------- /examples/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "strconv" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/bwmarrin/discordgo" 12 | "github.com/lus/dgc" 13 | ) 14 | 15 | func main() { 16 | // Open a simple Discord session 17 | token := os.Getenv("TOKEN") 18 | session, err := discordgo.New("Bot " + token) 19 | if err != nil { 20 | panic(err) 21 | } 22 | err = session.Open() 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | // Wait for the user to cancel the process 28 | defer func() { 29 | sc := make(chan os.Signal, 1) 30 | signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) 31 | <-sc 32 | }() 33 | 34 | // Create a dgc router 35 | // NOTE: The dgc.Create function makes sure all the maps get initialized 36 | router := dgc.Create(&dgc.Router{ 37 | // We will allow '!' and 'example!' as the bot prefixes 38 | Prefixes: []string{ 39 | "!", 40 | "example!", 41 | }, 42 | 43 | // We will ignore the prefix case, so 'eXaMpLe!' is also a valid prefix 44 | IgnorePrefixCase: true, 45 | 46 | // We don't want bots to be able to execute our commands 47 | BotsAllowed: false, 48 | 49 | // We may initialize our commands in here, but we will use the corresponding method later on 50 | Commands: []*dgc.Command{}, 51 | 52 | // We may inject our middlewares in here, but we will also use the corresponding method later on 53 | Middlewares: []dgc.Middleware{}, 54 | 55 | // This handler gets called if the bot just got pinged (no argument provided) 56 | PingHandler: func(ctx *dgc.Ctx) { 57 | ctx.RespondText("Pong!") 58 | }, 59 | }) 60 | 61 | // Register the default help command 62 | router.RegisterDefaultHelpCommand(session, nil) 63 | 64 | // Register a simple middleware that injects a custom object 65 | router.RegisterMiddleware(func(next dgc.ExecutionHandler) dgc.ExecutionHandler { 66 | return func(ctx *dgc.Ctx) { 67 | // Inject a custom object into the context 68 | ctx.CustomObjects.Set("myObject", 69) 69 | 70 | // You can retrieve the object like this 71 | obj := ctx.CustomObjects.MustGet("myObject").(int) 72 | fmt.Println(obj) 73 | 74 | // Call the next execution handler 75 | next(ctx) 76 | } 77 | }) 78 | 79 | // Register a simple command that responds with our custom object 80 | router.RegisterCmd(&dgc.Command{ 81 | // We want to use 'obj' as the primary name of the command 82 | Name: "obj", 83 | 84 | // We also want the command to get triggered with the 'object' alias 85 | Aliases: []string{ 86 | "object", 87 | }, 88 | 89 | // These fields get displayed in the default help messages 90 | Description: "Responds with the injected custom object", 91 | Usage: "obj", 92 | Example: "obj", 93 | 94 | // You can assign custom flags to a command to use them in middlewares 95 | Flags: []string{}, 96 | 97 | // We want to ignore the command case 98 | IgnoreCase: true, 99 | 100 | // You may define sub commands in here 101 | SubCommands: []*dgc.Command{}, 102 | 103 | // We want the user to be able to execute this command once in five seconds and the cleanup interval shpuld be one second 104 | RateLimiter: dgc.NewRateLimiter(5*time.Second, 1*time.Second, func(ctx *dgc.Ctx) { 105 | ctx.RespondText("You are being rate limited!") 106 | }), 107 | 108 | // Now we want to define the command handler 109 | Handler: objCommand, 110 | }) 111 | 112 | // Initialize the router 113 | router.Initialize(session) 114 | } 115 | 116 | func objCommand(ctx *dgc.Ctx) { 117 | // Respond with the just set custom object 118 | ctx.RespondText(strconv.Itoa(ctx.CustomObjects.MustGet("myObject").(int))) 119 | } 120 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/bwmarrin/discordgo" 8 | ) 9 | 10 | // regexSplitting represents the regex to split the arguments at 11 | var regexSplitting = regexp.MustCompile("\\s+") 12 | 13 | // Router represents a DiscordGo command router 14 | type Router struct { 15 | Prefixes []string 16 | IgnorePrefixCase bool 17 | BotsAllowed bool 18 | Commands []*Command 19 | Middlewares []Middleware 20 | PingHandler ExecutionHandler 21 | Storage map[string]*ObjectsMap 22 | } 23 | 24 | // Create makes sure all maps get initialized 25 | func Create(router *Router) *Router { 26 | router.Storage = make(map[string]*ObjectsMap) 27 | return router 28 | } 29 | 30 | // RegisterCmd registers a new command 31 | func (router *Router) RegisterCmd(command *Command) { 32 | router.Commands = append(router.Commands, command) 33 | } 34 | 35 | // GetCmd returns the command with the given name if it exists 36 | func (router *Router) GetCmd(name string) *Command { 37 | // Loop through all commands to find the correct one 38 | for _, command := range router.Commands { 39 | // Define the slice to check 40 | toCheck := make([]string, len(command.Aliases)+1) 41 | toCheck = append(toCheck, command.Name) 42 | toCheck = append(toCheck, command.Aliases...) 43 | 44 | // Check the prefix of the string 45 | if stringArrayContains(toCheck, name, command.IgnoreCase) { 46 | return command 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | // RegisterMiddleware registers a new middleware 53 | func (router *Router) RegisterMiddleware(middleware Middleware) { 54 | router.Middlewares = append(router.Middlewares, middleware) 55 | } 56 | 57 | // InitializeStorage initializes a storage map 58 | func (router *Router) InitializeStorage(name string) { 59 | router.Storage[name] = newObjectsMap() 60 | } 61 | 62 | // Initialize initializes the message event listener 63 | func (router *Router) Initialize(session *discordgo.Session) { 64 | session.AddHandler(router.Handler()) 65 | } 66 | 67 | // Handler provides the discordgo handler for the given router 68 | func (router *Router) Handler() func(*discordgo.Session, *discordgo.MessageCreate) { 69 | return func(session *discordgo.Session, event *discordgo.MessageCreate) { 70 | // Define useful variables 71 | message := event.Message 72 | content := message.Content 73 | 74 | // Check if the message was sent by a bot 75 | if message.Author.Bot && !router.BotsAllowed { 76 | return 77 | } 78 | 79 | // Execute the ping handler if the message equals the current bot's mention 80 | if (content == "<@!"+session.State.User.ID+">" || content == "<@"+session.State.User.ID+">") && router.PingHandler != nil { 81 | router.PingHandler(&Ctx{ 82 | Session: session, 83 | Event: event, 84 | Arguments: ParseArguments(""), 85 | Router: router, 86 | }) 87 | return 88 | } 89 | 90 | // Check if the message starts with one of the defined prefixes 91 | hasPrefix, content := stringHasPrefix(content, router.Prefixes, router.IgnorePrefixCase) 92 | if !hasPrefix { 93 | return 94 | } 95 | 96 | // Get rid of additional spaces 97 | content = strings.Trim(content, " ") 98 | 99 | // Check if the message is empty after the prefix processing 100 | if content == "" { 101 | return 102 | } 103 | 104 | // Split the messages at any whitespace 105 | parts := regexSplitting.Split(content, -1) 106 | 107 | // Check if the message starts with a command name 108 | for _, command := range router.Commands { 109 | // Check if the first part is the current command 110 | if !stringArrayContains(getIdentifiers(command), parts[0], command.IgnoreCase) { 111 | continue 112 | } 113 | content = strings.Join(parts[1:], " ") 114 | 115 | // Define the command context 116 | ctx := &Ctx{ 117 | Session: session, 118 | Event: event, 119 | Arguments: ParseArguments(content), 120 | CustomObjects: newObjectsMap(), 121 | Router: router, 122 | Command: command, 123 | } 124 | 125 | // Trigger the command 126 | command.trigger(ctx) 127 | } 128 | } 129 | } 130 | 131 | func getIdentifiers(command *Command) []string { 132 | // Define an array containing the commands name and the aliases 133 | toCheck := make([]string, len(command.Aliases)+1) 134 | toCheck = append(toCheck, command.Name) 135 | toCheck = append(toCheck, command.Aliases...) 136 | return toCheck 137 | } 138 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/bwmarrin/discordgo" 10 | ) 11 | 12 | // RegisterDefaultHelpCommand registers the default help command 13 | func (router *Router) RegisterDefaultHelpCommand(session *discordgo.Session, rateLimiter RateLimiter) { 14 | // Initialize the helo messages storage 15 | router.InitializeStorage("dgc_helpMessages") 16 | 17 | // Initialize the reaction add listener 18 | session.AddHandler(func(session *discordgo.Session, event *discordgo.MessageReactionAdd) { 19 | // Define useful variables 20 | channelID := event.ChannelID 21 | messageID := event.MessageID 22 | userID := event.UserID 23 | 24 | // Check whether or not the reaction was added by the bot itself 25 | if event.UserID == session.State.User.ID { 26 | return 27 | } 28 | 29 | // Check whether or not the message is a help message 30 | rawPage, ok := router.Storage["dgc_helpMessages"].Get(channelID + ":" + messageID + ":" + event.UserID) 31 | if !ok { 32 | return 33 | } 34 | page := rawPage.(int) 35 | if page <= 0 { 36 | return 37 | } 38 | 39 | // Check which reaction was added 40 | reactionName := event.Emoji.Name 41 | switch reactionName { 42 | case "⬅️": 43 | // Update the help message 44 | embed, newPage := renderDefaultGeneralHelpEmbed(router, page-1) 45 | page = newPage 46 | session.ChannelMessageEditEmbed(channelID, messageID, embed) 47 | 48 | // Remove the reaction 49 | session.MessageReactionRemove(channelID, messageID, reactionName, userID) 50 | break 51 | case "❌": 52 | // Delete the help message 53 | session.ChannelMessageDelete(channelID, messageID) 54 | break 55 | case "➡️": 56 | // Update the help message 57 | embed, newPage := renderDefaultGeneralHelpEmbed(router, page+1) 58 | page = newPage 59 | session.ChannelMessageEditEmbed(channelID, messageID, embed) 60 | 61 | // Remove the reaction 62 | session.MessageReactionRemove(channelID, messageID, reactionName, userID) 63 | break 64 | } 65 | 66 | // Update the stores page 67 | router.Storage["dgc_helpMessages"].Set(channelID+":"+messageID+":"+event.UserID, page) 68 | }) 69 | 70 | // Register the default help command 71 | router.RegisterCmd(&Command{ 72 | Name: "help", 73 | Description: "Lists all the available commands or displays some information about a specific command", 74 | Usage: "help [command name]", 75 | Example: "help yourCommand", 76 | IgnoreCase: true, 77 | RateLimiter: rateLimiter, 78 | Handler: generalHelpCommand, 79 | }) 80 | } 81 | 82 | // generalHelpCommand handles the general help command 83 | func generalHelpCommand(ctx *Ctx) { 84 | // Check if the user provided an argument 85 | if ctx.Arguments.Amount() > 0 { 86 | specificHelpCommand(ctx) 87 | return 88 | } 89 | 90 | // Define useful variables 91 | channelID := ctx.Event.ChannelID 92 | session := ctx.Session 93 | 94 | // Send the general help embed 95 | embed, _ := renderDefaultGeneralHelpEmbed(ctx.Router, 1) 96 | message, _ := ctx.Session.ChannelMessageSendEmbed(channelID, embed) 97 | 98 | // Add the reactions to the message 99 | session.MessageReactionAdd(channelID, message.ID, "⬅️") 100 | session.MessageReactionAdd(channelID, message.ID, "❌") 101 | session.MessageReactionAdd(channelID, message.ID, "➡️") 102 | 103 | // Define the message as a help message 104 | ctx.Router.Storage["dgc_helpMessages"].Set(channelID+":"+message.ID+":"+ctx.Event.Author.ID, 1) 105 | } 106 | 107 | // specificHelpCommand handles the specific help command 108 | func specificHelpCommand(ctx *Ctx) { 109 | // Define the command names 110 | commandNames := strings.Split(ctx.Arguments.Raw(), " ") 111 | 112 | // Define the command 113 | var command *Command 114 | for index, commandName := range commandNames { 115 | if index == 0 { 116 | command = ctx.Router.GetCmd(commandName) 117 | continue 118 | } 119 | if command == nil { 120 | break 121 | } 122 | command = command.GetSubCmd(commandName) 123 | } 124 | 125 | // Send the help embed 126 | ctx.Session.ChannelMessageSendEmbed(ctx.Event.ChannelID, renderDefaultSpecificHelpEmbed(ctx, command)) 127 | } 128 | 129 | // renderDefaultGeneralHelpEmbed renders the general help embed on the given page 130 | func renderDefaultGeneralHelpEmbed(router *Router, page int) (*discordgo.MessageEmbed, int) { 131 | // Define useful variables 132 | commands := router.Commands 133 | prefix := router.Prefixes[0] 134 | 135 | // Calculate the amount of pages 136 | pageAmount := int(math.Ceil(float64(len(commands)) / 5)) 137 | if page > pageAmount { 138 | page = pageAmount 139 | } 140 | if page <= 0 { 141 | page = 1 142 | } 143 | 144 | // Calculate the slice of commands to display on this page 145 | startingIndex := (page - 1) * 5 146 | endingIndex := startingIndex + 5 147 | if page == pageAmount { 148 | endingIndex = len(commands) 149 | } 150 | displayCommands := commands[startingIndex:endingIndex] 151 | 152 | // Prepare the fields for the embed 153 | fields := make([]*discordgo.MessageEmbedField, len(displayCommands)) 154 | for index, command := range displayCommands { 155 | fields[index] = &discordgo.MessageEmbedField{ 156 | Name: command.Name, 157 | Value: "`" + command.Description + "`", 158 | Inline: false, 159 | } 160 | } 161 | 162 | // Return the embed and the new page 163 | return &discordgo.MessageEmbed{ 164 | Type: "rich", 165 | Title: "Command List (Page " + strconv.Itoa(page) + "/" + strconv.Itoa(pageAmount) + ")", 166 | Description: "These are all the available commands. Type `" + prefix + "help ` to find out more about a specific command.", 167 | Timestamp: time.Now().Format(time.RFC3339), 168 | Color: 0xffff00, 169 | Fields: fields, 170 | }, page 171 | } 172 | 173 | // renderDefaultSpecificHelpEmbed renders the specific help embed of the given command 174 | func renderDefaultSpecificHelpEmbed(ctx *Ctx, command *Command) *discordgo.MessageEmbed { 175 | // Define useful variables 176 | prefix := ctx.Router.Prefixes[0] 177 | 178 | // Check if the command is invalid 179 | if command == nil { 180 | return &discordgo.MessageEmbed{ 181 | Type: "rich", 182 | Title: "Error", 183 | Timestamp: time.Now().Format(time.RFC3339), 184 | Color: 0xff0000, 185 | Fields: []*discordgo.MessageEmbedField{ 186 | { 187 | Name: "Message", 188 | Value: "```The given command doesn't exist. Type `" + prefix + "help` for a list of available commands.```", 189 | Inline: false, 190 | }, 191 | }, 192 | } 193 | } 194 | 195 | // Define the sub commands string 196 | subCommands := "No sub commands" 197 | if len(command.SubCommands) > 0 { 198 | subCommandNames := make([]string, len(command.SubCommands)) 199 | for index, subCommand := range command.SubCommands { 200 | subCommandNames[index] = subCommand.Name 201 | } 202 | subCommands = "`" + strings.Join(subCommandNames, "`, `") + "`" 203 | } 204 | 205 | // Define the aliases string 206 | aliases := "No aliases" 207 | if len(command.Aliases) > 0 { 208 | aliases = "`" + strings.Join(command.Aliases, "`, `") + "`" 209 | } 210 | 211 | // Return the embed 212 | return &discordgo.MessageEmbed{ 213 | Type: "rich", 214 | Title: "Command Information", 215 | Description: "Displaying the information for the `" + command.Name + "` command.", 216 | Timestamp: time.Now().Format(time.RFC3339), 217 | Color: 0xffff00, 218 | Fields: []*discordgo.MessageEmbedField{ 219 | { 220 | Name: "Name", 221 | Value: "`" + command.Name + "`", 222 | Inline: false, 223 | }, 224 | { 225 | Name: "Sub Commands", 226 | Value: subCommands, 227 | Inline: false, 228 | }, 229 | { 230 | Name: "Aliases", 231 | Value: aliases, 232 | Inline: false, 233 | }, 234 | { 235 | Name: "Description", 236 | Value: "```" + command.Description + "```", 237 | Inline: false, 238 | }, 239 | { 240 | Name: "Usage", 241 | Value: "```" + prefix + command.Usage + "```", 242 | Inline: false, 243 | }, 244 | { 245 | Name: "Example", 246 | Value: "```" + prefix + command.Example + "```", 247 | Inline: false, 248 | }, 249 | }, 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /arguments.go: -------------------------------------------------------------------------------- 1 | package dgc 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/karrick/tparse/v2" 10 | ) 11 | 12 | var ( 13 | // RegexArguments defines the regex the argument string has to match 14 | RegexArguments = regexp.MustCompile("(\"[^\"]+\"|[^\\s]+)") 15 | 16 | // RegexUserMention defines the regex a user mention has to match 17 | RegexUserMention = regexp.MustCompile("<@!?(\\d+)>") 18 | 19 | // RegexRoleMention defines the regex a role mention has to match 20 | RegexRoleMention = regexp.MustCompile("<@&(\\d+)>") 21 | 22 | // RegexChannelMention defines the regex a channel mention has to match 23 | RegexChannelMention = regexp.MustCompile("<#(\\d+)>") 24 | 25 | // RegexBigCodeblock defines the regex a big codeblock has to match 26 | RegexBigCodeblock = regexp.MustCompile("(?s)\\n*```(?:([\\w.\\-]*)\\n)?(.*)```") 27 | 28 | // RegexSmallCodeblock defines the regex a small codeblock has to match 29 | RegexSmallCodeblock = regexp.MustCompile("(?s)\\n*`(.*)`") 30 | 31 | // CodeblockLanguages defines which languages are valid codeblock languages 32 | CodeblockLanguages = []string{ 33 | "as", 34 | "1c", 35 | "abnf", 36 | "accesslog", 37 | "actionscript", 38 | "ada", 39 | "ado", 40 | "adoc", 41 | "apache", 42 | "apacheconf", 43 | "applescript", 44 | "arduino", 45 | "arm", 46 | "armasm", 47 | "asciidoc", 48 | "aspectj", 49 | "atom", 50 | "autohotkey", 51 | "autoit", 52 | "avrasm", 53 | "awk", 54 | "axapta", 55 | "bash", 56 | "basic", 57 | "bat", 58 | "bf", 59 | "bind", 60 | "bnf", 61 | "brainfuck", 62 | "c", 63 | "c++", 64 | "cal", 65 | "capnp", 66 | "capnproto", 67 | "cc", 68 | "ceylon", 69 | "clean", 70 | "clj", 71 | "clojure-repl", 72 | "clojure", 73 | "cls", 74 | "cmake.in", 75 | "cmake", 76 | "cmd", 77 | "coffee", 78 | "coffeescript", 79 | "console", 80 | "coq", 81 | "cos", 82 | "cpp", 83 | "cr", 84 | "craftcms", 85 | "crm", 86 | "crmsh", 87 | "crystal", 88 | "cs", 89 | "csharp", 90 | "cson", 91 | "csp", 92 | "css", 93 | "d", 94 | "dart", 95 | "dcl", 96 | "delphi", 97 | "dfm", 98 | "diff", 99 | "django", 100 | "dns", 101 | "do", 102 | "docker", 103 | "dockerfile", 104 | "dos", 105 | "dpr", 106 | "dsconfig", 107 | "dst", 108 | "dts", 109 | "dust", 110 | "ebnf", 111 | "elixir", 112 | "elm", 113 | "erb", 114 | "erl", 115 | "erlang-repl", 116 | "erlang", 117 | "excel", 118 | "f90", 119 | "f95", 120 | "feature", 121 | "fix", 122 | "flix", 123 | "fortran", 124 | "freepascal", 125 | "fs", 126 | "fsharp", 127 | "gams", 128 | "gauss", 129 | "gcode", 130 | "gemspec", 131 | "gherkin", 132 | "glsl", 133 | "gms", 134 | "go", 135 | "golang", 136 | "golo", 137 | "gradle", 138 | "graph", 139 | "groovy", 140 | "gss", 141 | "gyp", 142 | "h", 143 | "h++", 144 | "haml", 145 | "handlebars", 146 | "haskell", 147 | "haxe", 148 | "hbs", 149 | "hpp", 150 | "hs", 151 | "hsp", 152 | "html.handlebars", 153 | "html.hbs", 154 | "html", 155 | "htmlbars", 156 | "http", 157 | "https", 158 | "hx", 159 | "hy", 160 | "hylang", 161 | "i7", 162 | "iced", 163 | "icl", 164 | "inform7", 165 | "ini", 166 | "instances", 167 | "irb", 168 | "irpf90", 169 | "java", 170 | "javascript", 171 | "jboss-cli", 172 | "jinja", 173 | "js", 174 | "json", 175 | "jsp", 176 | "jsx", 177 | "julia", 178 | "k", 179 | "kdb", 180 | "kotlin", 181 | "lasso", 182 | "lassoscript", 183 | "lazarus", 184 | "ldif", 185 | "leaf", 186 | "less", 187 | "lfm", 188 | "lisp", 189 | "livecodeserver", 190 | "livescript", 191 | "llvm", 192 | "lpr", 193 | "ls", 194 | "lsl", 195 | "lua", 196 | "m", 197 | "mak", 198 | "makefile", 199 | "markdown", 200 | "mathematica", 201 | "matlab", 202 | "maxima", 203 | "md", 204 | "mel", 205 | "mercury", 206 | "mips", 207 | "mipsasm", 208 | "mizar", 209 | "mk", 210 | "mkd", 211 | "mkdown", 212 | "ml", 213 | "mm", 214 | "mma", 215 | "mojolicious", 216 | "monkey", 217 | "moo", 218 | "moon", 219 | "moonscript", 220 | "n1ql", 221 | "nc", 222 | "nginx", 223 | "nginxconf", 224 | "nim", 225 | "nimrod", 226 | "nix", 227 | "nixos", 228 | "nsis", 229 | "obj-c", 230 | "objc", 231 | "objectivec", 232 | "ocaml", 233 | "openscad", 234 | "osascript", 235 | "oxygene", 236 | "p21", 237 | "parser3", 238 | "pas", 239 | "pascal", 240 | "patch", 241 | "pb", 242 | "pbi", 243 | "pcmk", 244 | "perl", 245 | "pf.conf", 246 | "pf", 247 | "php", 248 | "php3", 249 | "php4", 250 | "php5", 251 | "php6", 252 | "pl", 253 | "plist", 254 | "pm", 255 | "podspec", 256 | "pony", 257 | "powershell", 258 | "pp", 259 | "processing", 260 | "profile", 261 | "prolog", 262 | "protobuf", 263 | "ps", 264 | "puppet", 265 | "purebasic", 266 | "py", 267 | "python", 268 | "q", 269 | "qml", 270 | "qt", 271 | "r", 272 | "rb", 273 | "rib", 274 | "roboconf", 275 | "rs", 276 | "rsl", 277 | "rss", 278 | "ruby", 279 | "ruleslanguage", 280 | "rust", 281 | "scad", 282 | "scala", 283 | "scheme", 284 | "sci", 285 | "scilab", 286 | "scss", 287 | "sh", 288 | "shell", 289 | "smali", 290 | "smalltalk", 291 | "sml", 292 | "sqf", 293 | "sql", 294 | "st", 295 | "stan", 296 | "stata", 297 | "step", 298 | "step21", 299 | "stp", 300 | "styl", 301 | "stylus", 302 | "subunit", 303 | "sv", 304 | "svh", 305 | "swift", 306 | "taggerscript", 307 | "tao", 308 | "tap", 309 | "tcl", 310 | "tex", 311 | "thor", 312 | "thrift", 313 | "tk", 314 | "toml", 315 | "tp", 316 | "ts", 317 | "twig", 318 | "typescript", 319 | "v", 320 | "vala", 321 | "vb", 322 | "vbnet", 323 | "vbs", 324 | "vbscript-html", 325 | "vbscript", 326 | "verilog", 327 | "vhdl", 328 | "vim", 329 | "wildfly-cli", 330 | "x86asm", 331 | "xhtml", 332 | "xjb", 333 | "xl", 334 | "xls", 335 | "xlsx", 336 | "xml", 337 | "xpath", 338 | "xq", 339 | "xquery", 340 | "xsd", 341 | "xsl", 342 | "yaml", 343 | "yml", 344 | "zep", 345 | "zephir", 346 | "zone", 347 | "zsh", 348 | } 349 | ) 350 | 351 | // Arguments represents the arguments that may be used in a command context 352 | type Arguments struct { 353 | raw string 354 | arguments []*Argument 355 | } 356 | 357 | // Codeblock represents a Discord codeblock 358 | type Codeblock struct { 359 | Language string 360 | Content string 361 | } 362 | 363 | // ParseArguments parses the raw string into several arguments 364 | func ParseArguments(raw string) *Arguments { 365 | // Define the raw arguments 366 | rawArguments := RegexArguments.FindAllString(raw, -1) 367 | arguments := make([]*Argument, len(rawArguments)) 368 | 369 | // Parse the raw arguments into correct ones 370 | for index, rawArgument := range rawArguments { 371 | rawArgument = stringTrimPreSuffix(rawArgument, "\"") 372 | arguments[index] = &Argument{ 373 | raw: rawArgument, 374 | } 375 | } 376 | 377 | // Return the arguments structure 378 | return &Arguments{ 379 | raw: raw, 380 | arguments: arguments, 381 | } 382 | } 383 | 384 | // Raw returns the raw string value of the arguments 385 | func (arguments *Arguments) Raw() string { 386 | return arguments.raw 387 | } 388 | 389 | // AsSingle parses the given arguments as a single one 390 | func (arguments *Arguments) AsSingle() *Argument { 391 | return &Argument{ 392 | raw: arguments.raw, 393 | } 394 | } 395 | 396 | // Amount returns the amount of given arguments 397 | func (arguments *Arguments) Amount() int { 398 | return len(arguments.arguments) 399 | } 400 | 401 | // Get returns the n'th argument 402 | func (arguments *Arguments) Get(n int) *Argument { 403 | if arguments.Amount() <= n { 404 | return &Argument{ 405 | raw: "", 406 | } 407 | } 408 | return arguments.arguments[n] 409 | } 410 | 411 | // Remove removes the n'th argument 412 | func (arguments *Arguments) Remove(n int) { 413 | // Check if the given index is valid 414 | if arguments.Amount() <= n { 415 | return 416 | } 417 | 418 | // Set the new argument slice 419 | arguments.arguments = append(arguments.arguments[:n], arguments.arguments[n+1:]...) 420 | 421 | // Set the new raw string 422 | raw := "" 423 | for _, argument := range arguments.arguments { 424 | raw += argument.raw + " " 425 | } 426 | arguments.raw = strings.TrimSpace(raw) 427 | } 428 | 429 | // AsCodeblock parses the given arguments as a codeblock 430 | func (arguments *Arguments) AsCodeblock() *Codeblock { 431 | raw := arguments.Raw() 432 | 433 | // Check if the raw string is a big codeblock 434 | matches := RegexBigCodeblock.MatchString(raw) 435 | if !matches { 436 | // Check if the raw string is a small codeblock 437 | matches = RegexSmallCodeblock.MatchString(raw) 438 | if matches { 439 | submatches := RegexSmallCodeblock.FindStringSubmatch(raw) 440 | return &Codeblock{ 441 | Language: "", 442 | Content: submatches[1], 443 | } 444 | } 445 | return nil 446 | } 447 | 448 | // Define the content and the language 449 | submatches := RegexBigCodeblock.FindStringSubmatch(raw) 450 | language := "" 451 | content := submatches[1] + submatches[2] 452 | if submatches[1] != "" && !stringArrayContains(CodeblockLanguages, submatches[1], false) { 453 | language = submatches[1] 454 | content = submatches[2] 455 | } 456 | 457 | // Return the codeblock 458 | return &Codeblock{ 459 | Language: language, 460 | Content: content, 461 | } 462 | } 463 | 464 | // Argument represents a single argument 465 | type Argument struct { 466 | raw string 467 | } 468 | 469 | // Raw returns the raw string value of the argument 470 | func (argument *Argument) Raw() string { 471 | return argument.raw 472 | } 473 | 474 | // AsBool parses the given argument into a boolean 475 | func (argument *Argument) AsBool() (bool, error) { 476 | return strconv.ParseBool(argument.raw) 477 | } 478 | 479 | // AsInt parses the given argument into an int32 480 | func (argument *Argument) AsInt() (int, error) { 481 | return strconv.Atoi(argument.raw) 482 | } 483 | 484 | // AsInt64 parses the given argument into an int64 485 | func (argument *Argument) AsInt64() (int64, error) { 486 | return strconv.ParseInt(argument.raw, 10, 64) 487 | } 488 | 489 | // AsUserMentionID returns the ID of the mentioned user or an empty string if it is no mention 490 | func (argument *Argument) AsUserMentionID() string { 491 | // Check if the argument is a user mention 492 | matches := RegexUserMention.MatchString(argument.raw) 493 | if !matches { 494 | return "" 495 | } 496 | 497 | // Parse the user ID 498 | userID := RegexUserMention.FindStringSubmatch(argument.raw)[1] 499 | return userID 500 | } 501 | 502 | // AsRoleMentionID returns the ID of the mentioned role or an empty string if it is no mention 503 | func (argument *Argument) AsRoleMentionID() string { 504 | // Check if the argument is a role mention 505 | matches := RegexRoleMention.MatchString(argument.raw) 506 | if !matches { 507 | return "" 508 | } 509 | 510 | // Parse the role ID 511 | roleID := RegexRoleMention.FindStringSubmatch(argument.raw)[1] 512 | return roleID 513 | } 514 | 515 | // AsChannelMentionID returns the ID of the mentioned channel or an empty string if it is no mention 516 | func (argument *Argument) AsChannelMentionID() string { 517 | // Check if the argument is a channel mention 518 | matches := RegexChannelMention.MatchString(argument.raw) 519 | if !matches { 520 | return "" 521 | } 522 | 523 | // Parse the channel ID 524 | channelID := RegexChannelMention.FindStringSubmatch(argument.raw)[1] 525 | return channelID 526 | } 527 | 528 | // AsDuration parses the given argument into a duration 529 | func (argument *Argument) AsDuration() (time.Duration, error) { 530 | return tparse.AbsoluteDuration(time.Now(), argument.raw) 531 | } 532 | --------------------------------------------------------------------------------