Skip to content

Plugin and Data API

Christian Krüger edited this page Oct 19, 2021 · 8 revisions

The Geckarbot is a modular Bot. Each command and functionality is provided by plugins. Each plugin is loaded via the Plugin API. To manage configurations for plugins there's also a Data API to provide loading and saving different configurations for different plugins.

Basic structure

Our basic structure provides four types of components besides the bot kernel which contains the Plugin and Data APIs:

  • Plugins are modular files that provide commands for the user. They are completely optional from a technical standpoint.
  • Core plugins are modular files that rely on specific global states in the bot itself or modify the behaviour of all other plugins. They don't have to be optional.
  • Utils are tools that don't require a global state management and can be used by plugins at will. Since plugins rely on them, they are not optional once implemented (and used).
  • Subsystems are tools that require a global state management and can be used by plugins at will. They are not optional and their global state is handled by the bot itself.

Plugin API

Like described, we differ two types of plugins: Core plugins and regular plugins. Core plugins rely on the global state and modify it, while regular plugins aren't allowed similar things. To do this, core plugins are loaded directly at program start while regular plugins are loaded after establishing the connection to the discord server.

General

When the bot is starting, the following things happens in this order:

  1. Load the bot's general config from geckarbot.json
  2. Load pre bot initializing injections
  3. Load logging setup
  4. Initialize Bot object including initializing Subsystems and Data API
  5. Load post bot initializing injections
  6. Load core plugins
  7. Start Bot including connecting to discord server
  8. Load plugins

Plugin boilerplate

A plugin's file has to be saved in the plugins directory. Inside the module, a class Plugin which inherits from base.BasePlugin must be defined with the following __init__ function:

from base.configurable import BasePlugin
from base.data import Config

class Plugin(BasePlugin, name="Testplugin"):

    def __init__(self):
        super().__init__()
        Config().bot.register(self)

