Alchemy.Cogs (alchemy v0.7.0)

This module provides quite a bit of sugar for registering commands.

To use the macros in this module, it must be used. This also defines a __using__ macro for that module, which will then allow these commands to be loaded in the main application via use

Example Module

defmodule Example do
  use Alchemy.Cogs

  Cogs.def ping do
    Cogs.say "pong"
  end

  Cogs.def echo do
    Cogs.say "please give me a word to echo"
  end
  Cogs.def echo(word) do
    Cogs.say word
  end
end

This defines a basic Cog, that can now be loaded into our application via use. The command created from this module are "!ping", and "!echo", ("!" is merely the default prefix, it could be anything from "?", to "SHARKNADO"). The ping command is straight forward, but as you can see, the echo command takes in an argument. When you define a command, the handler will try and get arguments up to the max arity of that command; in this case, echo has a max arity of one, so the parser will pass up to one argument to the function. In the case that the parser can't get enough arguments, it will pass a lower amount. We explicitly handle this case here, in this case sending a useful error message back.

Shared names across multiple modules

If I define a command ping in module A, and a ping in module B, which ping should become the command? In general, you should avoid doing this, but the module used last will override previously loaded commands with a matching name.

Parsing

The way the parser works is simple: a message is first decomposed into parts:

prefix <> command <> " " <> rest

If the prefix doesn't match, the message is ignored. If it does match, a new Task is started to handle this event. This task will try and find the function corresponding to the command called, and will return preemptively if no such function is found. After that, rest is passed to the parser, which will try and extract arguments to pass to the function. The default parsing method is simply splitting by whitespace. Thankfully, you can define a custom parser for a command via Cogs.set_parser/2. This parser will act upon rest, and parse out the relevant arguments.

The message argument

When you define a function with Cogs.def the function gets expanded to take an extra message parameter, which is the message triggering the command. This contains a lot of useful information, and is what enables a lot of the other macros to work. Because of this, be wary of naming something else message.

Loading and Unloading

Loading a cog merely requires having started the client:

use Example

If you need to remove this cog from the handler:

Cogs.unload(Example)

Or you just want to disable a single function:

Cogs.disable(:ping)

Link to this section Summary

Functions

Returns a map from command name (string) to the command information.

Registers a new command, under the name of the function.

Disables a command.

Makes all commands in this module sub commands of a group.

Gets the guild struct from which a command was triggered.

Gets the id of the guild from which a command was triggered.

Returns the base permissions for a member in a guild.

Gets the member that triggered a command.

Returns the permission bitset of the current member in the channel the command was called from.

Sends a message to the same channel as the message triggering a command.

Allows you to register a custom message parser for a command.

Sets the client's command prefix to a specific string.

Unloads a module from the handler.

Halts the current command until an event is received.

Waits for a specific event satisfying a condition.

Link to this section Types

Specs

parser() :: (String.t() -> Enum.t())

Link to this section Functions

Specs

all_commands() :: map()

Returns a map from command name (string) to the command information.

Each command is either {module, arity, function_name}, or {module, arity, function_name, parser}.

This can be useful for providing some kind of help command, or telling a user if a command is defined, e.g. :

Cogs.def iscommand(maybe) do
  case Cogs.all_commands()[maybe] do
    nil -> Cogs.say "#{maybe} is not a command"
    _   -> Cogs.say "#{maybe} is a command"
  end
end
Link to this macro

def(func, body)

(macro)

Registers a new command, under the name of the function.

This macro modifies the function definition, to accept an extra message parameter, allowing the message that triggered the command to be passed, as a t:Alchemy.Message/0

Examples

Cogs.def ping do
  Cogs.say "pong"
end

In this case, "!ping" will trigger the command, unless another prefix has been set with set_prefix/1

Cogs.def mimic, do: Cogs.say "Please send a word for me to echo"
Cogs.def mimic(word), do: Cogs.say word

Messages will be parsed, and arguments will be extracted, however, to deal with potentially missing arguments, pattern matching should be used. So, in this case, when a 2nd argument isn't given, an error message is sent back.

Link to this function

disable(command)

Specs

disable(atom()) :: :ok

Disables a command.

If you want to remove a whole module from the cogs, use Cogs.unload/1.

This will stop a command from being triggered. The only way to reenable the command is to reload the module with use.

Examples

defmodule Example do
  use Alchemy.Cogs

  Cogs.def ping, do: Cogs.say "pong"

  Cogs.def foo, do: Cogs.say "bar"
end
Client.start(@token)
use Example
Cogs.disable(:foo)

Only ping will be triggerable now.

use Example

At runtime this will add foo back in, given it's still in the module.

Link to this macro

group(str)

(macro)

Makes all commands in this module sub commands of a group.

Examples

defmodule C do
  use Alchemy.Cogs

  Cogs.group("cool")

  Cogs.def foo, do: Cogs.say "foo"
end

