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

utilities.mddocs/

Utilities and Tools

Sopel provides comprehensive utility functions and classes for IRC formatting, time handling, web operations, mathematical calculations, logging, and identifier management. These tools simplify common bot development tasks and provide robust functionality for plugin developers.

Capabilities

IRC Text Formatting

Functions and constants for applying IRC formatting codes to text messages.

# Formatting functions
def bold(text: str) -> str:
    """
    Apply bold formatting to text.
    
    Args:
        text (str): Text to make bold
        
    Returns:
        Text with bold formatting codes
    """

def italic(text: str) -> str:
    """
    Apply italic formatting to text.
    
    Args:
        text (str): Text to make italic
        
    Returns:
        Text with italic formatting codes
    """

def underline(text: str) -> str:
    """
    Apply underline formatting to text.
    
    Args:
        text (str): Text to underline
        
    Returns:
        Text with underline formatting codes
    """

def strikethrough(text: str) -> str:
    """
    Apply strikethrough formatting to text.
    
    Args:
        text (str): Text to strikethrough
        
    Returns:
        Text with strikethrough formatting codes
    """

def monospace(text: str) -> str:
    """
    Apply monospace formatting to text.
    
    Args:
        text (str): Text to make monospace
        
    Returns:
        Text with monospace formatting codes
    """

def reverse(text: str) -> str:
    """
    Apply reverse formatting to text.
    
    Args:
        text (str): Text to reverse colors
        
    Returns:
        Text with reverse formatting codes
    """

def color(text: str, fg: int = None, bg: int = None) -> str:
    """
    Apply color formatting to text.
    
    Args:
        text (str): Text to colorize
        fg (int): Foreground color code (0-15)
        bg (int): Background color code (0-15)
        
    Returns:
        Text with color formatting codes
    """

def hex_color(text: str, fg: str = None, bg: str = None) -> str:
    """
    Apply hexadecimal color formatting to text.
    
    Args:
        text (str): Text to colorize
        fg (str): Foreground hex color (e.g., "#FF0000")
        bg (str): Background hex color
        
    Returns:
        Text with hex color formatting codes
    """

def plain(text: str) -> str:
    """
    Remove all formatting codes from text.
    
    Args:
        text (str): Text to strip formatting from
        
    Returns:
        Plain text without formatting codes
    """

def hex_color(text: str, fg: str = None, bg: str = None) -> str:
    """
    Apply hex color formatting to text.
    
    Args:
        text (str): Text to color
        fg (str, optional): Foreground hex color (e.g., '#FF0000')
        bg (str, optional): Background hex color (e.g., '#00FF00')
        
    Returns:
        Text with hex color formatting codes
    """

def strikethrough(text: str) -> str:
    """
    Apply strikethrough formatting to text.
    
    Args:
        text (str): Text to strike through
        
    Returns:
        Text with strikethrough formatting codes
    """

def monospace(text: str) -> str:
    """
    Apply monospace formatting to text.
    
    Args:
        text (str): Text to make monospace
        
    Returns:
        Text with monospace formatting codes
    """

def reverse(text: str) -> str:
    """
    Apply reverse-color formatting to text.
    
    Args:
        text (str): Text to reverse colors
        
    Returns:
        Text with reverse-color formatting codes
    """

# Formatting constants
CONTROL_NORMAL: str       # Reset all formatting
CONTROL_COLOR: str        # Color control code
CONTROL_HEX_COLOR: str    # Hex color control code
CONTROL_BOLD: str         # Bold control code
CONTROL_ITALIC: str       # Italic control code
CONTROL_UNDERLINE: str    # Underline control code
CONTROL_STRIKETHROUGH: str # Strikethrough control code
CONTROL_MONOSPACE: str    # Monospace control code
CONTROL_REVERSE: str      # Reverse control code

# Color enumeration
class colors(enum.Enum):
    """Standard IRC color codes."""
    WHITE: int = 0
    BLACK: int = 1
    BLUE: int = 2
    GREEN: int = 3
    RED: int = 4
    BROWN: int = 5
    PURPLE: int = 6
    ORANGE: int = 7
    YELLOW: int = 8
    LIGHT_GREEN: int = 9
    TEAL: int = 10
    LIGHT_CYAN: int = 11
    LIGHT_BLUE: int = 12
    PINK: int = 13
    GREY: int = 14
    LIGHT_GREY: int = 15

