IRC (Internet Relay Chat) protocol library for Python
—
IRC-specific string handling, nickname parsing, channel detection, mode parsing, and scheduling utilities for IRC applications. These utilities provide essential functionality for working with IRC protocol data.
Parse and manipulate IRC nicknames and user masks.
class NickMask(str):
"""IRC nickname mask parsing and manipulation."""
@property
def nick(self) -> str:
"""
Extract nickname from mask.
Returns:
str, nickname portion of mask
"""
@property
def user(self) -> str:
"""
Extract username from mask.
Returns:
str, username portion of mask (after !)
"""
@property
def host(self) -> str:
"""
Extract hostname from mask.
Returns:
str, hostname portion of mask (after @)
"""
@property
def userhost(self) -> str:
"""
Get user@host portion of mask.
Returns:
str, user@host portion
"""
@classmethod
def from_params(cls, nick: str, user: str, host: str):
"""
Create NickMask from individual components.
Parameters:
- nick: str, nickname
- user: str, username
- host: str, hostname
Returns:
NickMask, constructed mask (nick!user@host)
"""
@classmethod
def from_group(cls, group):
"""
Create NickMask from regex match group.
Parameters:
- group: regex match group containing mask components
Returns:
NickMask instance
"""Utilities for working with IRC channels and messages.
def is_channel(string: str) -> bool:
"""
Check if string is a channel name.
Checks if the string starts with valid channel prefixes
according to IRC standards (#, &, +, !).
Parameters:
- string: str, string to check
Returns:
bool, True if string appears to be a channel name
"""
def ip_numstr_to_quad(num: str) -> str:
"""
Convert IP address from numeric string to dotted quad format.
Converts DCC IP address format to standard dotted decimal.
Parameters:
- num: str, numeric IP address string
Returns:
str, dotted quad IP address (e.g., "192.168.1.1")
"""
def ip_quad_to_numstr(quad: str) -> str:
"""
Convert IP address from dotted quad to numeric string format.
Converts standard dotted decimal to DCC numeric format.
Parameters:
- quad: str, dotted quad IP address
Returns:
str, numeric IP address string
"""IRC-specific string processing and case folding.
class IRCFoldedCase(str):
"""IRC-compliant case folding for nicknames and channels."""
@property
def translation(self) -> dict:
"""Character translation table for IRC case folding."""
def lower(self) -> str:
"""
Convert to lowercase using IRC rules.
Uses IRC-specific case folding where [ ] \ are lowercase
equivalents of { } |.
Returns:
str, lowercase string according to IRC rules
"""
def casefold(self) -> str:
"""
Case-fold string for IRC comparison.
Returns:
str, case-folded string
"""
def __setattr__(self, key, val):
"""Prevent modification of immutable string attributes."""
def lower(string: str) -> str:
"""
IRC-compliant string lowercasing.
Converts string to lowercase using IRC case folding rules
where ASCII brackets are equivalent to braces.
Parameters:
- string: str, string to convert
Returns:
str, lowercase string using IRC rules
"""Case-insensitive dictionary implementation using IRC string rules.
class IRCDict(dict):
"""Case-insensitive dictionary using IRC case folding rules."""
@staticmethod
def transform_key(key: str) -> str:
"""
Transform key using IRC case folding.
Applies IRC-specific case folding to dictionary keys,
ensuring case-insensitive lookups follow IRC standards.
Parameters:
- key: str, dictionary key
Returns:
str, transformed key
"""Parse IRC user and channel mode strings.
def parse_nick_modes(mode_string: str) -> list:
"""
Parse user mode string into list of mode changes.
Parses MODE commands targeting users, handling both
setting (+) and unsetting (-) of modes.
Parameters:
- mode_string: str, mode string (e.g., "+iwx", "-o+v")
Returns:
list, list of (action, mode) tuples where action is '+' or '-'
"""
def parse_channel_modes(mode_string: str) -> list:
"""
Parse channel mode string into list of mode changes.
Parses MODE commands targeting channels, handling modes
with and without parameters.
Parameters:
- mode_string: str, channel mode string
Returns:
list, list of (action, mode, parameter) tuples
"""
def _parse_modes(mode_string: str, unary_modes: str = "") -> list:
"""
Generic mode parser for IRC mode strings.
Internal function used by specific mode parsers.
Parameters:
- mode_string: str, raw mode string
- unary_modes: str, modes that don't take parameters
Returns:
list, parsed mode changes
"""Task scheduling utilities for IRC bots and clients.
class IScheduler:
"""Abstract interface for event scheduling."""
def execute_every(self, period, func):
"""
Execute function periodically.
Parameters:
- period: time period between executions
- func: callable, function to execute
"""
def execute_at(self, when, func):
"""
Execute function at specific time.
Parameters:
- when: datetime, when to execute
- func: callable, function to execute
"""
def execute_after(self, delay, func):
"""
Execute function after delay.
Parameters:
- delay: time delay before execution
- func: callable, function to execute
"""
def run_pending(self):
"""Run any pending scheduled tasks."""
class DefaultScheduler(IScheduler):
"""Default scheduler implementation using the schedule library."""
def execute_every(self, period, func):
"""
Schedule function to run every period.
Parameters:
- period: int/float, seconds between executions
- func: callable, function to execute
"""
def execute_at(self, when, func):
"""
Schedule function to run at specific time.
Parameters:
- when: datetime, execution time
- func: callable, function to execute
"""
def execute_after(self, delay, func):
"""
Schedule function to run after delay.
Parameters:
- delay: int/float, delay in seconds
- func: callable, function to execute
"""from irc.client import NickMask
# Parse nickname mask from IRC message
mask = NickMask("alice!alice@example.com")
print(f"Nick: {mask.nick}") # alice
print(f"User: {mask.user}") # alice
print(f"Host: {mask.host}") # example.com
print(f"Userhost: {mask.userhost}") # alice@example.com
# Create mask from components
mask2 = NickMask.from_params("bob", "robert", "isp.net")
print(mask2) # bob!robert@isp.net
# Parse server messages
def on_join(connection, event):
user_mask = NickMask(event.source)
print(f"{user_mask.nick} joined from {user_mask.host}")
# Use in bot
import irc.client
client = irc.client.SimpleIRCClient()
client.connection.add_global_handler("join", on_join)
client.connect("irc.libera.chat", 6667, "maskbot")
client.start()from irc.client import is_channel
# Check if strings are channels
channels = ["#general", "&local", "+private", "!unique", "user", "server.name"]
for item in channels:
if is_channel(item):
print(f"{item} is a channel")
else:
print(f"{item} is not a channel")
# Use in message handler
def on_pubmsg(connection, event):
target = event.target
message = event.arguments[0]
if message.startswith("!join "):
channel_name = message[6:]
if is_channel(channel_name):
connection.join(channel_name)
else:
connection.privmsg(target, f"'{channel_name}' is not a valid channel name")from irc.strings import lower, IRCFoldedCase
from irc.dict import IRCDict
# IRC case folding
nick1 = "Alice[Bot]"
nick2 = "alice{bot}"
# These are equivalent in IRC
print(lower(nick1)) # alice{bot}
print(lower(nick2)) # alice{bot}
print(lower(nick1) == lower(nick2)) # True
# Use IRCDict for case-insensitive storage
users = IRCDict()
users["Alice[Bot]"] = {"level": "admin"}
users["Bob^Away"] = {"level": "user"}
# Lookups are case-insensitive
print(users["alice{bot}"]) # {'level': 'admin'}
print(users["bob~away"]) # {'level': 'user'}
# Practical example: user database
class UserDatabase:
def __init__(self):
self.users = IRCDict()
def add_user(self, nick, info):
self.users[nick] = info
def get_user(self, nick):
return self.users.get(nick)
def is_admin(self, nick):
user = self.get_user(nick)
return user and user.get("level") == "admin"
# Usage in bot
db = UserDatabase()
db.add_user("Alice[Admin]", {"level": "admin"})
def on_pubmsg(connection, event):
nick = event.source.nick
message = event.arguments[0]
if message.startswith("!kick") and db.is_admin(nick):
# Admin command - nick comparison is case-insensitive
target = message.split()[1]
connection.kick(event.target, target)from irc.modes import parse_nick_modes, parse_channel_modes
# Parse user modes
user_modes = parse_nick_modes("+iwx-o")
print(user_modes) # [('+', 'i'), ('+', 'w'), ('+', 'x'), ('-', 'o')]
# Parse channel modes
channel_modes = parse_channel_modes("+nt-k+l")
print(channel_modes) # [('+', 'n'), ('+', 't'), ('-', 'k'), ('+', 'l')]
# Use in mode event handler
def on_mode(connection, event):
target = event.target
mode_string = event.arguments[0]
mode_args = event.arguments[1:] if len(event.arguments) > 1 else []
if is_channel(target):
modes = parse_channel_modes(mode_string)
print(f"Channel {target} mode changes: {modes}")
for i, (action, mode) in enumerate(modes):
if mode in "ovh": # User privilege modes
if i < len(mode_args):
user = mode_args[i]
print(f"{action}{mode} {user}")
else:
modes = parse_nick_modes(mode_string)
print(f"User {target} mode changes: {modes}")from irc.client import ip_numstr_to_quad, ip_quad_to_numstr
# Convert between DCC IP formats
numeric_ip = "3232235777" # DCC format
dotted_ip = ip_numstr_to_quad(numeric_ip)
print(dotted_ip) # 192.168.1.1
# Convert back
numeric_again = ip_quad_to_numstr(dotted_ip)
print(numeric_again) # 3232235777
# Use in DCC handling
def on_pubmsg(connection, event):
message = event.arguments[0]
nick = event.source.nick
if message.startswith("!dcc"):
# Create DCC connection
dcc = connection.dcc("chat")
dcc.listen()
# Get connection info
address = dcc.socket.getsockname()
host_ip = "192.168.1.100" # Your external IP
port = address[1]
# Convert IP to DCC format
numeric_ip = ip_quad_to_numstr(host_ip)
# Send DCC offer
dcc_msg = f"\x01DCC CHAT chat {numeric_ip} {port}\x01"
connection.privmsg(nick, dcc_msg)from irc.schedule import DefaultScheduler
import irc.client
import datetime
class ScheduledBot:
def __init__(self):
self.client = irc.client.SimpleIRCClient()
self.scheduler = DefaultScheduler()
self.setup_handlers()
self.setup_scheduled_tasks()
def setup_handlers(self):
def on_connect(connection, event):
connection.join("#scheduled")
def on_pubmsg(connection, event):
message = event.arguments[0]
channel = event.target
if message.startswith("!remind "):
# Parse reminder: !remind 30 Take a break
parts = message[8:].split(" ", 1)
if len(parts) == 2:
try:
delay = int(parts[0])
reminder_text = parts[1]
# Schedule reminder
self.scheduler.execute_after(
delay,
lambda: connection.privmsg(channel, f"Reminder: {reminder_text}")
)
connection.privmsg(channel, f"Reminder set for {delay} seconds")
except ValueError:
connection.privmsg(channel, "Invalid delay time")
elif message == "!time":
# Schedule time announcement in 5 seconds
self.scheduler.execute_after(
5,
lambda: connection.privmsg(channel, f"Time: {datetime.datetime.now()}")
)
self.client.connection.add_global_handler("welcome", on_connect)
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
def setup_scheduled_tasks(self):
"""Set up recurring scheduled tasks."""
def hourly_announcement():
if self.client.connection.is_connected():
self.client.connection.privmsg("#scheduled", "Hourly ping!")
def daily_stats():
if self.client.connection.is_connected():
uptime = "Bot has been running for some time"
self.client.connection.privmsg("#scheduled", f"Daily stats: {uptime}")
# Schedule recurring tasks
self.scheduler.execute_every(3600, hourly_announcement) # Every hour
self.scheduler.execute_every(86400, daily_stats) # Every day
# Schedule one-time task
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
self.scheduler.execute_at(tomorrow, lambda: print("Tomorrow arrived!"))
def start(self):
"""Start bot with scheduler integration."""
self.client.connect("irc.libera.chat", 6667, "scheduledbot")
# Override the event loop to include scheduler
while True:
try:
self.client.reactor.process_once(timeout=1.0)
self.scheduler.run_pending()
except KeyboardInterrupt:
break
self.client.connection.quit("Scheduler bot shutting down")
# Usage
bot = ScheduledBot()
bot.start()import irc.client
from irc.client import NickMask, is_channel, ip_quad_to_numstr
from irc.strings import lower
from irc.dict import IRCDict
from irc.modes import parse_channel_modes
import datetime
import json
class UtilityBot:
def __init__(self):
self.client = irc.client.SimpleIRCClient()
self.user_data = IRCDict() # Case-insensitive user storage
self.channel_stats = IRCDict() # Case-insensitive channel storage
self.setup_handlers()
def setup_handlers(self):
def on_connect(connection, event):
connection.join("#utilities")
print("UtilityBot connected and ready!")
def on_join(connection, event):
nick = event.source.nick
channel = event.target
# Track channel stats
if channel not in self.channel_stats:
self.channel_stats[channel] = {"joins": 0, "messages": 0}
self.channel_stats[channel]["joins"] += 1
# Welcome message with user info
mask = NickMask(event.source)
connection.privmsg(channel, f"Welcome {nick} from {mask.host}!")
def on_pubmsg(connection, event):
message = event.arguments[0]
channel = event.target
nick = event.source.nick
mask = NickMask(event.source)
# Track message stats
if channel in self.channel_stats:
self.channel_stats[channel]["messages"] += 1
# Store user info
self.user_data[nick] = {
"host": mask.host,
"last_message": message,
"last_seen": datetime.datetime.now().isoformat()
}
# Handle commands
if message.startswith("!"):
self.handle_command(connection, channel, nick, message)
def on_mode(connection, event):
target = event.target
mode_string = event.arguments[0]
if is_channel(target):
modes = parse_channel_modes(mode_string)
print(f"Mode change in {target}: {modes}")
self.client.connection.add_global_handler("welcome", on_connect)
self.client.connection.add_global_handler("join", on_join)
self.client.connection.add_global_handler("pubmsg", on_pubmsg)
self.client.connection.add_global_handler("mode", on_mode)
def handle_command(self, connection, channel, nick, message):
"""Handle bot commands."""
cmd_parts = message[1:].split()
command = cmd_parts[0].lower()
if command == "userinfo":
target_nick = cmd_parts[1] if len(cmd_parts) > 1 else nick
user_info = self.user_data.get(target_nick)
if user_info:
response = f"{target_nick}: Host={user_info['host']}, Last seen={user_info['last_seen']}"
connection.privmsg(channel, response)
else:
connection.privmsg(channel, f"No info available for {target_nick}")
elif command == "stats":
if channel in self.channel_stats:
stats = self.channel_stats[channel]
response = f"{channel}: {stats['joins']} joins, {stats['messages']} messages"
connection.privmsg(channel, response)
elif command == "ischannel":
if len(cmd_parts) > 1:
test_string = cmd_parts[1]
result = "is" if is_channel(test_string) else "is not"
connection.privmsg(channel, f"'{test_string}' {result} a channel")
elif command == "lower":
if len(cmd_parts) > 1:
test_string = " ".join(cmd_parts[1:])
lowered = lower(test_string)
connection.privmsg(channel, f"IRC lowercase: '{lowered}'")
elif command == "ip":
if len(cmd_parts) > 1:
try:
ip_addr = cmd_parts[1]
numeric = ip_quad_to_numstr(ip_addr)
connection.privmsg(channel, f"{ip_addr} = {numeric} (DCC format)")
except:
connection.privmsg(channel, "Invalid IP address format")
elif command == "export":
# Export user data as JSON
data = dict(self.user_data) # Convert IRCDict to regular dict
json_data = json.dumps(data, indent=2)
# Send in private to avoid spam
connection.privmsg(nick, "User data export:")
for line in json_data.split('\n')[:20]: # Limit lines
connection.privmsg(nick, line)
elif command == "help":
help_text = [
"Available commands:",
"!userinfo [nick] - Show user information",
"!stats - Show channel statistics",
"!ischannel <string> - Test if string is channel name",
"!lower <text> - Convert to IRC lowercase",
"!ip <address> - Convert IP to DCC format",
"!export - Export user data (private message)",
"!help - Show this help"
]
for line in help_text:
connection.privmsg(nick, line)
def start(self):
"""Start the utility bot."""
self.client.connect("irc.libera.chat", 6667, "utilitybot")
self.client.start()
# Usage
bot = UtilityBot()
bot.start()Install with Tessl CLI
npx tessl i tessl/pypi-irc