To use this foo command, one has to type !cool foo, from there on arguments will be passed like normal.

The relevant parsing will be done in the command task, as if there were a command !cool that redirected to subfunctions. Because of this, Cogs.disable/1 will not be able to disable the subcommands, however, Cogs.unload/1 still works as expected. Reloading a grouped module will also disable removed commands, unlike with ungrouped modules.

Link to this macro

guild()

(macro)

Gets the guild struct from which a command was triggered.

If only the id is needed, see :guild_id/0

Examples

Cogs.def guild do
  {:ok, %Alchemy.Guild{name: name}} = Cogs.guild()
  Cogs.say(name)
end
Link to this macro

guild_id()

(macro)

Gets the id of the guild from which a command was triggered.

Returns {:ok, id}, or {:error, why}. Will never return ok outside of a guild, naturally. This is to be used when the guild_id is necessary for an operation, but the full guild struct isn't needed.

Link to this macro

guild_permissions()

(macro)

Returns the base permissions for a member in a guild.

Functions similarly to permissions.

Link to this macro

member()

(macro)

Gets the member that triggered a command.

Returns either {:ok, member}, or {:error, why}. Will not return ok if the command wasn't run in a guild. As opposed to message.author, this comes with a bit more info about who triggered the command. This is useful for when you want to use certain information in a command, such as permissions, for example.

Link to this macro

permissions()

(macro)

Returns the permission bitset of the current member in the channel the command was called from.

If you just want the base permissions of the member in the guild, see guild_permissions. Returns {:ok, perms}, or {:error, why}. Fails if not called from a guild, or the guild or the member couldn't be fetched from the cache.

Example

Cogs.def perms do
  with {:ok, permissions} <- Cogs.permissions() do
    Cogs.say "Here's a list of your permissions `#{Permissions.to_list(permissions)}`"
  end
end
Link to this macro

say(content, options \\ [])

(macro)

Sends a message to the same channel as the message triggering a command.

This can only be used in a command defined with Cogs.def

This is just a thin macro around Alchemy.Client.send_message/2

Examples

Cogs.def ping, do: Cogs.say("pong!")
Link to this macro

set_parser(name, parser)

(macro)

Allows you to register a custom message parser for a command.

The parser will be applied to part of the message not used for command matching.

prefix <> command <> " " <> rest

Examples

Cogs.set_parser(:echo, &List.wrap/1)
Cogs.def echo(rest) do
  Cogs.say(rest)
end
Link to this function

set_prefix(prefix)

Specs

set_prefix(String.t()) :: :ok

Sets the client's command prefix to a specific string.

This will only work after the client has been started

Example

Client.start(@token)
Cogs.set_prefix("!!")

Specs

unload(atom()) :: :ok

Unloads a module from the handler.

If you just want to disable a single command, use Cogs.disable/1

Examples

Client.start(@token)
use Commands2

Turns out we want to stop using Commands2 commands in our bot, so we can simply unload the module:

Cogs.unload(Commands2)

Now none of the commands defined in that module will be accessible. If we want to reverse that, we can merely do:

use Commands2

and reload them back in.

Link to this macro

wait_for(type, fun)

(macro)

Halts the current command until an event is received.

The event type is an item corresponding to the events in Alchemy.Events, i.e. on_message_edit -> Cogs.wait_for(:message_edit, ...). The fun is the function that gets called with the relevant event arguments; see Alchemy.Events for more info on what events have what arguments.

The :message event is a bit special, as it will specifically wait for a message not triggered by a bot, in that specific channel, unlike other events, which trigger generically across the entire bot.

The process will kill itself if it doesn't receive any such event for 20s.

Examples

Cogs.def color do
  Cogs.say "What's your favorite color?"
  Cogs.wait_for :message, fn msg ->
    Cogs.say "#{msg.content} is my favorite color too!"
  end
end
Cogs.def typing do
  Cogs.say "I'm waiting for someone to type.."
  Cogs.wait_for :typing, fn _,_,_ ->
    Cogs.say "Someone somewhere started typing..."
  end
Link to this macro

wait_for(type, condition, fun)

(macro)

Waits for a specific event satisfying a condition.

Same as wait_for/2, except this takes an extra condition that needs to be met for the waiting to handle to trigger.

Examples

Cogs.def foo do
  Cogs.say "Send me foo"
  Cogs.wait_for(:message, & &1.content == "foo", fn _msg ->
    Cogs.say "Nice foo man!"
  end)

Note that, if no event of the given type is received after 20s, the process will kill itself, it's possible that this will never get met, but no event satisfying the condition will ever arrive, essentially rendering the process a waste. To circumvent this, it might be smart to send a preemptive kill message:

self = self()
Task.start(fn ->
  Process.sleep(20_000)
  Process.exit(self, :kill)
)
Cogs.wait_for(:message, fn x -> false end, fn _msg ->
  Cogs.say "If you hear this, logic itself is falling apart!!!"
end)