Time and Duration Utilities

Functions for time formatting, timezone handling, and duration calculations.

def validate_timezone(zone: str) -> str:
    """
    Validate and normalize timezone string.
    
    Args:
        zone (str): Timezone identifier
        
    Returns:
        Normalized timezone string
        
    Raises:
        ValueError: If timezone is invalid
    """

def validate_format(tformat: str) -> str:
    """
    Validate time format string.
    
    Args:
        tformat (str): Time format string
        
    Returns:
        Validated format string
        
    Raises:
        ValueError: If format is invalid
    """

def get_nick_timezone(db: 'SopelDB', nick: str) -> str | None:
    """
    Get timezone preference for a user.
    
    Args:
        db (SopelDB): Database connection
        nick (str): User nickname
        
    Returns:
        User's timezone or None if not set
    """

def get_channel_timezone(db: 'SopelDB', channel: str) -> str | None:
    """
    Get timezone preference for a channel.
    
    Args:
        db (SopelDB): Database connection
        channel (str): Channel name
        
    Returns:
        Channel's timezone or None if not set
    """

def get_timezone(db: 'SopelDB' = None, config: 'Config' = None, zone: str = None, 
                 nick: str = None, channel: str = None) -> str:
    """
    Get timezone from various sources with fallback logic.
    
    Args:
        db (SopelDB): Database connection
        config (Config): Bot configuration
        zone (str): Explicit timezone
        nick (str): User nickname to check
        channel (str): Channel name to check
        
    Returns:
        Resolved timezone string
    """

def format_time(dt=None, zone=None, format=None) -> str:
    """
    Format datetime with timezone and format options.
    
    Args:
        dt: Datetime object (defaults to now)
        zone (str): Timezone for formatting
        format (str): Time format string
        
    Returns:
        Formatted time string
    """

def seconds_to_split(seconds: int) -> 'Duration':
    """
    Convert seconds to Duration namedtuple.
    
    Args:
        seconds (int): Number of seconds
        
    Returns:
        Duration with years, days, hours, minutes, seconds
    """

def get_time_unit(unit: str) -> int:
    """
    Get number of seconds in a time unit.
    
    Args:
        unit (str): Time unit name (second, minute, hour, day, week, etc.)
        
    Returns:
        Number of seconds in the unit
    """

def seconds_to_human(seconds: int, precision: int = 2) -> str:
    """
    Convert seconds to human-readable duration string.
    
    Args:
        seconds (int): Number of seconds
        precision (int): Number of time units to include
        
    Returns:
        Human-readable duration (e.g., "2 hours, 30 minutes")
    """

# Duration namedtuple
Duration = namedtuple('Duration', ['years', 'days', 'hours', 'minutes', 'seconds'])

Web Utilities

Functions for web requests, user agent management, and HTTP operations.

def get_user_agent() -> str:
    """
    Get default User-Agent string for web requests.
    
    Returns:
        User-Agent string identifying Sopel
    """

def get_session() -> 'requests.Session':
    """
    Get configured requests session with appropriate headers.
    
    Returns:
        Requests session object with Sopel User-Agent
    """

# Additional web utilities available in sopel.tools.web module

Mathematical Calculations

Safe mathematical expression evaluation and utility functions.

def eval_equation(equation: str) -> float:
    """
    Safely evaluate mathematical expression.
    
    Args:
        equation (str): Mathematical expression to evaluate
        
    Returns:
        Result of the calculation
        
    Raises:
        ValueError: If expression is invalid or unsafe
    """

def guarded_mul(left: float, right: float) -> float:
    """
    Multiply two numbers with overflow protection.
    
    Args:
        left (float): First number
        right (float): Second number
        
    Returns:
        Product of the numbers
        
    Raises:
        ValueError: If result would overflow
    """

