Advanced Tips & Tricks#
Now that you know the basics about plugins, you may have more questions, or you might have a specific need but can’t quite grasp how to implement it. After all, this documentation alone can’t cover every possible case!
In this chapter, we’ll try to share the many tips and tricks both core developers and plugin authors have found over time.
If something is not in here, feel free to ask about it on our IRC channel, or maybe open an issue with the solution if you devise one yourself.
Running a function on a schedule#
Sopel provides the @plugin.interval
decorator
to run plugin callables periodically, but plugin developers semi-frequently ask
how to run a function at the same time every day/week.
Integrating this kind of feature into Sopel’s plugin API is trickier than one might think, and it’s actually simpler to have plugins just use a library like schedule directly:
import schedule
from sopel import plugin
def scheduled_message(bot):
bot.say("This is the scheduled message.", "#channelname")
def setup(bot):
# schedule the message at midnight every day
schedule.every().day.at('00:00').do(scheduled_message, bot=bot)
@plugin.interval(60)
def run_schedule(bot):
schedule.run_pending()
As long as the bot
is passed as an argument, the scheduled function can
access config settings or any other attributes/properties it needs.
Multiple plugins all setting up their own checks with interval
naturally
creates some overhead, but it shouldn’t be significant compared to all the
other things happening inside a Sopel bot with numerous plugins.
Restricting commands to certain channels#
Allowing games, for example, to be run only in specific channels is a relatively common request, but a difficult feature to support directly in Sopel’s plugin API. Fortunately it is fairly trivial to build a custom decorator function that handles this in a configurable way.
Here is a sample plugin that defines such a custom decorator, plus the scaffolding needed for the plugin to pull its list of channels from the bot’s settings:
import functools
from sopel import plugin
from sopel.config import types
class MyPluginSection(types.StaticSection):
allowed_channels = types.ListAttribute('allowed_channels', default=['#botspam'])
def setup(bot):
bot.settings.define_section('myplugin', MyPluginSection)
def my_plugin_require_channel(func):
@functools.wraps(func)
def decorated(bot, trigger):
if trigger.sender not in bot.settings.myplugin.allowed_channels:
return
return func(bot, trigger)
return decorated
@plugin.command('command_name')
@plugin.require_chanmsg
@my_plugin_require_channel
def my_command(bot, trigger):
bot.say('This is the good channel.')
Important
When using this example in your own plugin code, remember to change
myplugin
to a section name appropriate for your plugin. It is also a
good idea to rename the MyPluginSection
class accordingly.
Note
The example here services the most common situations we have seen users ask for help with on IRC. This kind of decorator could be written in many different ways. Implementation of more complex approaches is left as an exercise for the reader.
Tracking events before/after the bot did#
When a user joins a channel, or quits the server, Sopel will automatically
update the information about said user and channel. For example, when they
join a channel, that information is recorded in
bot.channels
by adding a new
User
object to the correct
channel.users
dict.
That’s all good until you want to do something before or after the change has been recorded by Sopel: you need to be careful how you declare your rules.
Before event#
To handle an event before Sopel records any change, you should use these decorators together:
@plugin.event('event-name') # replace by your event
@plugin.priority('high') # ensure execution before Sopel
@plugin.thread(False) # ensure sequential execution
@plugin.unblockable # optional
def before_event_name(bot, trigger):
# the bot is not updated yet
Requesting high priority and sequential (unthreaded) execution together ensures that anything you do in your callable will be done before Sopel updates its state: users won’t be added or removed yet on JOIN/QUIT.
After event#
To handle an event after Sopel recorded any change, you should use these decorators together:
@plugin.event('event-name') # replace by your event
@plugin.priority('low') # ensure execution after Sopel
@plugin.thread(False) # optional
@plugin.unblockable # optional
def after_event_name(bot, trigger):
# the bot has been updated already
The low priority is enough to ensure that anything you do in your callable will be done after Sopel updated its state: users won’t exist anymore after a QUIT/PART event, and they will be available after a JOIN event.
Note that you don’t specifically need to use @plugin.thread(False)
, but
it is still recommended to prevent any race condition.
Re-using commands from other plugins#
Because plugins are just Python modules it is possible to import functionality from other plugins, including commands. For example, this can be used to add an alias for an existing command:
from sopel import plugin
from sopel.builtins import wikipedia as wp
@plugin.command("wiki")
@plugin.output_prefix(wp.wikipedia.output_prefix)
def wiki_alias(bot, trigger):
wp.wikipedia(bot, trigger)
Warning
Any callables imported from other plugins will be treated as if they were
exposed in the current plugin. This can lead to duplication of plugin
rules. For the most predictable results, import the other plugin as a
module rather than unpacking its callables using a from
import.
Warning
While this example shows off loading a built-in plugin, some plugins may
not be as easy to import. For example, a Single file plugin may
not be available on sys.path
without extra handling not shown here.
Managing Capability negotiation#
Capability negotiation is a feature of IRCv3 that allows a server to advertise a list of optional capabilities, and allows its clients to request such capabilities. You can see that as feature flags, activated by the client.
Capability negotiation takes place after:
connecting to the IRC server
client’s identification (
USER
andNICK
)
And before:
the
RPL_WELCOME
event (001)ISUPPORT
messagesclient’s authentication (except for SASL, which occurs in the capability negotiation phase)
Warning
This is a very advanced feature, and plugin authors should understand how capability negotiation works before using it. Even if Sopel tries to make it as simple as possible, plugin authors should be aware of the known limitations and possible caveats.
Declaring requests: the capability
decorator#
In sopel.plugin
there is an advanced capability
decorator: it is a class that declares a capability request and an optional
handler to run after the capability is acknowledged or denied by the server:
"""Sample plugin file"""
from sopel import plugin
# this will register a capability request
CAP_ACCOUNT_TAG = plugin.capability('account-tag')
# this will work as well
@plugin.capability('message-prefix')
def cap_message_prefix(cap_req, bot, acknowledged):
# do something if message-prefix is ACK or NAK
...
- class sopel.plugin.capability(
- *cap_req: str,
- handler: CapabilityHandler | None = None,
Decorate a function to request a capability and handle the result.
- Parameters:
name – name of the capability to negotiate with the server; this positional argument can be used multiple times to form a single
CAP REQ
handler – optional keyword argument, acknowledgement handler
The Client Capability Negotiation is a feature of IRCv3 that exposes a mechanism for a server to advertise a list of features and for clients to request them when they are available.
This decorator will register a capability request, allowing the bot to request capabilities if they are available. You can request more than one at a time, which will make for one single request.
The handler must follow the
CapabilityHandler
protocol.Note
Due to how Capability Negotiation works, a request will be acknowledged or denied all at once. This means that this may succeed:
@plugin.capability('away-notify')
But this may not:
@plugin.capability('away-notify', 'example/incompatible-cap')
Even though the
away-notify
capability is available and can be enabled, the secondCAP REQ
will be denied because the server won’t acknowledge a request that contains an incompatible capability.In that case, if you don’t need both at the same time, you should use two different handlers:
@plugin.capability('away-notify') def cap_away_notify(cap_req, bot, ack): # handle away-notify acknowledgement @plugin.capability('example/incompatible-cap') def cap_example_incompatible_cap(cap_req, bot, ack): # handle example/incompatible-cap acknowledgement # or, most probably, lack thereof
This will allow the server to acknowledge or deny each capability independently.
Warning
A function cannot be decorated more than once by this decorator, as the result is an instance of
capability
.If you want to handle a
CAP
message without requesting the capability, you should use theevent()
decorator instead.Warning
The list of
cap_req
is limited in size to prevent the bot from separating theCAP REQ
in multiple lines as the bot does not know how to call back the capability handler upon receiving the multi-lineACK * REQ
.See also
The IRCv3 specification on Client Capability Negotiation.
- callback(
- bot: SopelWrapper,
- acknowledged: bool,
Execute the acknowlegement callback of a capability request.
- Parameters:
bot – a Sopel instance
acknowledged – tell if the capability request is acknowledged (
True
) or deny (False
)
- Returns:
a 2-value tuple that contains if the request is done and the result of the handler (if any)
It executes the handler when the capability request receives an acknowledgement (either positive or negative), and returns the result. The handler’s return value is used to know if the capability request is done, or if the bot must wait for resolution from the plugin that requested the capability.
This method returns a 2-value tuple:
the first value tells if the negotiation is done for this request
the second is the handler’s return value (if any)
If no handler is registered, this automatically returns
(True, None)
, as the negotiation is considered done (without any result).This doesn’t prevent the handler from raising an exception.
- property cap_req: tuple[str, ...]#
Capability request as a sorted tuple.
This is the capability request that will be sent to the server as is. A request is acknowledged or denied for all the capabilities it contains, so the request
(example/cap1, example/cap2)
is not the same as two requests, one forexample/cap1
and the other forexample/cap2
. This makes each request unique.
- class sopel.plugin.CapabilityNegotiation(value)#
Capability Negotiation status.
- CONTINUE = 2#
The capability negotiation must continue.
This must be returned by a capability request handler to signify to the bot that the capability requires further processing (e.g. SASL authentication) and negotiation must not end yet.
The plugin author MUST signal the bot once the negotiation is done.
- DONE = 1#
The capability negotiation can end.
This must be returned by a capability request handler to signify to the bot that the capability has been properly negotiated and negotiation can end if all other conditions are met.
- ERROR = 3#
The capability negotiation callback was improperly executed.
If a capability request’s handler returns this status, or if it raises an exception, the bot will mark the request as errored. A handler can use this return value to inform the bot that something wrong happened, without being an error in the code itself.
- class sopel.plugin.CapabilityHandler(*args, **kwargs)#
Protocol
definition for capability handler.When a plugin requests a capability, it can define a callback handler for that request using
capability
as a decorator. That handler will be called upon Sopel receiving either anACK
(capability enabled) or aNAK
(capability denied) CAP message.Example:
from sopel import plugin from sopel.bot import SopelWrapper @plugin.capability('example/cap-name') def capability_handler( cap_req: tuple[str, ...], bot: SopelWrapper, acknowledged: bool, ) -> plugin.CapabilityNegotiation: if acknowledged: # do something if acknowledged # i.e. # activate a plugin's feature pass else: # do something else if not # i.e. use a fallback mechanism # or deactivate a plugin's feature if needed pass # always return if Sopel can send "CAP END" (DONE) # or if the plugin must notify the bot for that later (CONTINUE) return plugin.CapabilityNegotiation.DONE
Note
This protocol class should be used for type checking and documentation purposes only.
- __call__(
- cap_req: tuple[str, ...],
- bot: SopelWrapper,
- acknowledged: bool,
A capability handler must be a callable with this signature.
- Parameters:
cap_req – the capability request, as a tuple of string
bot – the bot instance
acknowledged – that flag that tells if the capability is enabled or denied
- Returns:
the return value indicates if the capability negotiation is complete for this request or not
Working with capabilities#
A plugin that requires capabilities, or that can enhance its features with
capabilities, should rely on
bot.capabilities
’s methods’:
@plugin.command('mycommand')
def mycommand_handler(bot, trigger):
if bot.capabilities.is_enabled('cap1'):
# be fancy with enhanced capabilities
else:
# stick to the basics
The is_enabled()
method in
particular is the most interesting, as it allows a plugin to always know if a
capability is available or not.
Note
Capability negotiation happens after the bot has loaded its plugins and
after the socket connection. As a result, it is not possible to know the
supported and enabled capabilities in the setup
plugin hook.
Ending negotiations#
Sopel automatically sends a CAP END
message when all requests are handled.
However in some cases, a plugin author may need to delay the end of CAP
negotiation to perform an action that must be done first. In that case, a
plugin must return CONTINUE
in its
callback.
This is the case for SASL authentication, as seen in the coretasks
internal plugin that manages that:
@plugin.capability('sasl')
def cap_sasl_handler(cap_req, bot, acknowledged):
# ... <skip for readability> ...
bot.write(('AUTHENTICATE', mech))
# If we want to do SASL, we have to wait before we can send CAP END.
# So if we are, wait on 903 (SASL successful) to send it.
return plugin.CapabilityNegotiation.CONTINUE
Later on, the plugin uses the
resume_capability_negotiation()
method to tell the bot
that the request is complete, and the bot will send the CAP END
automatically:
@plugin.event(events.RPL_SASLSUCCESS)
@plugin.thread(False)
@plugin.unblockable
@plugin.priority('medium')
def sasl_success(bot: SopelWrapper, trigger: Trigger):
"""Resume capability negotiation on successful SASL auth."""
LOGGER.info("Successful SASL Auth.")
bot.resume_capability_negotiation(
cap_sasl_handler.cap_req,
'coretasks'
)
Important
Plugin callables that modify the bot’s capability negotiation state should
always use @plugin.thread(False)
and @plugin.unblockable
to prevent
unwanted race conditions.