Anatomy of a plugin¶
A Sopel plugin consists of a Python module containing one or more
callable
s. It may optionally also contain configure
, setup
, and
shutdown
hooks.
Defining rules¶
The main goal of a Sopel plugin is to react to IRC messages. For that, Sopel uses a Rule system: plugins define rules, which Sopel loads, and then Sopel triggers any matching rules for each message it receives.
Sopel identifies a callable as a rule when it has been decorated with any of
these decorators from sopel.plugin
:
Generic rule:
rule()
,find()
, andsearch()
(and their lazy versions:rule_lazy()
,find_lazy()
, andsearch_lazy()
)Named rule:
command()
,action_command()
, andnickname_command()
URL callback:
url()
(and its lazy version,url_lazy()
)
Additionally, Sopel identifies a callable as a generic rule when these decorators are used alone:
In that case, it will use a match-all regex (r'.*'
):
from sopel import plugin
@plugin.event('JOIN')
def on_join(bot, trigger):
pass
# the above is equivalent to this:
@plugin.rule(r'.*')
@plugin.event('JOIN')
def on_join(bot, trigger):
pass
Channel vs. private messages¶
By default, rules can be triggered from a channel or a private message. It is possible to limit that to either one of these options:
channel only:
sopel.plugin.require_chanmsg()
private message only:
sopel.plugin.require_privmsg()
Access right requirements¶
By default anyone can trigger a rule, and for some it might be better to limit who can trigger them. There are decorators for that:
sopel.plugin.require_account()
: requires services/NickServ authentication; works only if the server implements modern IRC authentication (see alsoTrigger.account
and the account-tag specification for more information)sopel.plugin.require_privilege()
: requires a specific level of privileges in the channel; works only for channel messages, not private messages, and you probably want to use it withrequire_chanmsg()
sopel.plugin.require_admin()
: only the bot’s owner and its admins can trigger the rulesopel.plugin.require_owner()
: only the bot’s owner can trigger the rule
Sometimes it’s not the channel privilege level of the user who triggers a command that matters, but the bot’s privilege level. For that, there are two options:
sopel.plugin.require_bot_privilege()
: this decorator is similar to therequire_privilege
decorator, but it checks the bot’s privilege level instead of the user’s; works only for channel messages, not private messages; and you probably want to use it with therequire_chanmsg
decorator.bot.has_channel_privilege()
is a method that can be used to check the bot’s privilege level in a channel, which can be used in any callable.
Rate limiting¶
All rules can have rate limiting with the
sopel.plugin.rate()
decorator. Rate limiting means how often a rule can
be triggered. This is different from the flood protection logic, which is how
often Sopel can send messages to the network. By default, a rule doesn’t have
any rate limiting.
There are three types of rate limiting:
per-user: how often a rule triggers for each user
per-channel: how often a rule triggers for a given channel
globally: how often a rule triggers accross the whole network
Example:
from sopel import plugin
@plugin.rule(r'Ah[!?.]?')
@plugin.rate(user=2)
def you_said_ah(bot, trigger):
bot.reply('Ha AH!')
A rule with rate-limiting can return sopel.plugin.NOLIMIT
to let the
user try again after a failed command, e.g. if a required argument is missing.
Rule labels¶
A rule has a label: it will be used for logging, documentation, and internal manipulation. There are two cases to consider:
Generic rules and URL callbacks use their callable’s name by default (i.e. the function’s
__name__
). This can be overridden with thesopel.plugin.label()
decorator.A Named rule is already named (by definition), so it uses its name directly as rule label. This can’t be overridden by a decorator.
This label is particularly useful for bot owners who want to disable a rule in
a specific channel. In the following example, the say_hello
rule from the
hello
plugin is disabled in the #rude
channel:
[#rude]
disable_commands = {'hello': ['say_hello']}
The rule in question is defined by the hello
plugin like so:
@plugin.rule(r'hello!?', r'hi!?', r'hey!?')
@plugin.label('say_hello')
def handler_hello(bot, trigger):
bot.reply('Ha AH!')
Plugin callables¶
When a message from the IRC server matches a Rule, Sopel will execute its attached callable. All plugin callables follow the same interface:
-
plugin_callable
(bot, trigger)¶ - Parameters
bot (
sopel.bot.SopelWrapper
) – wrapped bot instancetrigger (
sopel.trigger.Trigger
) – the object that triggered the call
A callable must accept two positional arguments: a
bot
object, and a
trigger
object. Both are tied to the specific
message that matches the rule.
The bot
provides the ability to send messages to the network (to say
something or to send a specific command such as JOIN
), and to check the
state of the bot such as its settings, memory, or database. It is a context
aware wrapper around the running Sopel
instance.
The trigger
provides information about the line which triggered the rule
and this callable to be executed.
The return value of a callable is ignored unless it is
sopel.plugin.NOLIMIT
, in which case
rate limiting will not be applied for that call.
(See sopel.plugin.rate()
.)
Note
Note that the name can, and should, be anything, and it doesn’t have to be
called plugin_callable
. At least, it should not be called callable
,
since that is a Python built-in function
:
from sopel import plugin
@plugin.command('hello')
def say_hello(bot, trigger):
"""Reply hello to you."""
bot.reply('Hello!')
Plugin jobs¶
Another feature available to plugins is the ability to define
jobs. A job is a Python callable decorated with
sopel.plugin.interval()
, which executes the callable
periodically on a schedule.
A job follows this interface:
-
plugin_job
(bot)¶ - Parameters
bot (
sopel.bot.Sopel
) – the bot instance
Note
Note that the name can be anything, and it doesn’t have to be called
plugin_job
:
from sopel import plugin
@plugin.interval(5)
def spam_every_5s(bot):
if "#here" in bot.channels:
bot.say("It has been five seconds!", "#here")
Important
A job may execute while the bot
is not connected, and it must not
assume any network access.
Plugin setup & shutdown¶
When loading and unloading plugins, a plugin can perform setup and shutdown
actions. For that purpose, a plugin can define optional functions named
setup
and shutdown
. There can be one and only one function with each
name for a plugin.
Setup¶
The setup
function must follow this interface:
-
setup
(bot)¶ - Parameters
bot (
sopel.bot.Sopel
) – the bot instance
This function is optional. If it exists, it will be called while the plugin is
being loaded. The purpose of this function is to perform whatever actions are
needed to allow a plugin to do its work properly (e.g, ensuring that the
appropriate configuration variables exist and are set). Note that this normally
occurs prior to connection to the server, so the behavior of the messaging
functions on the sopel.bot.Sopel
object it’s passed is undefined and
they are likely to fail.
Throwing an exception from this function will stop Sopel from loading the plugin, and none of its rules or jobs will be registered. The exception will be caught, an error message logged, and Sopel will try to load the next plugin.
This is useful when requiring the presence of configuration values (by raising
a ConfigurationError
error) or making other environmental
requirements (dependencies, file/folder access rights, and so on).
The bot will not continue loading plugins or connecting during the execution of this function. As such, an infinite loop (such as an unthreaded polling loop) will cause the bot to hang.
Shutdown¶
The shutdown
function must follow this interface:
-
shutdown
(bot)¶ - Parameters
bot (
sopel.bot.Sopel
) – the bot instance
This function is optional. If it exists, it will be called while the bot
is shutting down. Note that this normally occurs after closing connection
to the server, so the behavior of the messaging functions on the
bot
object it’s passed is undefined and they are
likely to fail.
The purpose of this function is to perform whatever actions are needed to allow a plugin to properly clean up after itself (e.g. ensuring that any temporary cache files are deleted).
The bot will not continue notifying other plugins or continue quitting during the execution of this function. As such, an infinite loop (such as an unthreaded polling loop) will cause the bot to hang.
New in version 4.1.
Plugin configuration¶
A plugin can define and use a configuration section. By subclassing
sopel.config.types.StaticSection
, it can define the options it uses
and may require. Then, it should add this section to the bot’s settings:
from sopel.config import types
class FooSection(types.StaticSection):
bar = types.ListAttribute('bar')
fizz = types.ValidatedAttribute('fizz', bool, default=False)
def setup(bot):
bot.settings.define_section('foo', FooSection)
This will allow the bot to properly load this part of the configuration file:
[foo]
bar =
spam
eggs
bacon
fizz = yes
See also
The define_section()
method to define a new
section so the bot can parse it properly.
Configuration wizard¶
When the owner sets up the bot, Sopel provides a configuration wizard. When a
plugin defines a configure
function, the user will be asked if they want
to configure said plugin, and if yes, this function will execute.
The configure
function must follow this interface:
-
configure
(settings)¶ - Parameters
settings (
sopel.config.Config
) – the bot’s configuration object
Its intended purpose is to use the methods of the passed
sopel.config.Config
object in order to create the configuration
variables it needs to work properly.
New in version 3.0.
Example:
def configure(config):
config.define_section('foo', FooSection)
config.foo.configure_setting('bar', 'What do you want?')
config.foo.configure_setting('fizz', 'Do you fizz?')
Note
The configure
function is called only from the command line, and
network access must not be assumed.
This process doesn’t call the bot’s setup
or shutdown
functions, so
this function must define the configuration section it wants to use.
See also
The define_section()
method to define a new
section, and the configure_setting()
method to prompt the user to set an option.