Skip to content

Plugincrafting: commands

JR edited this page Sep 30, 2022 · 5 revisions

Plugin behaviour, or rather the behaviour of the annotated functions that make up your plugin, can easily be extended using User Defined Attributes. As of time of writing there are a few other UDAs with which you can limit when your functions fire.

Bot commands

// Nested in IRCEventHandler!
struct Command
{
    private import kameloso.traits : UnderscoreOpDispatcher;

    PrefixPolicy _policy;
    string _word;
    string _description;
    string[] _syntaxes;
    bool _hidden;

    mixin UnderscoreOpDispatcher;
}

The template mixin UnderscoreOpDispatcher mixes in an opDispatch that simulates mutator (setter) functions for all members in that struct or class that start with an underscore, with the underscore omitted as the name of the function.

struct Command
{
    PrefixPolicy _policy;
    string _word;
    string _description;
    string[] _syntaxes;
    bool _hidden;

    mixin UnderscoreOpDispatcher;

    // opDispatch then simulates the following:

    ref auto policy(PrefixPolicy);
    ref auto word(string);
    ref auto description(string);
    ref auto syntaxes(string);
    ref auto hidden(bool);
}

A bot command is the start of a message, delimited with (hardcoded) spaces, colons, exclamation and/or question marks. word should be set with the string of the prefix you want to catch. It filters messages by what they start with.

// Nested in IRCEventHandler!
struct Regex
{
    private import kameloso.traits : UnderscoreOpDispatcher;

    PrefixPolicy _policy;
    StdRegex!char _engine;
    string _expression;
    string _description;
    bool _hidden;

    ref auto expression(string);  // explicit function, not generated by UnderscoreOpDispatcher

    mixin UnderscoreOpDispatcher;

    ref auto policy(PrefixPolicy);
    ref auto engine(StdRegex!char);
    //ref auto expression(string);
    ref auto description(string);
    ref auto hidden(bool);
}

A bot regex is like a bot command, except it takes a regular expression string and is applied to the whole message.

Bot nickname prefixing commands

PrefixPolicy decides how a string must start for it to be considered a command. It has five settings;

  • direct means there is no special start required, "hello" will match "hello".
  • prefixed means the command should start with the command prefix, as configured in the configuration file under [Core]. The default is !, allowing for commands like !hello.
  • nickname means it requires the bot nickname as prefix (kameloso: hello) and the function will not trigger without it -- except when targetted directly to the bot in QUERY events, where it is optional.

As such we can annotate a function that requires a string to be prefixed with the bot nickname, plus a command to control behaviour.

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.CHAN)
    .addCommand(
        IRCEventHandler.Command()
            .word("hello")
            .policy(PrefixPolicy.nickname)
    )
)
void onHello(MyPlugin plugin, const IRCEvent event)
{
    import std.stdio : writeln;
    writeln("Hello world!");
}

The above would be triggered by saying kameloso: hello.

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.CHAN)
    .addCommand(
        IRCEventHandler.Command()
            .word("hello")
            .policy(PrefixPolicy.prefixed)
    )
)
void onHello(MyPlugin plugin, const IRCEvent event)
{
    import std.stdio : writeln;
    writeln("Hello world!");
}

With PrefixPolicy.prefixed the above would be triggered by saying !hello, assuming ! is set up to be the command prefix.

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.CHAN)
    .addRegex(
        IRCEventHandler.Regex()
            .expression("\bhello\b")
            .policy(PrefixPolicy.direct)
    )
)
void onHello(MyPlugin plugin, const IRCEvent event)
{
    import std.stdio : writeln;
    writeln("Hello world!");
}

Expressed as a regular expression, the above would be triggered if a message contained the word hello.

.expression("\bhello\b")

See the D Wiki page on Regular Expressions as well as the Phobos page on std.regex for more information about crafting expressions.

Permissions

.permissionsRequired filters whether a user can access a function based on who they are in the context of the current channel. If the bot doesn't know, it will query the server for information and try to catch the account name.

  • admin, or you; the bot's administrator(s).
  • staff, or the owners of a channel.
  • operator, or someone in the operator list for the current channel.
  • elevated, or someone with sub-operator elevated privileges in your channel.
  • whitelist, or someone in the whitelist class list for the current channel.
  • registered, or anyone logged onto services (where possible).
  • anyone, or simply everyone, but will still do account lookups.
  • ignore, which will skip all checks.

