Framework Documentation
This document provides an overview and a functional guide for the nIRC Bot Framework. It is designed for high-performance, non-blocking interaction with IRC servers. The framework abstracts raw IRC protocol handling, offering an elegant, decorator-based interface for defining commands and reacting to server events.
Core Configuration
The following constants manage the bot's identity and connection parameters. In a production environment, these should be loaded from secure environment variables or a configuration file.
| Constant | Type | Description | Example |
|---|---|---|---|
| BOT_NICK | str | The nickname the bot will use on the server. | "ServiceBot" |
| BOT_USERNAME | str | The bot's ident/username. | "svc_agent" |
| BOT_REALNAME | str | The bot's "real name" shown in WHOIS. | "IRC Service Agent v1.0" |
| BOT_PASSWORD | Optional[str] | Password for NickServ identification. Set to None if not required. |
"MySecurePass" |
| COMMAND_PREFIX | str | The character(s) used to trigger bot commands. | "!" |
| CHANNELS_TO_JOIN | Dict[str, Optional[str]] | Map of channels to join on connection with optional keys. | {"#public": None, "#admin": "key123"} |
Core Abstractions
The framework is built on a few key classes that abstract away the complexity of the IRC protocol, allowing you to focus on building features.
The Bot Class (The Engine)
The main class responsible for managing the connection, registering handlers, dispatching events, and managing background tasks.
Constructor Arguments (__init__)
| Argument | Type | Description | Required |
|---|---|---|---|
| prefix | str | The command prefix (e.g., ! or .). | Yes |
| conn | IRCConnection | The pre-configured IRCConnection object used for I/O. | Yes |
| nick | str | The bot's primary nickname. | Yes |
| username | str | The bot's username (IDENT). | Yes |
| realname | str | The bot's real name (GECOS). | Yes |
| password | Optional[str] | Password for NickServ identification, if required. (Default: None) | No |
| downloads_dir | str | Directory for saving files from DCC send requests. (Default: downloads) | No |
General Methods
| Method / Property | Type | Description |
|---|---|---|
| start(channel_map) | async method | Establishes the connection, performs initial setup, and enters the main event loop. |
| send_message(...) | method | Sends a raw PRIVMSG to a specified channel or user. |
| send_raw(message) | async method | Sends a raw IRC protocol command directly to the server. |
| start_task(task_func, *args) | method | Starts a background task defined with @bot.task. |
| load_cog(cog_name) | method | Dynamically loads commands and event handlers and tasks from a separate file. |
| unload_cog(cog_name) | method | Unloads a loaded Cog while removing the Cog commands from the registry. |
| reload_cog(cog_name) | method | Hot reloads a Cog's contents. |
| oper(username, password) | async method | Sets OPER mode for the bot on the entire IRC server. |
Decorators for Event Binding
| Decorator | Contextual Event | Context Type | Description |
|---|---|---|---|
| @bot.command(name) | Standard Command | Context | Binds a function to a command triggered via COMMAND_PREFIX (e.g., !help). |
| @bot.on_ready() | Connection Ready | Bot instance | Fires **once** after successful IRC registration (numeric 001/376). Ideal for initialization. |
| @bot.on_raw() | All Raw Lines | Context | Fires for every single, unfiltered line received from the IRC server. Use ctx.full_line. |
| @bot.on_message() | Any Channel Message | Context | Fires on all PRIVMSG events that are **not** commands. Essential for keyword checks. |
| @bot.on_join() | User joins channel | Context | Binds a function to the JOIN IRC event. |
| @bot.on_leave() | User parts or quits | Context | Binds a function to the PART or QUIT IRC event. |
| @bot.on_dcc() | DCC File Transfer | DCCFile instance | Fires when the bot receives a DCC SEND request and initiates the file transfer process. |
| @bot.on_nick() | Nick change | Context | Fires after each user NICK change. |
| @bot.prefix_command(prefix) | Prefixless Command | Context | Binds to a message starting with prefix (e.g., >). |
The DCCFile Class (DCC File Transfer)
This class is passed when the @bot.on_dcc() event fires. It represents an incoming Direct Client-to-Client (DCC) SEND request and provides access to file metadata and transfer progress.
Key Attributes (Metadata and Status)
| Attribute | Type | Description |
|---|---|---|
| context | Context | The context object. |
| filename | str | The name of the file being sent, as reported in the DCC command. |
| extension | str | The file extension. |
| filesize | int | The total expected size of the file in bytes. |
| ip_address | str | The dotted IPv4 address (e.g., 12.34.56.78) of the sending client. |
| port | int | The port number the sender is listening on for the DCC connection. |
| is_done | bool | Whether the file is done sending. |
| is_good | bool | Whether the file operation completed without errors. |
| percent | float | The percentage of the operation. |
| progress | int | The progress in bytes of how much data got transfered. |
| sender | str | The nickname of the user who initiated the file transfer. |
| full_path | str | The full local path where the file is being saved. |
| safe_filename | str | The filename but with underscores instead of spaces. |
| save_dir | str | The parent directory to where the file will be saved into. |
Helper Methods
| Method / Property | Type | Description | Example |
|---|---|---|---|
| start_transfer | property (None) | Starts the transfer process and returns a bool depending on whether it failed or succeeded. | dcc_file.start_transfer() |
The IRCConnection Class (Network I/O)
This class manages the asynchronous socket connection, handles raw reading and writing of IRC data, and buffers messages using asyncio streams.
Constructor Arguments (__init__)
| Argument | Type | Description | Required |
|---|---|---|---|
| host | str | The IRC server hostname or IP address. | Yes |
| port | int | The IRC server port (usually 6667). | Yes |
| quit_msg | Optional[str] | The parting message (Default: None). | No |
| logger | Optional[Logger] | A Logger instance for logging network events. If None, a silent logger is used. (Default: None) | No |
Key Methods
| Method | Type | Description |
|---|---|---|
| connect() | async method | Establishes the socket connection to the IRC server. |
| send_raw(message) | async method | Sends a raw IRC protocol message line to the server. |
| read_line() | async method | Asynchronously reads a single, stripped line from the server. Returns None on connection close. |
| close() | method | Closes the network connection writer and updates state. |
The Logger Class (Logging Utility)
A utility class for structured logging output to the console and an optional file, with filtering based on configurable log levels.
Constructor Arguments (__init__)
| Argument | Type | Description | Default |
|---|---|---|---|
| file_path | Optional[str] | Path to the log file. If provided, all logs are appended to this file. | None |
| min_level | int (LogLevel) | The minimum log level (e.g., LogLevel.INFO) required for a message to be outputted. | LogLevel.INFO |
Logging Methods
| Method | Log Level | Description |
|---|---|---|
| debug(...) | DEBUG (10) | For fine-grained debugging information, including raw network traffic. |
| info(...) | INFO (20) | For standard confirmation messages and general bot state changes. |
| error(...) | ERROR (40) | For critical, non-fatal errors that prevent certain operations. |
| raw_recv(line) | DEBUG (10) | Specifically logs a raw line received from the IRC server. |
| raw_send(message) | DEBUG (10) | Specifically logs a raw message being sent to the IRC server. |
Logging Constants: LogLevel and LOG_PREFIX
The LogLevel Class
Defines standard logging levels used for filtering output verbosity in the Logger.
| Constant | Value | Description |
|---|---|---|
| DEBUG | 10 | The lowest level; typically includes all raw network I/O. |
| INFO | 20 | Standard status messages and successful operations. |
| ERROR | 40 | Critical failures or issues preventing core functionality. |
The LOG_PREFIX Dictionary
A dictionary mapping log categories to short prefixes for organized, human-readable log output.
| Key | Prefix | Category |
|---|---|---|
| "NET" | "[NET]" | Network connection status and disconnects. |
| "ERROR" | "[ERROR]" | Errors caught during processing. |
| "RAW" | "[RAW]" | Unfiltered, raw IRC lines (typically DEBUG level). |
| "DISPATCH" | "[DISPATCH]" | Event and command handler dispatch actions. |
| "TASK" | "[TASK]" | Background task lifecycle events. |
| "CORE" | "[CORE]" | Bot internal state changes (e.g., registration). |
| "USER" | "[USER]" | User events like JOIN/PART/QUIT. |
| "COMMAND" | "[COMMAND]" | When a user executes a command. |
| "DCC" | "[DCC]" | Direct Client-to-Client (DCC) file transfer events, including connection and progress. |
The Context Class (The Data Carrier)
An object passed to all command and event handlers. It contains the message payload, high-level response methods, and access to channel and user objects.
| Attribute | Type | Description |
|---|---|---|
| bot | Bot | A direct reference to the main Bot instance. Useful for accessing global properties or methods. |
| logger | Logger | A convenience reference to bot.logger for quick logging within a context. |
| author | str | Nickname of the user who triggered the event. |
| target | str | Destination where the bot should reply. |
| message | str | The full raw message text received. |
| command_name | str | The invoked command name (only for standard/prefix commands). |
| arg | str | The full argument string following the command/prefix. |
| args | List[str] | The arguments split by whitespace. |
| full_line | str | The complete, raw IRC line from the server (used primarily with @bot.on_raw). |
Response and Interaction Methods
| Method / Property | Type | Description | Example |
|---|---|---|---|
| send(text) | method | Sends a message to ctx.target. | ctx.send("Reply in channel.") |
| reply(text) | async method | Alias for send() for improved readability. | await ctx.reply("Confirmation received.") |
| author_obj | property (Member) | Returns a Member object for the message author. | ctx.author_obj.send("Sent privately.") |
| channel_obj | property (Channel) | Returns a Channel object representing the target of the message. | ctx.channel_obj.unban('BadUser') |
| get_member(nick) | method (Member) | Retrieves a Member object for any specified nickname. | ctx.get_member('Admin') |
| unban(target_user) | method | Removes ban for a target_user from the current ctx.target channel. | ctx.unban('BannedUser') |
The Member Class (User Interaction & Moderation)
A utility class representing a user, focused on private communication and moderator actions.
Attributes
| Attribute | Type | Description |
|---|---|---|
| bot | Bot | A direct reference to the main Bot instance. |
Methods
| Method | Type | Description | Example |
|---|---|---|---|
| send(text) | method | Sends a direct private message (PRIVMSG) to the member's nickname. | user.send("Your query is complete.") |
| kick(channel, reason) | method | Kicks the member from the specified channel. | user.kick('#general', 'Spam') |
| ban(channel, reason) | method | Bans the member's hostmask from the specified channel. | user.ban('#general', 'Flood') |
| mute(channel) | method | Mutes the member (server-side mute/voice removal in channel). | user.mute('#general') |
| unmute(channel) | method | Unmutes the member. | user.unmute('#general') |
| is_muted(channel) | method (bool) | Checks the local cache if the user is currently muted/voiced down in the channel. | if user.is_muted('#general'): ... |
The Channel Class (Target Abstraction)
A class representing an IRC channel, providing methods for channel-specific management.
Attributes
| Attribute | Type | Description |
|---|---|---|
| bot | Bot | A direct reference to the main Bot instance. |
Methods
| Method | Type | Description |
|---|---|---|
| get_topic() | method (str) | Returns the current topic of the channel. |
| set_topic(new_topic) | method | Sets a new topic for the channel. |
| unban(user_mask) | method | Removes a ban by user_mask (e.g., *!*@host). |
| oper() | async method | Becomes an OPER on the current channel, the bot has to have permission to do so, usually via Bot.oper(). |
Asynchronous Task Runner
The framework includes a simple mechanism for defining and running background, repeating asynchronous tasks without blocking the main event loop. **Tasks must be explicitly started using bot.start_task(...), usually within an @bot.on_ready() handler.**
Task Definition and Control
| Decorator / Method | Parameters | Description |
|---|---|---|
| @bot.task(...) | interval (float), max_repeat (int, optional) | Decorator to mark an async function as a repeating task. max_repeat sets a limit, after which the task stops. |
| bot.start_task(func, *args) | task_func, args (optional) | Initiates the task defined by @bot.task, passing any arguments required by the function. |
Example Bot Script
This script demonstrates command handling, prefix commands, asynchronous tasks, and event handlers.
from nIRC.irc import Bot, Context, IRCConnection, Logger
from typing import Optional
import asyncio
BOT_NICK = "nIRC"
COMMAND_PREFIX = "!"
CHANNELS_TO_JOIN = {
"#chat": None,
"#logs": None,
}
SERVER = "127.0.0.1"
PORT = 6667
SERVER_PASSWORD: Optional[str] = None
@Bot.command("load")
async def load_cmd(ctx: Context):
if not ctx.arg:
await ctx.reply("Usage: !load ")
return
cog_name = ctx.arg
try:
res= ctx.bot.load_cog(cog_name)
if res[0]== 0:
await ctx.reply(f"Cog '{cog_name}' loaded successfully.")
elif res[0]== 1:
await ctx.reply(f"Cog '{cog_name}' is already loaded.")
else:
await ctx.reply(f"Error loading Cog:")
for line in str(res[1]).splitlines():
await ctx.send(line)
except Exception as e:
await ctx.reply(f"Error loading cog '{cog_name}': {e}")
@Bot.command("unload")
async def unload_cmd(ctx: Context):
if not ctx.arg:
await ctx.reply("Usage: !unload ")
return
cog_name = ctx.arg
try:
res= ctx.bot.unload_cog(cog_name)
if res[0]== 0:
await ctx.reply(f"Cog '{cog_name}' unloaded successfully.")
elif res[0]== 1:
await ctx.reply(f"Cog '{cog_name}' is not loaded.")
else:
await ctx.reply(f"Error unloading Cog:")
for line in str(res[1]).splitlines():
await ctx.send(line)
except Exception as e:
await ctx.reply(f"Error unloading cog '{cog_name}': {e}")
@Bot.command("reload")
async def reload_cmd(ctx: Context):
if not ctx.arg:
await ctx.reply("Usage: !reload ")
return
cog_name = ctx.arg
try:
res= ctx.bot.reload_cog(cog_name)
if res[0]== 0:
await ctx.reply(f"Cog '{cog_name}' reloaded successfully.")
elif res[0]== 1:
await ctx.reply(f"Cog '{cog_name}' is not loaded.")
else:
await ctx.reply(f"Error reloading Cog:")
for line in str(res[1]).splitlines():
await ctx.send(line)
except Exception as e:
await ctx.reply(f"Error reloading cog '{cog_name}': {e}")
@Bot.on_ready()
async def initialization_setup(bot: Bot):
"""
Runs once after connection and IRC registration is complete (after 376).
Starts tasks and can perform one-time setup actions.
"""
if "#chat" in bot.channel_map:
await bot.conn.send_raw(f"MODE #chat +m") # Set +m (moderated) on #chat as an example one-time setup.
# You can load cogs on startup here, try using the following:
# bot.load_cog("cogs.test")
async def run_bot():
"""Main entry point to initialize and run the bot."""
global logger
logger= Logger(file_path= "log.txt", min_level= 0)
irc_connection = IRCConnection(SERVER, PORT, logger)
bot = Bot(
prefix=COMMAND_PREFIX,
conn=irc_connection,
nick=BOT_NICK,
username="nirc_bot",
realname="neko IRC bot framework example client",
password=SERVER_PASSWORD
)
try:
await bot.start(CHANNELS_TO_JOIN) #type: ignore
except Exception as e:
print(f"FATAL BOT ERROR: {e}")
if __name__ == "__main__":
print("--- Starting Live Asynchronous nIRC Bot Framework Example ---")
print(f"Connecting as NICK: {BOT_NICK} to {SERVER}:{PORT}")
print(f"Channels configured: {', '.join(CHANNELS_TO_JOIN.keys())}")
try:
asyncio.run(run_bot())
except KeyboardInterrupt:
print("\n[BOT] Shutting down gracefully via Ctrl+C.")
except RuntimeError as e:
if "cannot run" in str(e):
print("\n[ERROR] Event loop error. If running in an interactive session, this is normal.")
else:
raise
Example Cog Script
This script demonstrates Cog integration.
As per convention, Cogs are placed in a "cogs" folder and loaded by issuing Bot.load_cog("cogs.cog_filename_without_ext").
from nIRC.irc import Bot, Context, DCCFile, Member
@Bot.command("hello")
async def hello_from_cog(ctx: Context):
"""A simple command loaded from a cog."""
await ctx.reply(f"Hello {ctx.author}! This command was loaded from test.py.")
@Bot.task(interval=60.0, max_repeat=5)
async def cog_task(bot_instance: Bot):
"""A recurring task loaded from a cog."""
current = getattr(cog_task, 'current_repeat', 0)
total = getattr(cog_task, 'max_repeat', 0)
bot_instance.logger.info("TASK", f"[COG] Cog task is running! Repeat {current}/{total}")
for channel in bot_instance.channel_map:
await bot_instance.send_message(channel, f"Cog task reporting in! (Run {current}/{total})")
@Bot.command("tasker")
async def cog_task_runner(ctx: Context):
"""Runs an nIRC bot task specifically one that requires no args"""
task_name= ctx.arg
if not ctx.arg:
await ctx.reply("Usage: !tasker ")
return
try:
func= ctx.bot.task_registry[task_name]
ctx.bot.start_task(func)
await ctx.reply(f"Running task: {task_name}")
except Exception as e:
await ctx.reply(f"Failed to run task: {e}")
@Bot.command("rmcmd")
async def cog_rm_cmd(ctx: Context):
"""Removes a command from the runnable commands"""
if not ctx.arg:
await ctx.reply("Usage: !rmcmd ")
return
removed= False
cmd_name= ctx.arg
old_reg= ctx.bot.commands.copy()
ctx.bot.commands.clear()
for cmd, func in old_reg.items():
if not cmd == cmd_name:
ctx.bot.commands[cmd]= func # Note: this will most likely break on a cog unload due to the main _event_registry remaining intact
else:
removed= True
if removed:
await ctx.reply(f"Removed command: '{cmd_name}'.")
else:
await ctx.reply(f"Commands '{cmd_name}' not found.")
@Bot.command("commands")
async def loaded_commands(ctx: Context):
"""Shows what commands you have loaded"""
coms= ""
pcoms= ""
events= ""
tasks= ""
for com in ctx.bot.commands:
coms+= str(com)+ " "
for pcom in ctx.bot.prefix_commands:
pcoms+= str(pcom)+ " "
for event in ctx.bot.event_handlers:
events+= str(event)+ f"({len(ctx.bot.event_handlers[event])}) "
for task in ctx.bot.task_registry:
tasks+= str(task)+ " "
await ctx.send("===Registered Events=======================================")
await ctx.send(f"Commands: {coms}")
await ctx.send(f"Prefix Commands: {pcoms}")
await ctx.send(f"Events: {events}")
await ctx.send(f"Tasks: {tasks}")
await ctx.send("===========================================================")
@Bot.prefix_command(">")
async def ai_query_command(ctx: Context):
"""
Fires on any message starting with '>' (e.g., >what is my status).
This is great for clean, non-standard bot interactions.
"""
if ctx.arg:
response = f"AI Service received query from {ctx.author}: '{ctx.arg}'. Processing..."
await ctx.reply(response)
await ctx.send(str(ctx.args))
await ctx.send(str(ctx.full_line))
else:
await ctx.reply("Please provide a query after the '>'.")
@Bot.command("calc")
async def calculate_command(ctx: Context):
"""
Demonstrates using ctx.arg and ctx.args.
A simplified math evaluator.
"""
if not ctx.arg:
await ctx.reply("Usage: !calc (e.g., 5 + 3 * 2)")
return
try:
# Warning: Using eval() is generally unsafe in production bots.
# This is for demonstration purposes only.
result = eval(ctx.arg)
await ctx.reply(f"Result for '{ctx.arg}': {result}")
except Exception:
await ctx.reply(f"Could not calculate the expression: {ctx.arg}")
@Bot.command("mod")
async def mod_command(ctx: Context):
"""
Handles moderation actions using Member and Channel abstractions.
Usage: !mod [reason/value]
"""
if len(ctx.args) < 2:
await ctx.reply("Usage: !mod [value]")
return
action = ctx.args[0].lower()
target_nick_or_mask = ctx.args[1]
value_or_reason = " ".join(ctx.args[2:]) or "No reason provided."
if action == "kick":
member = ctx.get_member(target_nick_or_mask)
await member.kick(ctx.target, value_or_reason)
await ctx.reply(f"Attempted to kick {target_nick_or_mask} from {ctx.target}.")
elif action == "ban":
member = ctx.get_member(target_nick_or_mask)
await member.ban(ctx.target, value_or_reason)
await ctx.reply(f"Attempted to ban and kick {target_nick_or_mask} from {ctx.target}.")
elif action == "topic" and ctx.target.startswith('#'):
await ctx.channel_obj.set_topic(value_or_reason)
await ctx.reply(f"New topic set to: {value_or_reason}")
elif action == "unban" and ctx.target.startswith('#'):
await ctx.channel_obj.unban(target_nick_or_mask)
await ctx.reply(f"Attempted to remove ban mask: {target_nick_or_mask}")
else:
await ctx.reply(f"Unknown moderation action: {action}. Use kick, ban, topic, or unban.")
@Bot.command("pmuser")
async def pm_user_command(ctx: Context):
"""Sends a private message to a specified user."""
if len(ctx.args) < 2:
await ctx.reply("Usage: !pmuser ")
return
recipient_nick = ctx.args[0]
message_text = " ".join(ctx.args[1:])
await ctx.bot.send_message(recipient_nick, f"PM from {ctx.author}: {message_text}")
await ctx.reply(f"PM sent to {recipient_nick}.")
@Bot.on_message()
async def keyword_responder(ctx: Context):
"""Responds to specific keywords in a channel message."""
message = ctx.message.lower()
if message.strip() == ctx.bot.nick.lower():
await ctx.reply(f"My command prefix is '{ctx.bot.prefix}'.")
coms= ""
for com in ctx.bot.commands:
coms+= ctx.bot.prefix+ str(com)+ " "
await ctx.send(f"Available commands: {coms}")
@Bot.on_join()
async def greet_joiner(ctx: Context):
"""Sends a friendly greeting when a new user joins."""
if ctx.author != ctx.bot.nick:
await ctx.reply(f"Welcome, {ctx.author}! Type {ctx.bot.prefix}commands to get a list of commands and events.")
@Bot.on_raw()
async def raw_logger(ctx: Context):
"""Logs the raw line to the console (for demonstration only, triggers on all lines)."""
if "ERROR" in ctx.full_line or "NOTICE" in ctx.full_line:
print(f"[RAW LOG] IMPORTANT LINE: {ctx.full_line}")
@Bot.prefix_command("?")
async def cog_prefix_cmd(ctx: Context):
"""A prefix command loaded from a cog."""
await ctx.reply(f"Cog prefix command '?' triggered with: {ctx.arg}")
@Bot.on_dcc()
async def get_file(file: DCCFile):
"""DCC handler for receiving files over DCC"""
file.context.logger.info("USER", f"Accepting file '{file.filename}' from {file.sender}.")
user = Member(file.context.bot, file.sender)
await user.send("Thanks for the file, it's *definitely* safe :3")
await file.start_transfer()