owenbot-0.1.0.0
LicenseBSD (see the LICENSE file)
Safe HaskellNone
LanguageHaskell2010
Extensions
  • UndecidableInstances
  • ScopedTypeVariables
  • OverloadedStrings
  • RecordPuns
  • TypeSynonymInstances
  • FlexibleInstances
  • ConstrainedClassMethods
  • MultiParamTypeClasses
  • FunctionalDependencies
  • ExplicitForAll

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)

Expand
  • 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 from h in CommandHandlerType. It makes logical sense to tell GHC this because h must be in the m 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 of CommandHandlerType, 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 the m (m ()) instance so I say it is safe. Read more
  • NamedFieldPuns: Shorten pattern matching ADT field names.

Implementation references

Expand
Synopsis

Troubleshooting

See the Common Errors section.

The fundamentals

These offer the core functionality for Commands. The most imporatnt functions are command and runCommand.

If your command demands a special syntax that is impossible with the existing command function, use parsecCommand (Parsec) or regexCommand (Regex).

data Command m Source #

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.

command Source #

Arguments

:: (CommandHandlerType m h, MonadDiscord m) 
=> Text

The name of the command.

-> h

The handler for the command, that takes an arbitrary amount of ParsableArguments and returns a m ()

-> 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

Expand

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!
    ...

runCommand Source #

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.

parsecCommand Source #

Arguments

:: MonadDiscord m 
=> Parser String

The custom parser for the command. It has to return a String.

-> (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.

onError Source #

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 Nothing (no problem) or Just "explanation". The function is in the 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.

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

parserForArg

Instances

Instances details
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: "He said, \"Lovely\"."

Instance details

Defined in Command.Parser

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.

Instance details

Defined in Command.Parser

ParsableArgument ActivityType Source #

Parses "playing", "streaming", "listening to" and "competing in" as ActivityTypes.

Instance details

Defined in Command.Parser

ParsableArgument UpdateStatusType Source #

Parses "online" "dnd" "idle" and "invisible" as UpdateStatusTypes

Instance details

Defined in Command.Parser

ParsableArgument Snowflake Source # 
Instance details

Defined in Command.Parser

ParsableArgument RemainingText Source #

The rest of the arguments. Spaces and quotes are preserved as-is, unlike with Text. At least one character is required.

Instance details

Defined in Command.Parser

ParsableArgument [Text] Source #

Zero or more texts. Each one could be quoted or not.

Instance details

Defined in Command.Parser

ParsableArgument a => ParsableArgument (Maybe a) Source #

An argument that can or cannot exist.

Instance details

Defined in Command.Parser

(ParsableArgument a, ParsableArgument b) => ParsableArgument (a, b) Source #

An argument that always has to be followed by another.

Instance details

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
        ...

Constructors

Remaining 

Fields

Instances

Instances details
ParsableArgument RemainingText Source #

The rest of the arguments. Spaces and quotes are preserved as-is, unlike with Text. At least one character is required.

Instance details

Defined in Command.Parser

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:

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:

Methods

liftIO :: IO a -> m a #

Lift a computation from the IO monad.

Instances

Instances details
MonadIO IO

Since: base-4.9.0.0

Instance details

Defined in Control.Monad.IO.Class

Methods

liftIO :: IO a -> IO a #

MonadIO Q 
Instance details

Defined in Language.Haskell.TH.Syntax

Methods

liftIO :: IO a -> Q a #

MonadIO RestIO 
Instance details

Defined in Discord.Internal.Rest.Prelude

Methods

liftIO :: IO a -> RestIO a #

MonadIO Req 
Instance details

Defined in Network.HTTP.Req

Methods

liftIO :: IO a -> Req a #

MonadIO m => MonadIO (MaybeT m) 
Instance details

Defined in Control.Monad.Trans.Maybe

Methods

liftIO :: IO a -> MaybeT m a #

MonadIO m => MonadIO (ResourceT m) 
Instance details

Defined in Control.Monad.Trans.Resource.Internal

Methods

liftIO :: IO a -> ResourceT m a #

MonadIO m => MonadIO (ListT m) 
Instance details

Defined in Control.Monad.Trans.List

Methods

liftIO :: IO a -> ListT m a #

MonadIO m => MonadIO (RandT g m) 
Instance details

Defined in Control.Monad.Trans.Random.Lazy

Methods

liftIO :: IO a -> RandT g m a #

MonadIO m => MonadIO (RandT g m) 
Instance details

Defined in Control.Monad.Trans.Random.Strict

Methods

liftIO :: IO a -> RandT g m a #

MonadIO m => MonadIO (IdentityT m) 
Instance details

Defined in Control.Monad.Trans.Identity

Methods

liftIO :: IO a -> IdentityT m a #

(Monoid w, MonadIO m) => MonadIO (WriterT w m) 
Instance details

Defined in Control.Monad.Trans.Writer.Strict

Methods

liftIO :: IO a -> WriterT w m a #

(Monoid w, MonadIO m) => MonadIO (WriterT w m) 
Instance details

Defined in Control.Monad.Trans.Writer.Lazy

Methods

liftIO :: IO a -> WriterT w m a #

MonadIO m => MonadIO (StateT s m) 
Instance details

Defined in Control.Monad.Trans.State.Strict

Methods

liftIO :: IO a -> StateT s m a #

MonadIO m => MonadIO (StateT s m) 
Instance details

Defined in Control.Monad.Trans.State.Lazy

Methods

liftIO :: IO a -> StateT s m a #

MonadIO m => MonadIO (ReaderT r m) 
Instance details

Defined in Control.Monad.Trans.Reader

Methods

liftIO :: IO a -> ReaderT r m a #

MonadIO m => MonadIO (ExceptT e m) 
Instance details

Defined in Control.Monad.Trans.Except

Methods

liftIO :: IO a -> ExceptT e m a #

(Error e, MonadIO m) => MonadIO (ErrorT e m) 
Instance details

Defined in Control.Monad.Trans.Error

Methods

liftIO :: IO a -> ErrorT e m a #

MonadIO m => MonadIO (ConduitT i o m) 
Instance details

Defined in Data.Conduit.Internal.Conduit

Methods

liftIO :: IO a -> ConduitT i o m a #

MonadIO m => MonadIO (ContT r m) 
Instance details

Defined in Control.Monad.Trans.Cont

Methods

liftIO :: IO a -> ContT r m a #

MonadIO m => MonadIO (ParsecT s u m) 
Instance details

Defined in Text.Parsec.Prim

Methods

liftIO :: IO a -> ParsecT s u m a #

(Monoid w, MonadIO m) => MonadIO (RWST r w s m) 
Instance details

Defined in Control.Monad.Trans.RWS.Strict

Methods

liftIO :: IO a -> RWST r w s m a #

(Monoid w, MonadIO m) => MonadIO (RWST r w s m) 
Instance details

Defined in Control.Monad.Trans.RWS.Lazy

Methods

liftIO :: IO a -> RWST r w s m a #

MonadIO m => MonadIO (Pipe l i o u m) 
Instance details

Defined in Data.Conduit.Internal.Pipe

Methods

liftIO :: IO a -> Pipe l i o u m a #

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.