-
Notifications
You must be signed in to change notification settings - Fork 2
Plugin and Data API
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.
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.
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.
When the bot is starting, the following things happens in this order:
- Load the bot's general config from geckarbot.json
- Load pre bot initializing injections
- Load logging setup
- Initialize Bot object including initializing Subsystems and Data API
- Load post bot initializing injections
- Load core plugins
- Start Bot including connecting to discord server
- Load plugins
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!configdumpcommand 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!configdumpor!storagedumpcommands, 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 theConfigurableTypelikePLUGIN,SUBSYSTEMorCOREPLUGIN.
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!helpcommand 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, raisebase.NotFound. -
command_help_string(self, command): Override this method to return a custom help string for commandcommandthat is determined at runtime (e.g. for localization). -
command_description(self, command): Override this method to return a custom description string for commandcommandthat is determined at runtime (e.g. for localization). -
command_usage(self, command): Override this method to return a custom usage string for commandcommandthat is determined at runtime (e.g. for localization). -
sort_commands(self, ctx, command, subcommands): Override to sort the subcommands ofcommandyourself 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:
- Register the plugins Cog including commands
- Create the
ConfigurableContainerand add it to the bots plugin list - Load the config, storage and lang
More information about Cogs can be found at discord.py Documentation.
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.COREPLUGINSince 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 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)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:
-
Configfor general plugin config -
Storagefor data which will be created by the plugin during runtime
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.
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")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().
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: ❌
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.