License | BSD (see the LICENSE file) |
---|---|
Safe Haskell | None |
Language | Haskell2010 |
Extensions |
|
Command.Command
Description
Amateur attempt at command abstraction and polyvariadic magic.
Inspired heavily by calamity-commands, which is provided by Ben Simms 2020 under the MIT license.
Ideally, this module wouldn't need to be touched after its initial creation (and hence quite the jump in complex GHC extensions compared to other modules), however it is documented quite extensively anyway.
As an OwenDev, you do not need to enable any GHC extensions, as the extensions are used internally within this module only.
Notable extensions used (if you want to know)
- ScopedTypeVariables: For using the same type variables in
where
statements as function declarations. - FlexibleInstances: To allow complex type variables in instance declarations,
like
CommandHandlerType m (a -> m ())
. Read more - FunctionalDependencies: To write that
m
can be determined fromh
inCommandHandlerType
. It makes logical sense to tell GHC this becauseh
must be in them
monad (otherwise,h
may be in another monad). Read more - MultiParamTypeClasses: For declaring CommandHandlerType that has 2 params. This comes with FunctionalDependencies automatically, and is not explicitly used.
- UndecidableInstances: Risky, but I think I logically checked over it. Used
in the
m (a -> b)
instance declaration ofCommandHandlerType
, because(a -> b)
doesn't explicitly determine the required functional dependency (h -> m
). The extension is risky because the type-checker can fail to terminate if the instance declarations recursively reference each other. In this module however, all instances strictly converges to them (m ())
instance so I say it is safe. Read more - NamedFieldPuns: Shorten pattern matching ADT field names.
Implementation references
Synopsis
- data Command m
- command :: (CommandHandlerType m h, MonadDiscord m) => Text -> h -> Command m
- runCommand :: forall m. MonadDiscord m => Command m -> Message -> m ()
- runCommands :: (MonadDiscord m, Alternative m) => [Command m] -> Message -> m ()
- runHelp :: MonadDiscord m => Text -> [Command m] -> Message -> m ()
- parsecCommand :: MonadDiscord m => Parser String -> (Message -> String -> m ()) -> Command m
- regexCommand :: MonadDiscord m => Text -> (Message -> [Text] -> m ()) -> Command m
- help :: Text -> Command m -> Command m
- alias :: Text -> Command m -> Command m
- onError :: (Message -> CommandError -> m ()) -> Command m -> Command m
- defaultErrorHandler :: MonadDiscord m => Message -> CommandError -> m ()
- requires :: (Message -> m (Maybe Text)) -> Command m -> Command m
- data CommandError
- class ParsableArgument a
- newtype RemainingText = Remaining {}
- module Discord.Internal.Monad
- class Monad m => MonadIO (m :: Type -> Type) where
Troubleshooting
See the Common Errors section.
The fundamentals
These offer the core functionality for Commands. The most imporatnt
functions are command
and runCommand
.
command
creates aCommand
runCommand
converts theCommand
to a normal receiver.
If your command demands a special syntax that is impossible with the
existing command
function, use parsecCommand
(Parsec) or regexCommand
(Regex).
A Command
is a datatype containing the metadata for a user-registered
command.
Command m
is a command that runs in the monad m
, which when triggered
will run a polyvariadic handler function. The handler *must* run in the
m
monad, which is enforced by the type-checker (to see the details, look in
CommandHandlerType
in the source code).
The contents of this abstract datatype are not exported from this module for
encapsulation. Use command
, parsecCommand
, or regexCommand
to
instantiate one.
Arguments
:: (CommandHandlerType m h, MonadDiscord m) | |
=> Text | The name of the command. |
-> h | The handler for the command, that takes an arbitrary amount of
|
-> Command m |
command name handler
creates a Command
named name
, which upon
receiving a message will run handler
. The name
cannot contain any spaces,
as it breaks the parser. The handler
function can take an arbitrary amount
of arguments of arbitrary types (it is polyvariadic), as long as they are
instances of ParsableArgument
.
The Command that this function creates is polymorphic in the Monad it is run
in. This means you can call it from DiscordHandler
or any other
Monad that satisfies the constraints of MonadDiscord
.
See some examples
pong
below responds to a ping by a pong.
pong :: (MonadDiscord m) => Command m pong = command "ping" $ \msg -> respond msg "pong!"
pong2
shows that runCommand
can be composed to create a normal receiver.
That is, it takes a Message and returns a unit action in the desired monad.
pong2 :: (MonadDiscord m) => Message -> m () pong2 = runCommand $ command "ping" $ \msg -> respond msg "pong!"
weather
below shows having arbitrary arguments in action. location
is
likely inferred to be Text
.
weather = command "weather" $ \msg location -> do result <- liftIO $ getWeather location respond msg $ "Weather at " <> location <> " is " <> result <> "!"
complex
shows that you can parse any type! (as long as you create an
instance declaration of ParsableArgument
for it). You may want to put this in
Command/Parser.hs
.
instance ParsableArgument Int where parserForArg = read <$> many digit -- some Parsec that returns an Int complex :: (MonadDiscord m) => Command m complex = command "setLimit" $ \m i -> do respond m $ show (i + 20 / 4 + 78^3) -- i is automatically inferred as Int! ...
Arguments
:: forall m. MonadDiscord m | |
=> Command m | The command to run against. |
-> Message | The message to run the command with. |
-> m () |
runCommand command msg
runs the specified Command
with the given
message. It first does the initial checks:
For commands registered with command
, the check will check for the prefix
and the name.
For commands registered with regexCommand
, the check will check against the
regex.
For commands registered with parsecCommand
, the check will check for the
prefix and the custom parser.
Any failures during this stage is silently ignored, as it may still be a valid
command elsewhere. After this, the requirements are checked (chance, priv, etc).
Finally, the commandApplier
is called. Any errors inside the handler is
caught and appropriately handled.
runCommand pong :: (MonadDiscord m) => Message -> m ()
runCommands :: (MonadDiscord m, Alternative m) => [Command m] -> Message -> m () Source #
runCommands
calls runCommand for all the Commands, and folds them with
the Monadic bind (>>
).
runHelp :: MonadDiscord m => Text -> [Command m] -> Message -> m () Source #
runHelp
creates a super duper simple help command that just lists
each command's names together with their help text.
Custom command parsing
Parsec and Regex options are available.
Arguments
:: MonadDiscord m | |
=> Parser String | The custom parser for the command. It has to return a |
-> (Message -> String -> m ()) | The handler for the command. |
-> Command m |
parsecCommand
defines a command that has no name, and has a custom
parser that begins from the start of a message. It can help things like
"::quotes" because they have special syntax that demands a special parser.
alias
, if used together, is ignored. Other compoasble functions like help
,
prefix
, requires
, and onError
are still valid.
The handler must take a Message
and a String
as argument (nothing
more, nothing less), where the latter is the result of the parser.
example = requires moderatorPrivs . prefix "~~" . parsecCommand (string "abc" >> many1 anyChar) $ \msg quoteName -> do ... this is triggered on "~~abc<one or more chars>" where quoteName contains the section enclosed in <>
regexCommand :: MonadDiscord m => Text -> (Message -> [Text] -> m ()) -> Command m Source #
regexCommand
defines a command that has no name, and has a custom
regular expression matcher, that searches across any part of the message.
prefix
and alias
, if used together, are ignored. Other compoasble
functions like help
, requires
, and onError
are still valid.
The handler must take a Message
and a '[T.Text]' as argument, where
the latter are the list of captured values from the Regex (same as the past
newCommand
).
thatcher = regexCommand "thatcher [Ii]s ([Dd]ead|[Aa]live)" $ \msg caps -> do let verb = head caps ...
Compsable Functions
These can be composed onto command
to overwrite default functionality.
help :: Text -> Command m -> Command m Source #
help
sets the help message for the command. The default is "Help not
available."
alias :: Text -> Command m -> Command m Source #
alias
adds an alias for the command's name. This is ignored if a custom
parser like regexCommand
or parsecCommand
is used.
Functionally, this is equivalent to defining a new command with the same handler, however the aliases may not appear on help pages.
Arguments
:: (Message -> CommandError -> m ()) | Error handler that takes the original message that caused the error, and the error itself. It runs in the same monad as the command handlers. |
-> Command m | |
-> Command m |
onError
overwrites the default error handler of a command with a custom
implementation. Usually the default defaultErrorHandler
suffices.
example = onError (\msg e -> respond msg (T.pack $ show e)) . command "example" $ do ...
defaultErrorHandler :: MonadDiscord m => Message -> CommandError -> m () Source #
defaultErrorHandler m e
is the default error handler unless you override
it manually. This is exported and documented for reference only.
- On argument error
- It calls
respond
with the errors. This isn't owoified for legibility. - On requirement error
- It sends a DM to the invoking user with the errors.
- On a processing error
- It calls
respond
with the error. - On a Discord request failure
- It calls
respond
with the error. - On a Runtime/Haskell error
- It calls
respond
with the error, owoified.
requires :: (Message -> m (Maybe Text)) -> Command m -> Command m Source #
requires
adds a requirement to the command. The requirement is a function
that takes a Message
and either returns
(no problem) or
Nothing
. The function is in the Just
"explanation"m
monad so it can access
any additional information from Discord as necessary.
Commands default to having no requirements.
Errors
data CommandError Source #
This represents any error that can arise from an invocation of a command.
Some are thrown by the system (such as ArgumentParseError), however you can
also manually throw them with throwM
within any handler.
Constructors
ArgumentParseError Text | Indicates the command arguments failed to parse, either because of lack of arguments, incorrect types, or too many arguments. |
RequirementError Text | Indicates that a requirement for this command failed to pass. The reason is specified in the text field. |
ProcessingError Text | This is a value unused by the system, and is free to be used by the handler as they wish. |
DiscordError RestCallErrorCode | Indicates there was a fatal Discord call somewhere in the handler. |
HaskellError SomeException | Indicates there was some sort of runtime error. This contains all sorts of errors, however they are guaranteed to be safe synchronous exceptions. |
Instances
Show CommandError Source # | |
Defined in Command.Error Methods showsPrec :: Int -> CommandError -> ShowS # show :: CommandError -> String # showList :: [CommandError] -> ShowS # | |
Exception CommandError Source # | |
Defined in Command.Error Methods toException :: CommandError -> SomeException # fromException :: SomeException -> Maybe CommandError # displayException :: CommandError -> String # |
Parsing arguments
The ParsableArgument
is the core dataclass for command arguments that
can be parsed.
As an OwenDev, if you want to make your own datatype parsable, all you
have to do is to add an instance declaration for it (and a parser) inside
Command/Parser.hs
. Parsec offers very very very useful functions that
you can simply compose together to create parsers.
class ParsableArgument a Source #
A ParsableArgument
is a dataclass that represents arguments that can be
parsed from a message text. Any datatype that is an instance of this dataclass
can be used as function arguments for a command handler in command
.
Minimal complete definition
Instances
ParsableArgument String Source # | Any number of non-space characters. If quoted, spaces are allowed.
Quotes in quoted phrases can be escaped with a backslash. The following is
parsed as a single string:
|
Defined in Command.Parser Methods | |
ParsableArgument Text Source # | Wrapper for the String version, since Text is the trend nowadays. Both are provided so that it can easily be used for arguments in other functions that only accept one of the types. |
Defined in Command.Parser Methods | |
ParsableArgument ActivityType Source # | Parses "playing", "streaming", "listening to" and "competing in" as
|
Defined in Command.Parser Methods | |
ParsableArgument UpdateStatusType Source # | Parses "online" "dnd" "idle" and "invisible" as |
Defined in Command.Parser Methods | |
ParsableArgument Snowflake Source # | |
Defined in Command.Parser Methods | |
ParsableArgument RemainingText Source # | The rest of the arguments. Spaces and quotes are preserved as-is, unlike
with |
Defined in Command.Parser Methods | |
ParsableArgument [Text] Source # | Zero or more texts. Each one could be quoted or not. |
Defined in Command.Parser Methods parserForArg :: Parser [Text] Source # | |
ParsableArgument a => ParsableArgument (Maybe a) Source # | An argument that can or cannot exist. |
Defined in Command.Parser Methods parserForArg :: Parser (Maybe a) Source # | |
(ParsableArgument a, ParsableArgument b) => ParsableArgument (a, b) Source # | An argument that always has to be followed by another. |
Defined in Command.Parser Methods parserForArg :: Parser (a, b) Source # |
newtype RemainingText Source #
Datatype wrapper for the remaining text in the input. Handy for capturing
everything remaining. The accessor function getDeez
isn't really meant to be
used since pattern matching can do everything. Open to renaming.
Example usage:
setStatus = command "status" $ \msg newStatus newType (Remaining newName) -> do ...
Instances
ParsableArgument RemainingText Source # | The rest of the arguments. Spaces and quotes are preserved as-is, unlike
with |
Defined in Command.Parser Methods |
Exported classes
Here are the monad dataclasses exported from this module.
The MonadDiscord class
MonadDiscord is the underlying Monad class for all interactions to the Discord REST API.
This is a common way to design abstraction so you can mock them. Same as Java interfaces, C++ virtual functions. Source that I based this design on:
module Discord.Internal.Monad
MonadIO
Exported solely for convenience purposes, since many modules that use
Commands require the MonadIO constraint, but it can get confusing where
to import it from (UnliftIO or Control.Monad.IO.Class). The one exported
from this module is from Control.Monad.IO.Class which is in base
.
class Monad m => MonadIO (m :: Type -> Type) where #
Monads in which IO
computations may be embedded.
Any monad built by applying a sequence of monad transformers to the
IO
monad will be an instance of this class.
Instances should satisfy the following laws, which state that liftIO
is a transformer of monads:
Instances
Common Errors
| Here are some common errors that can occur when defining commands. They may appear cryptic, but they are most of the time dealable.
Could not deduce (ParsableArgument p0) arising from the use of 'command'.
The type variable p0
is ambiguous.
- The type for one of the arguments to your handler function cannot be inferred. Make sure you use the argument, otherwise, just remove it.
Could not deduce (ParsableArgument SomeType) arising from the use of 'command'.
- The type could be inferred as SomeType, but it's not an instance
of ParsableArgument. Contribute your own parser in
Command/Parser.hs
.
Could not deduce (MonadIO m) arising from the use of 'liftIO'.
- Your handler requires IO actions, but you haven't given the
appropriate constraint. Add
(MonadIO m)
. - Rationale: This happens because some handlers are pure and don't need IO -
it's better to explicitly signify which actions you're going to use
in the constraints than to add a catch-all constraint into the
definition of
MonadDiscord
.
If an error is super duper cryptic, it may be a bug in the Commands module itself, in which case we may need a rewrite.