github.com/starshine-sys/bcr@v0.21.0/command.go (about) 1 package bcr 2 3 import ( 4 "flag" 5 "strings" 6 "sync" 7 "time" 8 9 "github.com/diamondburned/arikawa/v3/discord" 10 "github.com/spf13/pflag" 11 "github.com/starshine-sys/snowflake/v2" 12 ) 13 14 // CustomPerms is a custom permission checker 15 type CustomPerms interface { 16 // The string used for the permissions if the check fails 17 String() string 18 19 // Returns true if the user has permission to run the command 20 Check(Contexter) (bool, error) 21 } 22 23 // Command is a single command, or a group 24 type Command struct { 25 Name string 26 Aliases []string 27 28 // Blacklistable commands use the router's blacklist function to check if they can be run 29 Blacklistable bool 30 31 // Summary is used in the command list 32 Summary string 33 // Description is used in the help command 34 Description string 35 // Usage is appended to the command name in help commands 36 Usage string 37 38 // Hidden commands are not returned from (*Router).Commands() 39 Hidden bool 40 41 Args *Args 42 43 CustomPermissions CustomPerms 44 45 // Flags is used to create a new flag set, which is then parsed before the command is run. 46 // These can then be retrieved with the (*FlagSet).Get*() methods. 47 Flags func(fs *pflag.FlagSet) *pflag.FlagSet 48 49 // similar to Flags above but only used internally to mirror Options 50 stdFlags func(ctx *Context, fs *flag.FlagSet) (*Context, *flag.FlagSet) 51 52 subCmds map[string]*Command 53 subMu sync.RWMutex 54 55 // GuildPermissions is the required *global* permissions 56 GuildPermissions discord.Permissions 57 // Permissions is the required permissions in the *context channel* 58 Permissions discord.Permissions 59 GuildOnly bool 60 OwnerOnly bool 61 62 Command func(*Context) error 63 Cooldown time.Duration 64 65 // id is a unique ID. This is automatically generated on startup and is (pretty much) guaranteed to be unique *per session*. This ID will *not* be consistent between restarts. 66 id snowflake.Snowflake 67 68 // Executed when a slash command is executed, with a *SlashContext being passed in. 69 // Also executed when Command is nil, with a *Context being passed in instead. 70 SlashCommand func(Contexter) error 71 // If this is set and SlashCommand is nil, AddCommand *will panic!* 72 // Even if the command has no options, this should be set to an empty slice rather than nil. 73 Options *[]discord.CommandOption 74 } 75 76 // AddSubcommand adds a subcommand to a command 77 func (c *Command) AddSubcommand(sub *Command) *Command { 78 if c.Options != nil && c.SlashCommand == nil { 79 panic("command.Options set without command.SlashCommand being set") 80 } 81 82 if c.Options != nil && c.Command == nil { 83 c.stdFlags = func(ctx *Context, fs *flag.FlagSet) (*Context, *flag.FlagSet) { 84 for _, o := range *c.Options { 85 if o.Required { 86 continue 87 } 88 89 name := strings.ToLower(o.Name) 90 91 switch o.Type { 92 case discord.StringOption, discord.ChannelOption, discord.UserOption, discord.RoleOption, discord.MentionableOption: 93 v := fs.String(name, "", o.Description) 94 ctx.FlagMap[name] = v 95 case discord.IntegerOption: 96 v := fs.Int64(name, 0, o.Description) 97 ctx.FlagMap[name] = v 98 case discord.BooleanOption: 99 v := fs.Bool(name, false, o.Description) 100 ctx.FlagMap[name] = v 101 case discord.NumberOption: 102 v := fs.Float64(name, 0, o.Description) 103 ctx.FlagMap[name] = v 104 default: 105 ctx.Router.Logger.Error("invalid CommandOptionType set in command %v, option %v: %v", c.Name, o.Name, o.Type) 106 } 107 } 108 109 return ctx, fs 110 } 111 } 112 113 sub.id = sGen.Get() 114 c.subMu.Lock() 115 defer c.subMu.Unlock() 116 if c.subCmds == nil { 117 c.subCmds = make(map[string]*Command) 118 } 119 120 c.subCmds[strings.ToLower(sub.Name)] = sub 121 for _, a := range sub.Aliases { 122 c.subCmds[strings.ToLower(a)] = sub 123 } 124 125 return sub 126 } 127 128 // GetCommand gets a command by name 129 func (r *Router) GetCommand(name string) *Command { 130 r.cmdMu.RLock() 131 defer r.cmdMu.RUnlock() 132 if v, ok := r.cmds[strings.ToLower(name)]; ok { 133 return v 134 } 135 return nil 136 } 137 138 // GetCommand gets a command by name 139 func (c *Command) GetCommand(name string) *Command { 140 c.subMu.RLock() 141 defer c.subMu.RUnlock() 142 if v, ok := c.subCmds[strings.ToLower(name)]; ok { 143 return v 144 } 145 return nil 146 } 147 148 // Args is a minimum/maximum argument count. 149 // If either is -1, it's treated as "no minimum" or "no maximum". 150 // This replaces the Check* functions in Context. 151 type Args [2]int 152 153 // MinArgs returns an *Args with only a minimum number of arguments. 154 func MinArgs(i int) *Args { 155 return &Args{i, -1} 156 } 157 158 // MaxArgs returns an *Args with only a maximum number of arguments. 159 func MaxArgs(i int) *Args { 160 return &Args{-1, i} 161 } 162 163 // ArgRange returns an *Args with both a minimum and maximum number of arguments. 164 func ArgRange(i, j int) *Args { 165 return &Args{i, j} 166 } 167 168 // ExactArgs returns an *Args with an exact number of required arguments. 169 func ExactArgs(i int) *Args { 170 return &Args{i, i} 171 } 172 173 type requireRole struct { 174 name string 175 176 // owners override the role check 177 owners []discord.UserID 178 // any of these roles is required for the check to succeed 179 roles []discord.RoleID 180 } 181 182 var _ CustomPerms = (*requireRole)(nil) 183 184 func (r *requireRole) String() string { 185 return r.name 186 } 187 188 func (r *requireRole) Check(ctx Contexter) (bool, error) { 189 for _, u := range r.owners { 190 if u == ctx.User().ID { 191 return true, nil 192 } 193 } 194 195 if ctx.GetMember() == nil { 196 return false, nil 197 } 198 199 for _, r := range r.roles { 200 for _, mr := range ctx.GetMember().RoleIDs { 201 if r == mr { 202 return true, nil 203 } 204 } 205 } 206 207 return false, nil 208 } 209 210 // RequireRole returns a CustomPerms that requires the given roles. 211 // If any of r's owner IDs are not valid snowflakes, this function will panic! 212 func (r *Router) RequireRole(name string, roles ...discord.RoleID) CustomPerms { 213 owners := []discord.UserID{} 214 215 for _, u := range r.BotOwners { 216 id, err := discord.ParseSnowflake(u) 217 if err != nil { 218 panic(err) 219 } 220 221 owners = append(owners, discord.UserID(id)) 222 } 223 224 return &requireRole{ 225 name: name, 226 owners: owners, 227 roles: roles, 228 } 229 }