Subclass IRCBot and override any of the on_* methods. For IRC commands not handled by the named hooks, define on_raw_<command>(self, msg) and it will be called automatically.
IRCBot is designed to be subclassed. All event hooks are regular async methods with no-op or minimal default behavior. Override only what you need.
import asyncio
from ircbot import IRCBot, BotConfig, load_env
from ircbot.commands import register_builtins
from ircbot.protocol import IRCMessage
class MyBot(IRCBot):
async def on_welcome(self, msg: IRCMessage) -> None:
# Always call super() unless you want to skip auto-join
await super().on_welcome(msg)
await self.privmsg("NickServ", f"IDENTIFY {self.config.password}")
async def on_join(self, msg: IRCMessage) -> None:
await super().on_join(msg)
if msg.nick != self.nick:
await self.privmsg(msg.target, f"Welcome, {msg.nick}!")
load_env()
bot = MyBot(BotConfig.from_env())
register_builtins(bot)
asyncio.run(bot.run())These methods are called by the internal dispatcher. Override any of them in a subclass.
Called when the server sends numeric 001 (RPL_WELCOME), which signals that registration is complete. The default implementation joins all channels listed in config.channels.
async def on_welcome(self, msg: IRCMessage) -> None:
await super().on_welcome(msg)
# Custom post-connect logic hereCalled on every PRIVMSG. The default implementation calls _try_command to check for a command prefix; only resolved commands are logged at info level.
If you override this without calling super(), command dispatch stops working.
async def on_privmsg(self, msg: IRCMessage) -> None:
# Intercept all messages before command dispatch
if "badword" in msg.text.lower():
await self.privmsg(msg.target, f"{msg.nick}: watch your language.")
return # suppress command dispatch for this message
await super().on_privmsg(msg)Called when any user (including the bot) joins a channel. msg.nick is the joiner, msg.target is the channel. The default logs the event.
Called when any user parts a channel. msg.target is the channel. If the user supplied a part message, it is in msg.text. The default logs the event.
Called when a user is kicked. msg.params[1] is the kicked nick. msg.target is the channel. The default logs the event and, if the bot itself was kicked, waits 5 seconds and rejoins.
async def on_kick(self, msg: IRCMessage) -> None:
await super().on_kick(msg) # handles auto-rejoin
# Additional logic, e.g. log to a databaseAfter the named hooks run, the dispatcher checks for a method named on_raw_<command> where <command> is the lowercased IRC command or numeric. This lets you handle any IRC message type without modifying the dispatcher.
class MyBot(IRCBot):
async def on_raw_notice(self, msg: IRCMessage) -> None:
"""Handle NOTICE messages."""
print(f"NOTICE from {msg.nick}: {msg.text}")
async def on_raw_332(self, msg: IRCMessage) -> None:
"""332 is RPL_TOPIC -- sent when joining a channel."""
channel = msg.params[1] if len(msg.params) > 1 else msg.target
topic = msg.text
print(f"Topic for {channel}: {topic}")
async def on_raw_mode(self, msg: IRCMessage) -> None:
"""Handle MODE changes."""
print(f"MODE change on {msg.target}: {msg.text}")Named hooks and on_raw_* are not mutually exclusive. The dispatcher calls the named hook first (e.g. on_privmsg), then checks for on_raw_privmsg. Both will fire if both are defined.
Use await self.send(line) for anything not covered by the convenience helpers:
# Convenience helpers
await self.privmsg("#channel", "Hello")
await self.notice("user", "This is a notice")
await self.join("#channel")
await self.part("#channel", "Goodbye")
await self.quit("Shutting down")
# Reply to whoever sent a message (channel or DM)
await self.reply(msg, "Got it")
# Raw line for anything else
await self.send("MODE #channel +b *!*@spammer.example.com")
await self.send("WHOIS somenick")Add instance variables in __init__, calling super().__init__() first:
class StatefulBot(IRCBot):
def __init__(self, config):
super().__init__(config)
self.seen: dict[str, str] = {} # nick -> last message
async def on_privmsg(self, msg):
self.seen[msg.nick] = msg.text
await super().on_privmsg(msg)You do not need to subclass IRCBot for simple bots. Register commands on a plain instance and run it:
load_env()
bot = IRCBot(BotConfig.from_env())
register_builtins(bot)
@bot.command("echo", help="Repeat your message back")
async def echo(bot, msg, args):
await bot.reply(msg, args)
asyncio.run(bot.run())Subclassing is most useful when you need to intercept events (joins, parts, raw numerics) or maintain persistent state across the bot's lifetime.