A command-based bot framework for XMPP
Go to file
2021-11-24 14:19:35 +01:00
mira base: Allow loading the password from a secret file 2021-11-24 14:19:35 +01:00
tests fix: Inconsistency in get_subscriptions_for_jid 2021-06-15 20:34:49 +02:00
.gitignore Initial commit 2021-03-31 00:31:29 +02:00
LICENSE meta: Add a license 2021-03-31 00:35:36 +02:00
README.md docs: Small README touch-up 2021-06-13 00:30:49 +02:00
setup.py tag: 0.3.1 2021-06-15 20:35:30 +02:00

mira

mira is a command-based bot framework for XMPP. It allows you to create bots that react to your users' commands or notify them based on a subscribtion system.

Configuration

The main bot configuration is based on a TOML file:

# The JID to authenticate as
jid = "awesome-bot@example.org"
# The password
password = "s3cr3t_p4ssw0rd"
# Where the persistent data should be stored.
# Defaults to /etc/mira/storage.json
storage_path = "/home/mira/storage.json"
# Loads the file as an avatar and publishes it (OPTIONAL)
avatar = "/etc/mira/avatar.png"

# A list of modules that should be loaded
[[modules]]
# Load the "test" module that greets a user to writes
# "test" to awesome-bot@example.org
name = "mira.modules.test"
greeting = "Hello, Mr. %%user%%!"

[[modules]]
# This module reacts only to users whose JID has the
# same domain as the bot
name = "mira.modules.other_test"
restrict_local = true

By default, mira expects the config at /etc/mira/config.toml, but the path can be overwritten by passing --config.

Writing a module

Writing a module is pretty easy. The following is the base template:

from mira.module import BaseModule

NAME = 'new'

class NewModule(BaseModule):
    __instance = None

    @staticmethod
    def get_instance(base, **kwargs):
        if NewModule.__instance == None:
            NewModule(base, **kwargs)

        return NewModule.__instance
    
    def __init__(self, base, **kwargs):
        if NewModule.__instance != None:
            raise Exception('Trying to init singleton twice')

        super().__init__(base, **kwargs)
        NewModule.__instance = self

    async def on_command(self, cmd, msg):
    	  # ...
	  pass

def get_instance(base, **kwargs):
    return TestModule.get_instance(base, **kwargs)

Currently, modules are implemented as singletons, so most of the code is just for turning the class into a singleton. What's required is the toplevel constant NAME! It is the name of the module as the same time the "command". mira will pass all messages that start with the value of NAME to the on_command function. It's parameters are the message body whitespace separated (cmd) and the original aioxmpp message object (msg).

If you want to add configuration options to your bot, you can use the module's get_option(key, default=None) function, which works exactly like dict.get(...).

If you have subcommands, like "new something1" and "new something2", then you can use a subcommand table. It is just a dictionary that tells mira which function to call on which subcommand. In the example above, it looks like this:

from mira.module import BaseModule

NAME = 'new'

class NewModule(BaseModule):
    __instance = None

    @staticmethod
    def get_instance(base, **kwargs):
        if NewModule.__instance == None:
            NewModule(base, **kwargs)

        return NewModule.__instance
    
    def __init__(self, base, **kwargs):
        if NewModule.__instance != None:
            raise Exception('Trying to init singleton twice')

        super().__init__(base, **kwargs)
        NewModule.__instance = self

	self._subcommand_table = {
	    'something1': self._something1,
	    'something2': self._something2,
	    '*': self._help
	}

    async def _something1(self, cmd, msg):
    	  # ...
    async def _something2(self, cmd, msg):
    	  # ...
    async def _help(self, cmd, msg):
    	  # ...

def get_instance(base, **kwargs):
    return TestModule.get_instance(base, **kwargs)

Note that '*' tells mira that the function should be called everytime no other subcommand matches.

To send a message, the simplest way is to use the module's send_message(to, body) function, which takes an aioxmpp JID as to and a string as body.

Storage

If your module needs to store data, then you can use the StorageManager. It is available via the module's _stm property. It provides the following two functions:

  • get_data(section): Retrieve data (dictionary) stored in the module's section. A module may have multiple sections
  • set_data(section, data): Stores data (dictionary) in the module's section. A module may have multiple sections

Subscriptions

If your users should be able to subscribe to certain events, then you can use the SubscriptionManager, available via the module's _sum property. It exposes a lot more functions, so it is probably the better to just look at it's code at mira/subscription.py.

Examples

If the test module mira.modules.test is not advanced enough, then you can find more examples here:

License

See LICENSE.