IMMP comes with a number of common idioms and features provided by built-in hooks, which can be used by your own classes to avoid reimplementing common bot tasks.

Data schema validation#

The included plugs make use of an internal schema validator to ensure all API responses contain the expected fields. These effectively act as assertions on the existence of all required fields, before starting to parse the object.

In the most basic case, prepare a Schema representing the data structure, and call it on each API response of that type before using it. For plug and hook configuration, attach it to the schema attribute to have incoming config validated at creation.

Storing data#

Some hooks may need to store their own information. The hook config is read-only – any changes are not persisted because the config must be provided at startup. For data encountered during the app’s lifetime, a database may be required.

The DatabaseHook resource provides generic database access using the Peewee ORM. Define your models as subclasses of BaseModel, then at startup, obtain the database connection DatabaseHook.db and call Database.create_tables() with a list of all your models. From there, you can use Peewee’s model methods to query and manage records.

Adding commands#

The CommandHook provides an easy way to register commands that interact with a custom hook. Each command method should be decorated using command(), and should accept a Message argument followed by any arguments expected from the user. Keyword arguments are not supported, though optional (with default values) and variable (varargs) parameters may be used.


This replaces the class-level attribute with a Command instance. This instance is callable, allowing you to use the underlying method like any other, though introspection may give surprising results if you rely on it being a traditional method.

Dynamic commands#

You may need to make commands available based on config or state. For simple cases, you can make a command conditional by setting the test field to a method returning True if the command is currently valid.

For more complex hooks, you may instead need to generate new commands based on config. This is where dynamic commands come into play. An unnamed command is effectively a template – it won’t itself be included in command sets, but you can provide qualified instances of it inside DynamicCommands.commands() using BaseCommand.complete().

Identities and access#

If your hook interacts with a third-party service, with users from connected plugs mapping to external users within the service, consider implementing IdentityProvider to make this information available to other hooks like Sync.

You can also use permissions from your service to enforce and restrict user access to channels by integrating AccessPredicate into your hook, to be used with Access.

Incoming HTTP requests#

Some networks may only support listening for messages by subscribing to a webhook. You may also need to provide some web-based UI for certain features. You can use the WebHook resource hook to spin up a local aiohttp.web server, and bind URL paths to it from your plug or hook.

Initial setup is abstracted by the WebContext context – an instance of this can be obtained using WebHook.context(). With this, you can configure new routes into your code like a native aiohttp application.


Routes are named in aiohttp using the module path as their prefix. The helper method WebContext.url_for() provides path resolution without providing the prefix each time.

Jinja2 templates#

If a template name is given to WebContext.route(), the request handler method will be wrapped by aiohttp_jinja2, meaning it should return a dict to act as the template context. See WebContext.env for the variables provided by default.