Sopel

The Python IRC Bot

Sopel 8.0 Migration

Version 8.0 brings Sopel into the modern era of Python, finally dropping support for end-of-life Python releases as of December 2023, including Python 2 and several older versions of Python 3. This allows us to make significant improvements under the hood that were held back by these legacy versions, e.g. the IRC connection backend and Sopel’s event system.

Owner/admin usage changes

Updated Python requirements & support policy

Sopel 8.0 requires Python 3.8 or higher. Support for Python 2.7 and 3.3–3.7 has been removed. In exchange, known issues sometimes preventing use of Sopel 7 with Python 3.11+ have been fixed.

During the lifecycle of Sopel 8.x, we will do our best to test against new Python releases as they come. We will continue to test against Python versions that reach end-of-life unless it becomes impractical to do so—in which case the next maintenance release of Sopel will officially drop support for the Python version(s) that can no longer be tested.

CLI changes

Sopel 8 continues the command-line interface overhaul we began in version 7, mostly in the form of removing support for legacy usage from Sopel’s 6.x era.

The bare sopel command now offers only basic control. Most of its legacy options have been removed:

Legacy command Modern command
sopel --quit or sopel -q sopel stop
sopel --kill or sopel -k sopel stop --kill or sopel stop -k
sopel --restart or sopel -r sopel restart
sopel --configure-all or sopel -w sopel configure
sopel --configure-modules sopel configure --plugins
sopel --list or sopel -l sopel-config list
sopel -v sopel -V / sopel --version
sopel --quiet n/a; this feature didn’t work anyway

Ignore system

In Sopel 8, “hostmask” blocks are now called “host” blocks, which reflects the reality of how those values are used by Sopel. Bot admins’ muscle memories will have to learn to type e.g. .blocks add host instead of .blocks add hostmask.

We intend to add actual “hostmask” blocking in a future Sopel release.

Note that the config file option for “hostmask” blocks already was, and still is, named host_blocks. This change only affects interactive blocklist editing by bot admins via IRC commands.

Configuration & plugin-handling changes

What’s a “module”, doc?

Sopel 8 no longer loads plugins from the <homedir>/modules directory by default. To migrate, rename this directory to plugins or declare it in the extra field of the [core] configuration section.

This is part of our efforts to clarify the difference between a “module” (which is a Python concept) and a “plugin” (which is a special kind of “module” that can extend Sopel with new functionality).

Encrypted IRC by default

Unless its config file specifies different values, Sopel 8 assumes that it should use_ssl and connect to the standard encrypted port 6697.

Older versions assumed they should not use_ssl and use port 6667.

Taming logging to a channel

DEBUG is no longer a valid choice for the logging_channel_level setting, and logging_channel_level is no longer inherited from the main logging_level setting. The new default logging_channel_level is WARNING.

Having Sopel output its log messages to an IRC channel of your choice can be convenient, but if DEBUG logs could be sent to IRC, it was possible to have Sopel give you too much of a good thing and get stuck. Every debug message sent to IRC would itself generate multiple additional debug log entries, filling the bot’s outbound message queue—and the best solution to prevent deadlocks is to simply prohibit DEBUG from being used with logging_channel_level.

Farewell, Phenny/Jenni

The project that eventually became Sopel diverged from Jenni in 2012. After more than a decade of maintaining backward compatibility (in a theoretical sense, at least) with plugins originally written for Jenni and its predecessor Phenny, Sopel 8.0 bids a fond farewell to plugin code from that era.

Beyond the maintenance burden of making sure Sopel could still technically load such old plugins, we felt it was disingenuous to continue “supporting” them when the rest of Sopel’s API has changed and evolved so much that the chance of such old plugins actually still working is now very low indeed. Thus, we’re not dropping support for those decade-old plugins as much as admitting that things have changed a lot and users would be better off seeking (or if necessary, writing) newer plugins with similar functionality.

Sopel 8 plugin changes

Removed built-in plugins

Sopel’s built-in plugins are slowly being migrated out to their own standalone packages. In 8.0, we say farewell to:

