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 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: 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 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 capability.

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

Warning

The list of cap_req 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.

callback(
bot: SopelWrapper,
acknowledged: bool,
) tuple[bool, CapabilityNegotiation | None]#

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 for example/cap1 and the other for example/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 an ACK (capability enabled) or a NAK (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,
) CapabilityNegotiation#

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.