def guarded_pow(num: float, exp: float) -> float:
    """
    Raise number to power with complexity protection.
    
    Args:
        num (float): Base number
        exp (float): Exponent
        
    Returns:
        Result of num ** exp
        
    Raises:
        ValueError: If operation is too complex
    """

def pow_complexity(num: int, exp: int) -> float:
    """
    Calculate complexity score for power operation.
    
    Args:
        num (int): Base number
        exp (int): Exponent
        
    Returns:
        Complexity score for the operation
    """

Identifier Handling

IRC identifier management with proper case-insensitive comparison.

class Identifier(str):
    """
    IRC identifier with RFC1459 case-insensitive comparison.
    
    Handles IRC nickname and channel name comparison properly.
    """
    
    def __new__(cls, identifier: str) -> 'Identifier':
        """Create new identifier instance."""
    
    def lower(self) -> str:
        """Get RFC1459 lowercase version of identifier."""

class IdentifierFactory:
    """Factory for creating Identifier instances."""
    
    def __init__(self, case_mapping: str = 'rfc1459'):
        """
        Initialize identifier factory.
        
        Args:
            case_mapping (str): Case mapping to use ('rfc1459' or 'ascii')
        """
    
    def __call__(self, identifier: str) -> Identifier:
        """Create identifier using this factory's case mapping."""

def ascii_lower(text: str) -> str:
    """
    Convert text to lowercase using ASCII rules.
    
    Args:
        text (str): Text to convert
        
    Returns:
        Lowercase text using ASCII case mapping
    """

Memory Storage Classes

Utility classes for storing data with identifier-based access.

class SopelMemory(dict):
    """
    Dictionary subclass for storing bot data.
    
    Provides additional utility methods for data management.
    """
    
    def __init__(self):
        """Initialize empty memory storage."""
    
    def lock(self, key: str) -> 'threading.Lock':
        """
        Get thread lock for a specific key.
        
        Args:
            key (str): Key to get lock for
            
        Returns:
            Threading lock object
        """

class SopelIdentifierMemory(SopelMemory):
    """
    Memory storage using IRC identifiers as keys.
    
    Provides case-insensitive access using IRC identifier rules.
    """
    
    def __init__(self, identifier_factory: IdentifierFactory = None):
        """
        Initialize identifier memory.
        
        Args:
            identifier_factory (IdentifierFactory): Factory for creating identifiers
        """

class SopelMemoryWithDefault(SopelMemory):
    """
    Memory storage with default value factory.
    
    Automatically creates default values for missing keys.
    """
    
    def __init__(self, default_factory: callable = None):
        """
        Initialize memory with default factory.
        
        Args:
            default_factory (callable): Function to create default values
        """

Logging Utilities

Functions for plugin logging and message output.

def get_logger(plugin_name: str) -> 'logging.Logger':
    """
    Get logger instance for a plugin.
    
    Args:
        plugin_name (str): Name of the plugin
        
    Returns:
        Logger configured for the plugin
    """

def get_sendable_message(text: str, max_length: int = 400) -> tuple[str, str]:
    """
    Split text into sendable message and excess.
    
    Args:
        text (str): Text to split
        max_length (int): Maximum message length in bytes
        
    Returns:
        Tuple of (sendable_text, excess_text)
    """

def get_hostmask_regex(mask: str) -> 'Pattern':
    """
    Create regex pattern for IRC hostmask matching.
    
    Args:
        mask (str): Hostmask pattern with wildcards
        
    Returns:
        Compiled regex pattern for matching
    """

def chain_loaders(*lazy_loaders) -> callable:
    """
    Chain multiple lazy loader functions together.
    
    Args:
        *lazy_loaders: Lazy loader functions
        
    Returns:
        Combined lazy loader function
    """

Usage Examples

IRC Text Formatting

from sopel import plugin
from sopel.formatting import bold, color, italic, plain