Over the longer term, this will help the Sopel project manage plugin maintenance and updates more quickly.

Notable changes to built-in plugins

currency plugin

The fiat_provider setting now takes precedence over the fixer_io_key. Previously, setting a fixer_io_key would use the fixer.io fiat exchange rate provider regardless of the fiat_provider setting.

exchangerate.host is no longer a valid fiat_provider choice; it has been replaced by the new default, open.er-api.com.

reload plugin

The .update command has been removed from the reload plugin. It relied on running Sopel in a way that we don’t officially support, so we chose to remove the command.

To replace .update, if you were using it, we recommend writing an external script that stops Sopel, updates the components you want, and then starts Sopel again.

wikipedia plugin

The old command names (.w, .wik, and .wiki) are no longer used; they were freed up for use by bespoke plugins. wikipedia functionality is now invoked by the .wikipedia command, or .wp for short.

The deprecated lang_per_channel config-file setting has been removed. Chanops and Sopel admins can set the preferred wiki language for a channel using the .wpclang command instead.

Sopel 8 API changes

Changes that could be particularly surprising are marked with a ⚠️ emoji.

⚠️ Command names are now literal

Sopel 8 no longer supports regular expressions in command names, which are now escaped during plugin loading. This change helps plugin developers avoid unintentional interference with the default capture groups for commands, and also clears the way for enhancements to plugin/command management.

Notes in the documentation for @sopel.plugin.command() list possible Sopel 8-compatible adaptations for a few common use cases.

⚠️ Time-handling changes

trigger.time is now an “aware” datetime object, meaning it has a UTC offset associated with it. Comparison or arithmetic operations between “aware” and “naïve” datetime objects are not allowed; code that manipulates trigger.time values will need to make sure “naïve” datetime objects created, or retrieved from sources, outside the bot are converted to “aware” values.

The fallback format string used by sopel.tools.time.format_time() changed from outputting the timezone name (UTC) to the UTC offset (+0000). This format string is used only if there is no preferred format set for the nick, channel, and/or config passed to the function.

format_time()’s handling of “aware” and “naïve” datetimes was also improved, but those changes should be transparent to both users and plugin developers. Let us know if you encounter situations where format_time() behaves surprisingly.

sopel.tools.time.validate_timezone() now also raises a ValueError even if the timezone to be validated is None. This was previously a special case that returned None.

Enums everywhere

(Insert the classic Buzz Lightyear meme here, if you want.)

The following parts of Sopel’s API each became an enumeration of some kind:

Now that Sopel is squarely in the Python 3 world, using Enums where they make sense will let us gradually improve how some parts of its API fit together in the long run.

None of these changes are expected to require any immediate code updates for correct function. However, code updates might be required in projects that make use of type-checking.

Identifier casemapping

Any reasonably modern IRC server advertises its method for normalizing nicknames to every client that connects. Sopel 8 builds on the bot.isupport mechanism added in Sopel 7 and makes use of the advertised CASEMAPPING value when comparing and normalizing nicknames itself.

To support handling this under the hood, the bot’s config.core.nick setting is now stored and returned as a normal str. It and any other strings that you’d like to treat as nicknames should be passed through the bot.make_identifier() method to get the same Identifier type you’re used to from Sopel 7, configured to use the IRC server’s advertised casemapping method.

Database changes

bot.db.get_nick_id() no longer creates a new ID by default if its create parameter is unspecified.

Plugin code doesn’t usually need to use this method. But if you do use it, you’ll possibly need to handle a new ValueError exception when calling this method on a nick that hasn’t yet been assigned an ID.

IRC event changes

The event names RPL_INVITELIST and RPL_ENDOFINVITELIST have been updated per clarifications from the “Modern IRC” living-specification project. RPL_INVEXLIST and RPL_ENDOFINVEXLIST were added to the list of named events that Sopel knows about.

Plugin code might need to be updated as a result of these changes, but keep in mind that different IRC servers mean different things when they send these events. If in doubt, specify a raw numeric value (e.g. '346'). Writing truly cross-network plugins that react to these events should be undertaken very carefully.

