CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-sopel

Simple and extensible IRC bot framework written in Python with plugin architecture and database support

Pending
Overview
Eval results
Files

irc-protocol.mddocs/

IRC Protocol

Sopel provides comprehensive IRC protocol handling including connection management, message sending, channel operations, user tracking, capability negotiation, and mode parsing. The IRC layer handles both low-level protocol details and high-level bot operations.

Capabilities

Bot Communication Methods

Core methods for sending messages and interacting with IRC channels and users.

class Sopel:
    """Main bot class with IRC communication methods."""
    
    def say(self, text: str, recipient: str, max_messages: int = 1, truncation: str = '', trailing: str = '') -> None:
        """
        Send a message to a channel or user.
        
        Args:
            text (str): Message text to send
            recipient (str): Channel or nick to send to
            max_messages (int): Maximum number of messages if text is split
            truncation (str): String to indicate message was truncated
            trailing (str): String to append after text
            
        Returns:
            None
        """
    
    def reply(self, text: str, destination: str = None, reply_to: str = None, max_messages: int = 1) -> int:
        """
        Reply to a user, optionally mentioning their nick.
        
        Args:
            text (str): Reply text
            destination (str): Channel or nick to reply to
            reply_to (str): Nick to mention in reply
            max_messages (int): Maximum number of messages if text is split
            
        Returns:
            Number of messages sent
        """
    
    def action(self, text: str, destination: str = None) -> None:
        """
        Send a CTCP ACTION (/me) message.
        
        Args:
            text (str): Action text
            destination (str): Channel or nick to send action to
        """
    
    def notice(self, text: str, destination: str = None) -> None:
        """
        Send a NOTICE message.
        
        Args:
            text (str): Notice text
            destination (str): Channel or nick to send notice to
        """
    
    def msg(self, recipient: str, text: str, max_messages: int = 1) -> int:
        """
        Send a private message to a specific recipient.
        
        Args:
            recipient (str): Nick or channel to message
            text (str): Message text
            max_messages (int): Maximum number of messages if text is split
            
        Returns:
            Number of messages sent
        """

class SopelWrapper:
    """Bot wrapper for use in plugin functions."""
    
    def say(self, message: str, destination: str = None, max_messages: int = 1, truncation: str = '', trailing: str = '') -> None:
        """Send message (same as Sopel.say)."""
    
    def reply(self, message: str, destination: str = None, reply_to: str = None, notice: bool = False) -> None:
        """Reply to user (same as Sopel.reply)."""
    
    def action(self, text: str, destination: str = None) -> None:
        """Send action (same as Sopel.action)."""
    
    def notice(self, text: str, destination: str = None) -> None:
        """Send notice (same as Sopel.notice)."""

Channel Operations

Methods for joining, leaving, and managing IRC channels.

class Sopel:
    """Channel management methods."""
    
    def join(self, channel: str, password: str = None) -> None:
        """
        Join an IRC channel.
        
        Args:
            channel (str): Channel name to join (with # prefix)
            password (str): Channel password if required
        """
    
    def part(self, channel: str, message: str = None) -> None:
        """
        Leave an IRC channel.
        
        Args:
            channel (str): Channel name to leave
            message (str): Part message
        """
    
    def kick(self, nick: str, channel: str = None, message: str = None) -> None:
        """
        Kick a user from a channel.
        
        Args:
            nick (str): Nickname to kick
            channel (str): Channel to kick from (defaults to current context)
            message (str): Kick message
        """
    
    def invite(self, nick: str, channel: str) -> None:
        """
        Invite a user to a channel.
        
        Args:
            nick (str): Nickname to invite
            channel (str): Channel to invite to
        """
    
    def mode(self, target: str, modes: str = None) -> None:
        """
        Set channel or user modes.
        
        Args:
            target (str): Channel or nick to set modes on
            modes (str): Mode string (e.g., "+o nick", "-v nick")
        """

User and Channel Tracking

Access to bot's knowledge of users and channels.

class Sopel:
    """User and channel tracking attributes."""
    
    @property
    def channels(self) -> 'IdentifierMemory':
        """
        Dictionary of channels the bot is in.
        
        Keys are Identifier objects for channel names.
        Values are Channel objects containing user lists and permissions.
        """
    
    @property  
    def users(self) -> 'IdentifierMemory':
        """
        Dictionary of users the bot is aware of.
        
        Keys are Identifier objects for nicknames.
        Values are User objects with user information.
        """
    
    @property
    def nick(self) -> 'Identifier':
        """Bot's current nickname."""
    
    def is_nick(self, nick: str) -> bool:
        """
        Check if a nickname belongs to this bot.
        
        Args:
            nick (str): Nickname to check
            
        Returns:
            True if nick is bot's nickname
        """

Connection Management

Methods for managing the IRC connection and bot lifecycle.

