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  }