IRC connection status monitoring

bot.connection_registered now indicates that a connection is open, the IRC server has accepted the bot as a client, and the bot can send commands. Previously, this boolean value indicated only that a connection was open.

This is useful to plugins with code that runs without a triggering IRC event but still outputs to IRC, such as a @sopel.plugin.interval() job or acting on data received via a socket. if not bot.connection_registered, the output can be skipped, retried after a delay, etc.

More consistent trigger objects

⚠️ STATUSMSG handling

Events sent to a channel can be scoped to users with a particular privilege level, or status, in that channel. IRC servers advertise the availability of this feature using the STATUSMSG token in RPL_ISUPPORT.

In Sopel 7, trigger.sender included the status prefix, and it was difficult for plugins to detect and remove it themselves if so desired. Sopel 8.0 removes the status prefix from trigger.sender and leaves only the channel name, while the status prefix (if any) is exposed as trigger.status_prefix. This change is useful, for example, to plugins that use trigger.sender to store or retrieve channel values in bot.db.

The bot passed to a plugin callable triggered by an IRC event automatically includes the status prefix, if present, in the default destination parameter to methods that send a message to IRC. Plugin callables that simply invoke bot.say(), bot.reply(), etc. in direct response to a trigger and without overriding the default destination will most likely get the expected behavior.

sopel-remind is an example of one kind of plugin that this change [might surprise][remind-statusmg]. Naïve implementation of such a feature might simply store (trigger.nick, trigger.sender, parsed_message) and send bot.reply(parsed_message, trigger.sender, trigger.nick) after the specified amount of time has passed. The trigger.status_prefix is lost, and the reminder will be sent to the entire channel instead of only the status-limited subset of users who could have seen the original command.

Only set the sender when it makes sense

Sopel 8.0 now only sets trigger.sender for the following events:

In all other cases, the sender will be None and plugin code should use the trigger.args list to retrieve all information about the event.

For general IRC events like QUIT, RPL_NAMREPLY, etc. that don’t happen “in” a channel or other specific context, the trigger.sender value that plugin callables saw in Sopel 7 was unpredictable at best. Writing correct behavior should be more intuitive with this change.

To have text, you must first have args

trigger.text will now be empty ('') if the event carries no args. In Sopel 7, trigger.text contained the command name (e.g. 'QUIT') in these cases, but that was a bug.

IRC privilege requirement changes

In Sopel 8.0, the @sopel.plugin.require_privilege() decorator now implies @sopel.plugin.require_chanmsg(), and the decorated callable will not run if triggered outside of a channel. Previously, require_privilege restrictions were simply ignored, and the callable would run anyway.

The @sopel.plugin.require_bot_privilege() decorator also now implies @sopel.plugin.require_chanmsg(), with the same associated behavior change.

Capability negotiation rework

The old CapReq method of asking Sopel to negotiate additional capabilities on behalf of your plugin has been replaced with a much more robust system based on the new sopel.plugin.capability class/decorator.

Plugins can now request capabilities in one of two ways:

"""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
    ...

The first method is easier if you just want to require a capability. The second is easier if your plugin needs to do something in response to the IRC server’s response to the capability request. See the documentation for more details about plugins and managing capabilities.

The bot’s hostmask

In Sopel 8, accessing bot.hostmask when the bot lacks sufficient data to determine its own hostmask will return None. The behavior in Sopel 7 was to raise a KeyError.

Testing tool changes

The MockIRCServer mock object from Sopel’s pytest plugin implements blocking as a keyword-only argument in Sopel 8. This applies to:

To upgrade test cases using this mock, simply add blocking= to this parameter of the method call if it’s missing.

Moved API features

We wanted to organize things better in Sopel 8.0, so some of the API features have moved. The old locations will still work until Sopel 9.0, but you’ll be way ahead of the game if you update your plugins now!

Removed API features

Previously deprecated parts of Sopel’s API have been removed, including:

Deprecated API features

More small legacy API features have been deprecated in version 8.0:

We continue to reorganize, modernize, and improve various parts of Sopel’s API whenever possible. Thank you for your patience!