class Sopel:
    """Connection and lifecycle management."""
    
    def run(self, host: str = None, port: int = None) -> None:
        """
        Start the bot and connect to IRC.
        
        Args:
            host (str): IRC server hostname (overrides config)
            port (int): IRC server port (overrides config)
        """
    
    def restart(self, message: str = None) -> None:
        """
        Restart the bot.
        
        Args:
            message (str): Quit message before restart
        """
    
    def quit(self, message: str = None) -> None:
        """
        Disconnect from IRC and quit.
        
        Args:
            message (str): Quit message
        """
    
    def write(self, args: list, text: str = None) -> None:
        """
        Send raw IRC command.
        
        Args:
            args (list): IRC command and parameters
            text (str): Optional message text
        """
    
    def safe_text_length(self, recipient: str) -> int:
        """
        Get maximum safe text length for messages to recipient.
        
        Args:
            recipient (str): Target channel or nick
            
        Returns:
            Maximum safe message length in bytes
        """

Capability Negotiation

IRC capability negotiation for modern IRC features.

class Sopel:
    """IRC capability handling."""
    
    @property
    def server_capabilities(self) -> dict:
        """Dictionary of server-supported capabilities."""
    
    @property
    def enabled_capabilities(self) -> set:
        """Set of currently enabled capabilities."""
    
    def request_capability(self, capability: str) -> None:
        """
        Request an IRC capability.
        
        Args:
            capability (str): Capability name to request
        """

Mode Parsing

IRC mode parsing and interpretation.

class ModeParser:
    """Parser for IRC mode strings."""
    
    def parse(self, mode_string: str, params: list = None) -> dict:
        """
        Parse IRC mode string.
        
        Args:
            mode_string (str): Mode string (e.g., "+ooo-v")
            params (list): Mode parameters
            
        Returns:
            Dictionary of parsed mode changes
        """

class Sopel:
    @property
    def modeparser(self) -> ModeParser:
        """Mode parser instance for handling IRC modes."""

Usage Examples

Basic Message Sending

from sopel import plugin

@plugin.command('say')
@plugin.example('.say #channel Hello everyone!')
def say_command(bot, trigger):
    """Make the bot say something in a channel."""
    args = trigger.group(2)
    if not args:
        bot.reply("Usage: .say <channel> <message>")
        return
    
    parts = args.split(' ', 1)
    if len(parts) < 2:
        bot.reply("Usage: .say <channel> <message>")
        return
    
    channel, message = parts
    bot.say(message, channel)
    bot.reply(f"Message sent to {channel}")

@plugin.command('me')
@plugin.example('.me is excited about IRC bots!')
def action_command(bot, trigger):
    """Make the bot perform an action."""
    if not trigger.group(2):
        bot.reply("Usage: .me <action>")
        return
    
    action_text = trigger.group(2)
    bot.action(action_text, trigger.sender)

Channel Management

@plugin.command('join')
@plugin.require_admin()
def join_command(bot, trigger):
    """Join a channel."""
    if not trigger.group(2):
        bot.reply("Usage: .join <channel> [password]")
        return
    
    args = trigger.group(2).split(' ', 1)
    channel = args[0]
    password = args[1] if len(args) > 1 else None
    
    if not channel.startswith('#'):
        channel = '#' + channel
    
    bot.join(channel, password)
    bot.reply(f"Joining {channel}")

@plugin.command('part')
@plugin.require_admin()
def part_command(bot, trigger):
    """Leave a channel."""
    channel = trigger.group(2) or trigger.sender
    
    if not channel.startswith('#'):
        bot.reply("Must specify a channel to leave")
        return
    
    bot.part(channel, "Leaving on admin request")
    if trigger.sender != channel:
        bot.reply(f"Left {channel}")

@plugin.command('kick')
@plugin.require_privilege(plugin.OP)
def kick_command(bot, trigger):
    """Kick a user from the channel."""
    args = trigger.group(2)
    if not args:
        bot.reply("Usage: .kick <nick> [reason]")
        return
    
    parts = args.split(' ', 1)
    nick = parts[0]
    reason = parts[1] if len(parts) > 1 else f"Kicked by {trigger.nick}"
    
    bot.kick(nick, trigger.sender, reason)

User and Channel Information

@plugin.command('userinfo')
def userinfo_command(bot, trigger):
    """Show information about a user."""
    nick = trigger.group(2) or trigger.nick
    
    if nick in bot.users:
        user = bot.users[nick]
        info_parts = [f"User info for {nick}:"]
        
        if hasattr(user, 'host'):
            info_parts.append(f"Host: {user.host}")
        if hasattr(user, 'account') and user.account:
            info_parts.append(f"Account: {user.account}")
        if hasattr(user, 'away') and user.away:
            info_parts.append("Status: Away")
        
        bot.reply(" | ".join(info_parts))
    else:
        bot.reply(f"No information available for {nick}")