The BasePlugin class defines the Plugin class as a discord.py Cog which can define commands for the bot. It also inherits from Configurable which provides following attributes and functions:

  • self.can_configdump: Defines if the !configdump command is allowed to print the config data of the plugin, e.g. if the config contains login credentials.
  • self.dump_except_keys: Defines a List[str] of top level keys in the Config or Storage of the plugin which won't be printed using !configdump or !storagedump commands, e.g. for keys which are containing login credentials.
  • default_config(self): Provides a default configuration if the plugins configuration can't be loaded. Returns an empty dictionary if not overwritten by the plugin.
  • default_storage(self): Provides a default storage if the plugins configuration can't be loaded. Returns an empty dictionary if not overwritten by the plugin.
  • get_lang(self): Can be used to provide a lang dictionary instead of a lang file, see [Multilanguage support](#Multilanguage support).
  • get_lang_code(self): Override this to use a specific language code for the plugin, e.g. if the plugin is only available in one language.
  • get_name(self): Returns the human-readable plugin name
  • get_configurable_type(self): Returns the ConfigurableType like PLUGIN, SUBSYSTEM or COREPLUGIN.

The BasePlugin class itself provides following function:

  • shutdown(self): Coroutine that is called when the bot or the plugin is shutting down. If a plugin have cleanup things to do on shutdown, do it here.
  • command_help(self, ctx, command): This coroutine is called when a !help command is executed that concerns a command that originated from this plugin. Override this to handle help commands yourself. If you want to resume the default help command handling, raise base.NotFound.
  • command_help_string(self, command): Override this method to return a custom help string for command command that is determined at runtime (e.g. for localization).
  • command_description(self, command): Override this method to return a custom description string for command command that is determined at runtime (e.g. for localization).
  • command_usage(self, command): Override this method to return a custom usage string for command command that is determined at runtime (e.g. for localization).
  • sort_commands(self, ctx, command, subcommands): Override to sort the subcommands of command yourself for displaying in help command.

The Plugin API creates the neccessary instances and gives the bot instance to instantiate the Cog. The Plugin must register itself via bot.register(self). The register process has following states:

  1. Register the plugins Cog including commands
  2. Create the ConfigurableContainer and add it to the bots plugin list
  3. Load the config, storage and lang

More information about Cogs can be found at discord.py Documentation.

Core plugins

Core plugins are allowed to provide some extra features for other plugins and the bot itself. If some registration functions needs data which the bot can access after connecting to a server only, the registration can't be done directly in the __init__() function, but via the on_ready() event listeners:

from base.configurable import BasePlugin
from base.data import Config

class Plugin(BasePlugin, name="Testplugin"):
    def __init__(self):
        super().__init__()
        Config().bot.register(self)

        @bot.listen()
        async def on_ready():
            do_stuff()

    def get_configurable_type(self):
        return ConfigurableType.COREPLUGIN

Since core plugins also inherits from BasePlugin they have to overwrite the get_configurable_type() method to return ConfigurableType.COREPLUGIN which will be used by the Data API.

Subsystems

Subsystems are core features provided by the bot itself. To create a subsystem, the subsystem class must be inherit by base.BaseSubsystem, which also inherits all attributes and functions from Configurable:

from base.configurable import BaseSubsystem
from base.data import Config

class Ignoring(BaseSubsystem):
    def __init__(self):
        super().__init__()
        Config().bot.plugins.append(ConfigurableContainer(self))

If a subsystem has a config or a storage that is needed to manage by the Data API it has to create a ConfigurableContainer object and append it to the bots plugin list by itself.

Subsystems must also be added to the Geckarbot class like:

class Geckarbot(commands.Bot):
    def __init__(self, *args, **kwargs):
        ...
        self.ignoring = ignoring.Ignoring(self)

Data API

The Data API provides configuration, storage and language/string management for plugins, subsystems and every class which inherits from base.Configurable and is listed in bot.plugins list. To access the Data API the plugin has to be registered via the Plugin API. Like mentioned before, during registration on the Plugin API, a plugin will also be registered on the Data API and its config will be loaded from its configuration file.

The configuration or storage for a plugin is provided as dictionaries, so the raw data provided by the Data API has to be converted by the plugin itself if needed on saving/loading.

The Data API inside the module data provides two classes for saving the plugins configuration:

  • Config for general plugin config
  • Storage for data which will be created by the plugin during runtime

Accessing Config and Storage

To access the Config and Storage of a plugin, following methods can be used:

  • get(plugin): Returns the data of the given plugin instance. If plugin is not registered, None will be returned.
  • set(plugin, config): Sets the given config object as config/storage for the given plugin. Must be serializable to json!
  • save(plugin): Saves the data of the given plugin instance to json file.
  • load(plugin): Loads the configuration of the given plugin instance from json file. If errors occured during loading, False will be returned and the default config for the plugin will be loaded. If configuration is successfully loaded, True will be returned, and None if plugin is not registered.

Resource Directory

Each plugin also has a resource directory in which additional resource files like pictures or music files can be stored. The directory is named resource/<pluginname>/. They can accessed like:

file = discord.File(f"{Config.resource_dir(self)}/treecko.jpg")

Multilanguage support

The Data API provides a language/string feature to get strings from a stringfile or string dictionary to output it for user in their language. The string file has to be provided as json in lang/<pluginname>.json. The basic structure contains the localization code of the provided language as keys and for each language a dictionary containing the actual strings:

{
  "en_US": {
    "bully_msg": "{} is bulling us :frowning:",
    "bully_msg_self": "I bully you :slight_smile:"
  },
  "de_DE": {
    "bully_msg": "{} mobbt uns :frowning:",
    "bully_msg_self": "Ich mobbe euch :slight_smile:"
  }
}

If the lang file is provied for example as python dictionary, it must have the same structure and can be set using the get_lang() method. The lang strings can be accessed using conf.Lang.lang(plugin, str_name, *args) like:

    async def who_mobbing(self, ctx):
        ...
        if bully is self.bot.user:
            text = Lang.lang(self, "bully_msg_self")
        else:
            text = Lang.lang(self, "bully_msg", utils.get_best_username(bully))
        await ctx.send(text)

As you can see, you can use place holder for pythons str.format() function. All additional arguments which will be give via *args to the Lang.lang() method will be used to insert it using str.format().

To decide the output language, first the language code in Config.LANGUAGE_CODE will be used, and if this language doesn't contain the str_name, the english (en_US) string will be used. If the corresponding string can't be found, the str_name itself will be returned. If instead None should returned, use Lang.lang_no_failsafe().

Emoji collection

The Lang class also provides a emoji collection for the letter based emojis and for success and error marks. It can be accessed via the Lang.EMOJI dictionary or using following attributes:

  • Lang().CMDSUCCESS: ✅
  • Lang().CMDERROR: ❌
  • Lang().CMDNOCHANGE: 🤷‍♀
  • Lang().CMDNOPERMISSIONS: ❌

Tips, tricks and some notes

Fast access to plugin config

To fast access without typing Config.get(self)['config_key'] (or for Storage) every time to access the plugins configuration, you can create a function for it:

    def conf_a(self):
        return Config().get(self)['a']

If a plugin manages it's configuration by itself or must convert the configuration or storage provided by the Data API, a load() and/or save() method can be used and called on plugin initialization or on saving data.

Clone this wiki locally