Much like with Commands and Regexes but with sender being filtered, you can control who gets to trigger your functions.

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.CHAN)
    .permissionsRequired(Permissions.whitelist)  // <--
    .addCommand(
        IRCEventHandler.Command()
            .word("hello")
            .policy(PrefixPolicy.prefixed)
    )
)
void onHello(MyPlugin plugin, const IRCEvent event)
{
    import std.stdio : writeln;
    writeln("Hello world!");
}

The function will now trigger if someone says kameloso: hello, iff they are in your whitelist for the current channel. It will also trigger if you say it, in the admin role, but never if someone outside does. The prefixing kameloso: will be optional if sent in a query.

Channel policy

By default events will only be able to trigger your functions if they occur in a channel in your homes array, where applicable. Naturally this doesn't concern event types that don't have a channel, such as a reply to WHOIS, sent directly to you from the server.

You can annotate functions to work on any and all channels with .channelPolicy.

@(IRCEvent.Type.JOIN)
@(ChannelPolicy.home)
void onJoin(MyPlugin plugin, const IRCEvent event) { /* ... */ }

This function will only trigger on JOINs to your home channels, and is the default behaviour if you don't specify a ChannelPolicy.

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.JOIN)
    .channelPolicy(ChannelPolicy.all)  // <--
    .addCommand(
        IRCEventHandler.Command()
            .word("hello")
            .policy(PrefixPolicy.prefixed)
    )
)
void onJoin(MyPlugin plugin, const IRCEvent event) { /* ... */ }

This is the same function, but now it triggers on JOINs in any channel your bot is in, even non-homes.

Real examples composed

In plugins/admin.d:

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.CHAN)
    .onEvent(IRCEvent.Type.QUERY)
    .onEvent(IRCEvent.Type.SELFCHAN)
    .permissionsRequired(Permissions.admin)
    .channelPolicy(ChannelPolicy.home)
    .addCommand(
        IRCEventHandler.Command()
            .word("sudo")
            .policy(PrefixPolicy.nickname)
            .description("[debug] Sends supplied text to the server, verbatim.")
            .syntax("$command [raw string]")
    )
)
void onCommandSudo(MyPlugin plugin, const IRCEvent event) { /* ... */ }

This triggers on CHAN and QUERY (and SELFCHAN) events, only when sent by an admin, with a nickname bot prefix (kameloso: ) only in CHAN events, and with the command string sudo.

In plugins/chatbot.d:

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.CHAN)
    .onEvent(IRCEvent.Type.QUERY)
    .onEvent(IRCEvent.Type.SELFCHAN)
    .permissionsRequired(Permissions.anyone)
    .channelPolicy(ChannelPolicy.home)
    .addCommand(
        IRCEventHandler.Command()
            .word("8ball")
            .policy(PrefixPolicy.prefixed)
            .description("Implements 8ball. Randomises a vague yes/no response.")
    )
)
void onCommand8ball(MyPlugin plugin, const IRCEvent event) { /* ... */ }

This triggers only on CHAN events with any permissions level, with a prefixed command prefix and the command string 8ball.

In plugins/webtitles.d, and note no BotCommand:

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.CHAN)
    .onEvent(IRCEvent.Type.SELFCHAN)
    .permissionsRequired(Permissions.ignore)
    .channelPolicy(ChannelPolicy.home)
)
void onMessage(MyPlugin plugin, const IRCEvent event) { /* ... */ }

This triggers on CHAN messages sent by anyone, with no command. This means it gets triggered on every message sent by that permissions level, with no filtering.

In plugins/seen.d:

@(IRCEventHandler()
    .onEvent(IRCEvent.Type.RPL_NAMREPLY)
    .channelPolicy(ChannelPolicy.any)
)
void onNameReply(MyPlugin plugin, const IRCEvent event) { /* ... */ }

This triggers on RPL_NAMREPLY, an event that fires when you join a channel and lists everyone in it. It will be called when this happens for any and all channels the bot is in, even if it is not a home, due to ChannelPolicy.any. There is no command involved.