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

import sopel_someplugin as sp

@plugin.command("new_command")
@plugin.output_prefix(sp.PLUGIN_OUTPUT_PREFIX)
def someplugin_alias(bot, trigger):
    sp.plugin_command(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

Some plugins may not be as easy to import as the example shown here. 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 and NICK)

And before:

  • the RPL_WELCOME event (001)

  • ISUPPORT messages

  • client’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. This decorator returns an instance of Capability 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
    ...
sopel.plugin.capability(
*name: str,
handler: CapabilityHandler | None = None,
) Capability

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 sopel.plugins.callables.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 second CAP 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 sopel.plugins.callables.Capability.

If you want to handle a CAP message without requesting the capability, you should use the event() decorator instead.

Warning

The list of names (name) is limited in size to prevent the bot from separating the CAP REQ in multiple lines as the bot does not know how to call back the capability handler upon receiving the multi-line ACK * REQ.

See also

The IRCv3 specification on Client Capability Negotiation.

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.