Development

Modules

Modules can be added by creating a module inside chitanda/modules. All modules inside of chitanda/modules will be dynamically imported upon bot startup. The name of the module identifies the module in configuration options. For example, chitanda/modules/catpics.py, the identifier will be catpics.

Modules can contain setup functions, bot hooks, commands, and database migrations.

If a module contains multiple commands, it can be turned into a package. The package must have an __init__.py to be dynamically imported. All python modules inside the package will be imported, and the name of the package identifies all python modules inside the package in the configuration file.

Setup

If an module imported dynamically by the bot has a setup function, it will be called on first import. This occurs after the bot’s __init__ finishes running. The only argument passed to setup is bot. This function should be synchronous.

During setup, functions can be added to the bot’s message_handler and response_handler hooks and routes can be added to the bot’s web_application (for webhook support).

Commands

To add a command to the bot, decorate a function with the register decorator, which takes, as an argument, the command trigger (without the configurable trigger character). Command functions must be asynchonous, and should have a message parameter.

Commands can either be coroutines or async generators; both are supported. The coroutines can return a generator as well.

The return value of a command or the values returned when iterating over it can be in two formats: a str or a dict. If returned as a str, the return value will be sent to message.target. If returned as a dict, the dict will be directly passed to the listener.message coroutine. The dict must have the keys target and message, with other key support depending on the listener it is passed to.

For example, a simple command to parrot text would be:

@register('parrot')
async def parrot(message):
   return message.contents
<user> .parrot squawk
<chitanda> squawk

When a command is called, it is passed a Message object as its only argument. A Message object has the following attributes:

class Message:
    bot  # The main bot class.
    listener  # The listener the message originated from.
    target  # The channel the message was sent to (same as author in PM).
    author  # The nickname (IRC) or ID of the message sender.
    contents  # The contents of the message with the trigger stripped out.
    private  # If the message was sent via PM or in a channel.

There are several decorators in the chitanda.decorators package which can be used to decorate commands. Some of these decorators add new instance variables to the message object.

Decorators

args

chitanda.decorators.args

A decorator factory that takes re.Pattern objects and/or str regexes as arguments. When a command is called, message.contents will be matched with the regexes in the order that they were passed into args. If there is a match, the return value of re.Match.groups() will be assigned to message.args. If there isn’t a match, a BotError will be raised.

Example:

import re
from chitanda.decorators import args, register

REUSED_REGEX = re.compile(r'a regex')

@register('generic_command')
@args(r'([^ ]+)', REUSED_REGEX)
async def call(message):
    print(message.args)

admin_only

chitanda.decorators.admin_only

Compares the sender of the command against the admin list. If the sender is an admin, the command will be called. Otherwise, a BotError will be raised.

auth_only

chitanda.decorators.auth_only

Check’s a user’s authorization before calling the command. This is primarily geared towards IRC, where it mandates NickServ identification. In services that require accounts to use, the command will always be called. If the user is found to be authorized, their account name/username will be assigned to message.username. If they are not authorized, a BotError will be raised.

channel_only

chitanda.decorators.channel_only

Requires that the message be sent in a channel, otherwise a BotError will be raised.

private_message_only

chitanda.decorators.private_message_only

Requires that the command be sent via PM, otherwise a BotError will be raised.

allowed_listeners

chitanda.decorators.allowed_listeners

A decorator factory that restricts the command to certain listeners. Each allowed listener type should be passed in as a separate argument. If the command is called on a disallowed listener, a BotError will be raised. Commands that are not allowed on a listener will not be shown in that listener’s help command.

Example:

from chitanda.decorators import register, allowed_listeners
from chitanda.listeners import IRCListener

@register('quit')
@allowed_listeners(IRCListener)
async def call(message):
    await listener.raw('QUIT\r\n')

Decorating Async Generators

When decorating an async generator, the admin_only and auth_only decorators must visually come last, i.e. decorate the function first. this is because they have different behaviors for async generators vs regular coroutines and detection of a decorated async generator isn’t accurate

from chitanda.decorators import args, auth_only, register

# Good

@register('pics cats')
@args(r'$')
@auth_only
async def call(message):
    for cat in _get_cat_pics():
        yield cat

# Bad

@register('pics cats')
@auth_only
@args(r'$')
async def call(message):
    for cat in _get_cat_pics():
        yield cat

Hooks

Hooks enable modules to process messages before the command is called and responses before they are sent to the recipient.

Pre-command hooks must be coroutines or async generators and take a message parameter, which is the Message object. If a value is returned from the hook, it will be handled the same way a return value from a command call would be handled. To add a pre-command hook, append the hook function to the bot.message_handlers list.

Pre-response hooks must be coroutines and take four parameters: bot, listener, target, response. Their return value is discarded. The response argument will always be a dictionary with target and message keys.

Database Migrations

Modules with database migrations must be python packages. Inside the package, the existence of a directory named migrations indicates that the module has database migrations to run. Migrations should be numerically named .sql files in the format 0001.sql, 0002.sql. They will be ran in the order of their ascending numerical identifiers.

The migrations that have been ran will be recorded in the database as to not re-run them.