@plugin.command('format')
@plugin.example('.format Hello World')
def format_example(bot, trigger):
    """Demonstrate text formatting."""
    text = trigger.group(2) or "Sample Text"
    
    # Apply various formatting
    formatted_examples = [
        bold(text),
        italic(text),
        color(text, colors.RED),
        color(text, colors.WHITE, colors.BLUE),
        bold(color(text, colors.GREEN))
    ]
    
    for example in formatted_examples:
        bot.say(example)
    
    # Show plain text version
    bot.say(f"Plain: {plain(formatted_examples[0])}")

@plugin.command('rainbow')
def rainbow_text(bot, trigger):
    """Create rainbow-colored text."""
    text = trigger.group(2) or "RAINBOW"
    rainbow_colors = [colors.RED, colors.ORANGE, colors.YELLOW, 
                     colors.GREEN, colors.BLUE, colors.PURPLE]
    
    colored_chars = []
    for i, char in enumerate(text):
        if char != ' ':
            color_code = rainbow_colors[i % len(rainbow_colors)]
            colored_chars.append(color(char, color_code))
        else:
            colored_chars.append(char)
    
    bot.say(''.join(colored_chars))

Time and Duration Utilities

from sopel import plugin
from sopel.tools.time import format_time, seconds_to_human, get_time_unit

@plugin.command('time')
@plugin.example('.time UTC')
def time_command(bot, trigger):
    """Show current time in specified timezone."""
    zone = trigger.group(2) or 'UTC'
    
    try:
        current_time = format_time(zone=zone, format='%Y-%m-%d %H:%M:%S %Z')
        bot.reply(f"Current time in {zone}: {current_time}")
    except ValueError as e:
        bot.reply(f"Invalid timezone: {e}")

@plugin.command('uptime')
def uptime_command(bot, trigger):
    """Show bot uptime."""
    import time
    
    # Calculate uptime (this would need to be tracked elsewhere)
    uptime_seconds = int(time.time() - bot.start_time)  # Hypothetical
    uptime_human = seconds_to_human(uptime_seconds)
    
    bot.reply(f"Bot uptime: {uptime_human}")

@plugin.command('remind')
@plugin.example('.remind 30m Check the logs')
def remind_command(bot, trigger):
    """Set a reminder with time parsing."""
    args = trigger.group(2)
    if not args:
        bot.reply("Usage: .remind <time> <message>")
        return
    
    parts = args.split(' ', 1)
    if len(parts) < 2:
        bot.reply("Usage: .remind <time> <message>")
        return
    
    time_str, message = parts
    
    # Parse time string (simplified)
    try:
        if time_str.endswith('m'):
            minutes = int(time_str[:-1])
            seconds = minutes * get_time_unit('minute')
        elif time_str.endswith('h'):
            hours = int(time_str[:-1])
            seconds = hours * get_time_unit('hour')
        else:
            seconds = int(time_str)
        
        # Schedule reminder (would need actual scheduling)
        bot.reply(f"Reminder set for {seconds_to_human(seconds)}: {message}")
        
    except ValueError:
        bot.reply("Invalid time format. Use: 30m, 2h, or seconds")

Mathematical Calculations

from sopel import plugin
from sopel.tools.calculation import eval_equation

@plugin.command('calc')
@plugin.example('.calc 2 + 2 * 3')
def calc_command(bot, trigger):
    """Safely calculate mathematical expressions."""
    expression = trigger.group(2)
    if not expression:
        bot.reply("Usage: .calc <expression>")
        return
    
    try:
        result = eval_equation(expression)
        bot.reply(f"{expression} = {result}")
    except ValueError as e:
        bot.reply(f"Calculation error: {e}")
    except Exception as e:
        bot.reply(f"Invalid expression: {e}")

@plugin.command('convert')
@plugin.example('.convert 100 F to C')
def convert_command(bot, trigger):
    """Temperature conversion with calculations."""
    args = trigger.group(2)
    if not args:
        bot.reply("Usage: .convert <temp> <F|C> to <C|F>")
        return
    
    parts = args.split()
    if len(parts) != 4 or parts[2].lower() != 'to':
        bot.reply("Usage: .convert <temp> <F|C> to <C|F>")
        return
    
    try:
        temp = float(parts[0])
        from_unit = parts[1].upper()
        to_unit = parts[3].upper()
        
        if from_unit == 'F' and to_unit == 'C':
            result = (temp - 32) * 5/9
            bot.reply(f"{temp}°F = {result:.2f}°C")
        elif from_unit == 'C' and to_unit == 'F':
            result = temp * 9/5 + 32
            bot.reply(f"{temp}°C = {result:.2f}°F")
        else:
            bot.reply("Supported conversions: F to C, C to F")
    except ValueError:
        bot.reply("Invalid temperature value")

