Simple and extensible IRC bot framework written in Python with plugin architecture and database support
—
Sopel's plugin system is the core of its extensibility, providing a decorator-based approach for creating bot functionality. Plugins are Python modules that use decorators to register functions as commands, rules, events, and other bot behaviors.
Register functions to respond to specific IRC commands triggered by a command prefix (typically . or !).
def command(*command_names: str):
"""
Register a function as a command.
Args:
*command_names (str): One or more command names that trigger this function
Example:
@plugin.command('hello')
def hello_cmd(bot, trigger): ...
@plugin.command('hi', 'hello', 'hey')
def greet_cmd(bot, trigger): ...
"""
commands = command
"""
Alias to command() decorator.
This is an alias for the command() decorator, not a separate function.
Use @plugin.command() instead.
"""
def action_command(command_name: str):
"""
Register a function as an action command (CTCP ACTION).
Args:
command_name (str): The command name for ACTION messages
"""
def action_commands(*command_names: str):
"""
Register a function for multiple action commands.
Args:
*command_names (str): Multiple action command names
"""
def nickname_command(command_name: str):
"""
Register a function triggered when bot's nick is used as command.
Args:
command_name (str): The command name after bot's nickname
Example:
@plugin.nickname_command('hello')
def nick_hello(bot, trigger): ...
# Triggered by: "BotName: hello"
"""
def nickname_commands(*command_names: str):
"""
Register a function for multiple nickname commands.
Args:
*command_names (str): Multiple nickname command names
"""Register functions to respond to messages matching regular expression patterns.
def rule(pattern: str):
"""
Register a function to trigger on regex pattern matches.
Args:
pattern (str): Regular expression pattern to match
Example:
@plugin.rule(r'.*\b(hello|hi)\b.*')
def greet_rule(bot, trigger): ...
"""
def rule_lazy(lazy_loader):
"""
Register a function with a lazy-loaded regex pattern.
Args:
lazy_loader (callable): Function returning list of regex patterns
"""
def find(pattern: str):
"""
Register a function to find pattern anywhere in message.
Args:
pattern (str): Pattern to search for in messages
"""
def find_lazy(lazy_loader):
"""
Register a function with lazy-loaded find patterns.
Args:
lazy_loader (callable): Function returning list of patterns
"""
def search(pattern: str):
"""
Register a function for pattern search with match groups.
Args:
pattern (str): Search pattern with capture groups
"""
def search_lazy(lazy_loader):
"""
Register a function with lazy-loaded search patterns.
Args:
lazy_loader (callable): Function returning list of search patterns
"""Register functions to respond to specific IRC events and protocol messages.
def event(*event_types: str):
"""
Register a function to handle specific IRC events.
Args:
*event_types (str): IRC event types to handle (JOIN, PART, QUIT, etc.)
Example:
@plugin.event('JOIN')
def on_join(bot, trigger): ...
"""
def ctcp(ctcp_command: str):
"""
Register a function to handle CTCP commands.
Args:
ctcp_command (str): CTCP command to handle (VERSION, PING, etc.)
Example:
@plugin.ctcp('VERSION')
def ctcp_version(bot, trigger): ...
"""Register functions to process URLs in messages with automatic URL detection and processing.
def url(url_pattern: str):
"""
Register a function to handle URLs matching a pattern.
Args:
url_pattern (str): Regex pattern for URL matching
Example:
@plugin.url(r'https?://example\.com/.*')
def handle_example_url(bot, trigger): ...
"""
def url_lazy(lazy_loader):
"""
Register a function with lazy-loaded URL patterns.
Args:
lazy_loader (callable): Function returning list of URL patterns
"""Register functions to run at regular intervals or specific times.
def interval(seconds: int):
"""
Register a function to run at regular intervals.
Args:
seconds (int): Interval between executions in seconds
Example:
@plugin.interval(300) # Every 5 minutes
def periodic_task(bot): ...
"""Decorators to restrict plugin functions based on user privileges and message context.
def require_privmsg():
"""
Restrict function to private messages only.
Example:
@plugin.require_privmsg()
@plugin.command('secret')
def secret_cmd(bot, trigger): ...
"""
def require_chanmsg():
"""
Restrict function to channel messages only.
"""
def require_account():
"""
Require user to be authenticated with services.
"""
def require_admin():
"""
Require user to be a bot admin.
"""
def require_owner():
"""
Require user to be a bot owner.
"""
def require_privilege(level: 'AccessLevel'):
"""
Require specific channel privilege level.
Args:
level (AccessLevel): Minimum required privilege level
Example:
@plugin.require_privilege(plugin.OP)
@plugin.command('kick')
def kick_cmd(bot, trigger): ...
"""
def require_bot_privilege(level: 'AccessLevel'):
"""
Require bot to have specific channel privileges.
Args:
level (AccessLevel): Required bot privilege level
"""Control how frequently plugin functions can be triggered to prevent spam and abuse.
def rate(user: int = 0, channel: int = 0, server: int = 0, *, message: Optional[str] = None):
"""
Apply rate limiting to plugin function.
Args:
user (int): Seconds between triggers per user (0 = no limit)
channel (int): Seconds between triggers per channel (0 = no limit)
server (int): Seconds between any triggers (0 = no limit)
message (str): Optional notice message when rate limit is reached
Example:
@plugin.rate(user=10, channel=30, message='Please wait!')
@plugin.command('expensive')
def expensive_cmd(bot, trigger): ...
"""
def rate_user(seconds: int):
"""
Rate limit per user.
Args:
seconds (int): Seconds between triggers per user
"""
def rate_channel(seconds: int):
"""
Rate limit per channel.
Args:
seconds (int): Seconds between triggers per channel
"""
def rate_global(seconds: int):
"""
Global rate limit.
Args:
seconds (int): Seconds between any triggers
"""Decorators to add metadata and control plugin function behavior.
def label(label_name: str):
"""
Add a label to a plugin function for identification.
Args:
label_name (str): Label for the function
"""
class example:
"""
Add usage example to plugin function documentation with testing support.
Args:
msg (str): Example command or message
result (str or list, optional): Expected output for testing
privmsg (bool, optional): If True, test as private message
admin (bool, optional): If True, test as admin user
owner (bool, optional): If True, test as owner user
repeat (int, optional): Number of times to repeat test
re (bool, optional): Use regex matching for result
ignore (list, optional): Patterns to ignore in output
user_help (bool, optional): Include in user help output
online (bool, optional): Mark as online test
vcr (bool, optional): Record HTTP requests for testing
Example:
@plugin.example('.weather London', 'Weather in London: 15°C')
@plugin.command('weather')
def weather_cmd(bot, trigger): ...
"""
def output_prefix(prefix: str):
"""
Set output prefix for plugin function responses.
Args:
prefix (str): Prefix string for bot responses
"""
def priority(level: str):
"""
Set execution priority for plugin function.
Args:
level (str): Priority level ('low', 'medium', 'high')
"""
def thread(threaded: bool = True):
"""
Control whether function runs in separate thread.
Args:
threaded (bool): True to run in thread, False for main thread
"""
def echo():
"""
Allow function to respond to bot's own messages.
"""
def unblockable():
"""
Prevent function from being blocked by ignore lists.
"""
def allow_bots():
"""
Allow function to respond to other bots.
"""Handle IRC capability negotiation for advanced protocol features.
def capability(*capabilities: str):
"""
Register IRC capabilities that the plugin uses.
Args:
*capabilities (str): IRC capability names
Example:
@plugin.capability('account-tag')
@plugin.command('whoami')
def whoami_cmd(bot, trigger): ...
"""from sopel import plugin
@plugin.command('hello')
@plugin.example('.hello')
def hello_command(bot, trigger):
"""Greet a user."""
bot.reply(f"Hello, {trigger.nick}!")
@plugin.command('roll')
@plugin.example('.roll 6')
@plugin.rate_user(5) # 5 second cooldown per user
def roll_dice(bot, trigger):
"""Roll a die."""
import random
sides = 6
if trigger.group(2):
try:
sides = int(trigger.group(2))
except ValueError:
bot.reply("Please provide a valid number.")
return
result = random.randint(1, sides)
bot.reply(f"🎲 Rolled a {result} (1-{sides})")from sopel import plugin, config, db
class WeatherSection(config.types.StaticSection):
api_key = config.types.ValidatedAttribute('api_key')
default_location = config.types.ValidatedAttribute('default_location', default='London')
@plugin.command('weather')
@plugin.example('.weather London')
@plugin.rate_channel(30) # 30 second channel cooldown
def weather_command(bot, trigger):
"""Get weather information."""
location = trigger.group(2) or bot.settings.weather.default_location
# Store user's last location query
bot.db.set_nick_value(trigger.nick, 'last_weather_location', location)
# Weather API call would go here
bot.reply(f"Weather for {location}: Sunny, 25°C")
@plugin.rule(r'.*\b(hot|cold|weather)\b.*')
@plugin.require_chanmsg()
def weather_mention(bot, trigger):
"""Respond to weather mentions."""
last_location = bot.db.get_nick_value(trigger.nick, 'last_weather_location')
if last_location:
bot.say(f"The weather in {last_location} is still nice!")from sopel import plugin
@plugin.event('JOIN')
def welcome_user(bot, trigger):
"""Welcome new users to the channel."""
if trigger.nick != bot.nick: # Don't welcome ourselves
bot.say(f"Welcome to {trigger.sender}, {trigger.nick}!")
@plugin.event('PART', 'QUIT')
def goodbye_user(bot, trigger):
"""Say goodbye to leaving users."""
if trigger.nick != bot.nick:
bot.say(f"Goodbye, {trigger.nick}!")from typing import Optional# Standard plugin function signature
def plugin_function(bot: SopelWrapper, trigger: Trigger):
"""
Standard plugin function.
Args:
bot (SopelWrapper): Bot interface for sending messages and accessing data
trigger (Trigger): Context information about the triggering message
"""
# Interval function signature (no trigger)
def interval_function(bot: SopelWrapper):
"""
Interval function for scheduled tasks.
Args:
bot (SopelWrapper): Bot interface
"""# Rate limiting bypass constant
NOLIMIT: int = 1
# Access level constants (imported from sopel.privileges)
VOICE: AccessLevel
HALFOP: AccessLevel
OP: AccessLevel
ADMIN: AccessLevel
OWNER: AccessLevel
OPER: AccessLevel
# Capability negotiation options
class CapabilityNegotiation(enum.Enum):
NONE: str = "none"
OPTIONAL: str = "optional"
REQUIRED: str = "required"Install with Tessl CLI
npx tessl i tessl/pypi-sopel