@plugin.command('chaninfo')
def chaninfo_command(bot, trigger):
    """Show information about current channel."""
    channel = trigger.sender
    
    if not channel.startswith('#'):
        bot.reply("This command only works in channels")
        return
    
    if channel in bot.channels:
        chan = bot.channels[channel]
        user_count = len(chan.users) if hasattr(chan, 'users') else 0
        bot.reply(f"Channel {channel} has {user_count} users")
        
        # Show channel modes if available
        if hasattr(chan, 'modes'):
            modes = '+'.join(chan.modes) if chan.modes else "none"
            bot.reply(f"Channel modes: {modes}")
    else:
        bot.reply(f"Not currently in {channel}")

Advanced IRC Features

@plugin.command('whois')
def whois_command(bot, trigger):
    """Get WHOIS information for a user."""
    nick = trigger.group(2)
    if not nick:
        bot.reply("Usage: .whois <nick>")
        return
    
    # Send WHOIS request
    bot.write(['WHOIS', nick])
    bot.reply(f"WHOIS request sent for {nick}")

@plugin.event('RPL_WHOISUSER')  # 311 numeric
def handle_whois_response(bot, trigger):
    """Handle WHOIS response."""
    # Parse WHOIS response: :server 311 bot_nick target_nick username hostname * :realname
    if len(trigger.args) >= 6:
        target_nick = trigger.args[1]
        username = trigger.args[2] 
        hostname = trigger.args[3]
        realname = trigger.args[5]
        
        bot.say(f"WHOIS {target_nick}: {username}@{hostname} ({realname})")

@plugin.command('mode')
@plugin.require_privilege(plugin.OP)
def mode_command(bot, trigger):
    """Set channel modes."""
    args = trigger.group(2)
    if not args:
        bot.reply("Usage: .mode <modes> [parameters]")
        return
    
    # Set modes on current channel
    bot.mode(trigger.sender, args)
    bot.reply(f"Mode change requested: {args}")

Connection Monitoring

@plugin.event('001')  # RPL_WELCOME - successful connection
def on_connect(bot, trigger):
    """Handle successful IRC connection."""
    bot.say("Bot connected successfully!", bot.settings.core.owner)

@plugin.event('PING')
def handle_ping(bot, trigger):
    """Handle PING from server."""
    # Sopel handles PING automatically, but you can add custom logic
    pass

@plugin.event('ERROR')
def handle_error(bot, trigger):
    """Handle ERROR messages from server."""
    error_msg = trigger.args[0] if trigger.args else "Unknown error"
    bot.say(f"IRC Error: {error_msg}", bot.settings.core.owner)

@plugin.command('reconnect')
@plugin.require_owner()
def reconnect_command(bot, trigger):
    """Reconnect to IRC server."""
    bot.quit("Reconnecting...")
    # Bot will automatically reconnect based on configuration

Capability Usage

@plugin.capability('account-tag')
@plugin.command('account')
def account_command(bot, trigger):
    """Show user's account information (requires account-tag capability)."""
    if 'account-tag' not in bot.enabled_capabilities:
        bot.reply("Account information not available (capability not supported)")
        return
    
    account = getattr(trigger, 'account', None)
    if account:
        bot.reply(f"You are logged in as: {account}")
    else:
        bot.reply("You are not logged in to services")

@plugin.event('CAP')
def handle_capability(bot, trigger):
    """Handle capability negotiation messages."""
    # Sopel handles this automatically, but you can add custom logic
    subcommand = trigger.args[1]
    if subcommand == 'ACK':
        capabilities = trigger.args[2].split()
        for cap in capabilities:
            bot.say(f"Capability enabled: {cap}", bot.settings.core.owner)

Types

IRC Message Context

class Trigger:
    """Context information for IRC messages."""
    
    # Message metadata
    nick: str        # Sender's nickname
    user: str        # Sender's username  
    host: str        # Sender's hostname
    hostmask: str    # Full hostmask (nick!user@host)
    sender: str      # Channel or nick message came from
    raw: str         # Raw IRC message
    
    # Message content
    args: list       # Message arguments
    event: str       # IRC event type (PRIVMSG, JOIN, etc.)
    
    # Message properties
    is_privmsg: bool # True if private message
    account: str     # Sender's services account (if available)
    
    # Regex match methods
    def group(self, n: int) -> str:
        """Get regex match group."""
    
    def groups(self) -> tuple:
        """Get all regex match groups."""

Channel and User Objects

class Channel:
    """Represents an IRC channel."""
    
    users: dict      # Users in channel mapped to privilege levels
    modes: set       # Channel modes
    topic: str       # Channel topic
    
class User:
    """Represents an IRC user."""
    
    nick: str        # Current nickname
    user: str        # Username
    host: str        # Hostname  
    account: str     # Services account
    away: bool       # Away status
    channels: set    # Channels user is in

class Identifier(str):
    """IRC identifier with case-insensitive comparison."""
    
    def lower(self) -> str:
        """Get RFC1459 lowercase version."""

Install with Tessl CLI

npx tessl i tessl/pypi-sopel

docs

configuration.md

database.md

index.md

irc-protocol.md

plugin-development.md

utilities.md

tile.json