Identifier and Memory Usage

from sopel import plugin
from sopel.tools import Identifier, SopelMemory

# Plugin-level memory storage
user_scores = SopelMemory()

@plugin.command('score')
def score_command(bot, trigger):
    """Show or modify user scores."""
    args = trigger.group(2)
    
    if not args:
        # Show current user's score
        nick = Identifier(trigger.nick)
        score = user_scores.get(nick, 0)
        bot.reply(f"Your score: {score}")
        return
    
    parts = args.split()
    if len(parts) == 1:
        # Show specified user's score
        nick = Identifier(parts[0])
        score = user_scores.get(nick, 0)
        bot.reply(f"Score for {nick}: {score}")
    elif len(parts) == 2:
        # Set score (admin only)
        if trigger.nick not in bot.settings.core.admins:
            bot.reply("Only admins can set scores")
            return
        
        nick = Identifier(parts[0])
        try:
            new_score = int(parts[1])
            user_scores[nick] = new_score
            bot.reply(f"Set score for {nick} to {new_score}")
        except ValueError:
            bot.reply("Score must be a number")

@plugin.command('leaderboard')
def leaderboard_command(bot, trigger):
    """Show score leaderboard."""
    if not user_scores:
        bot.reply("No scores recorded yet")
        return
    
    # Sort by score (descending)
    sorted_scores = sorted(user_scores.items(), key=lambda x: x[1], reverse=True)
    top_5 = sorted_scores[:5]
    
    leaderboard = ["🏆 Leaderboard:"]
    for i, (nick, score) in enumerate(top_5, 1):
        leaderboard.append(f"{i}. {nick}: {score}")
    
    bot.reply(" | ".join(leaderboard))

Logging and Debugging

from sopel import plugin
from sopel.tools import get_logger

# Get plugin-specific logger
LOGGER = get_logger('my_plugin')

@plugin.command('debug')
@plugin.require_admin()
def debug_command(bot, trigger):
    """Show debug information."""
    args = trigger.group(2)
    
    if args == 'memory':
        # Show memory usage information
        import psutil
        process = psutil.Process()
        memory_mb = process.memory_info().rss / 1024 / 1024
        bot.reply(f"Memory usage: {memory_mb:.1f} MB")
        
    elif args == 'channels':
        # Show channel information
        channel_count = len(bot.channels)
        channel_list = list(bot.channels.keys())[:5]  # First 5
        bot.reply(f"In {channel_count} channels: {', '.join(channel_list)}")
        
    elif args == 'users':
        # Show user information
        user_count = len(bot.users)
        bot.reply(f"Tracking {user_count} users")
        
    else:
        bot.reply("Debug options: memory, channels, users")
    
    # Log debug information
    LOGGER.info(f"Debug command used by {trigger.nick}: {args}")

@plugin.command('log')
@plugin.require_owner()
def log_test(bot, trigger):
    """Test logging at different levels."""
    message = trigger.group(2) or "Test message"
    
    LOGGER.debug(f"Debug: {message}")
    LOGGER.info(f"Info: {message}")
    LOGGER.warning(f"Warning: {message}")
    LOGGER.error(f"Error: {message}")
    
    bot.reply("Log messages sent at all levels")

Types

Formatting Types

# Control character constants
CONTROL_FORMATTING: list  # List of all formatting control chars
CONTROL_NON_PRINTING: list  # List of non-printing control chars

Time Types

Duration = namedtuple('Duration', ['years', 'days', 'hours', 'minutes', 'seconds'])

Memory Types

# Type aliases for memory storage
IdentifierMemory = SopelIdentifierMemory
Memory = SopelMemory
MemoryWithDefault = SopelMemoryWithDefault

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