Skip to main content

Build an IRC Bot with Python

A complete step-by-step tutorial

By TwistedNET Team

IRC bots are one of the most rewarding programming projects you can build. They are practical, interactive, and give you a deep understanding of how network protocols work under the hood. Whether you want to automate channel moderation, provide useful information to users, or just create something fun, building an IRC bot with Python is the perfect way to get started.

In this comprehensive tutorial, we will walk through the entire process of creating an IRC bot from scratch using nothing but Python's standard library. No third-party frameworks, no abstraction layers. Just raw sockets, the IRC protocol, and your own code. By the end, you will have a fully functional bot running on TwistedNET's IRC network that responds to commands, handles errors gracefully, and can be extended with any feature you can imagine.

Why Build an IRC Bot?

// Practical reasons to dive in

Building an IRC bot teaches you fundamental concepts that apply across all of software engineering. You will learn about TCP socket programming, text-based protocol parsing, event-driven architecture, and state management. These are the same skills used in building web servers, API clients, and distributed systems.

Beyond the educational value, IRC bots serve genuinely useful purposes. Channel bots can moderate conversations, log important discussions, provide real-time notifications from external services, run trivia games, look up weather data, translate messages, or even serve as bridges to other platforms. On TwistedNET's chat rooms, bots are a valued part of the community.

Python is the ideal language for this project. Its readable syntax makes the protocol logic easy to follow, its standard library includes everything you need for socket and SSL connections, and its string manipulation capabilities make parsing IRC messages straightforward. You do not need to install a single package to get started.

Prerequisites

// What you need before we start

$
Python 3.8 or later

Any modern Python 3 version will work. Check yours with python3 --version

$
Basic Python knowledge

Functions, loops, string handling, and basic OOP. Nothing advanced required.

$
An IRC client for testing

Use TwistedNET's web client or install one from our IRC clients page.

$
A text editor or IDE

VS Code, Vim, Emacs, or any editor you are comfortable with.

Understanding the IRC Protocol

// How IRC works at the wire level

Before writing any code, it helps to understand what IRC actually is at the protocol level. IRC (Internet Relay Chat) is a text-based protocol that operates over TCP. Every message between your bot and the server is a simple line of text terminated by \r\n. There are no binary headers, no complex framing, and no compression. This simplicity is what makes IRC so accessible to implement from scratch.

When your bot connects to an IRC server like irc.twistednet.org, it needs to perform a handshake by sending a few specific commands. The server responds with numeric codes and text messages. Your bot reads these responses, parses them, and reacts accordingly. The entire conversation is human-readable, which makes debugging straightforward.

Here are the core IRC commands your bot needs to know. For a full list, check our IRC commands reference:

# Connection registration
NICK mybotname                    # Set your bot's nickname
USER mybotname 0 * :My IRC Bot   # Set username and realname

# Channel operations
JOIN #channel                     # Join a channel
PART #channel :Goodbye            # Leave a channel
PRIVMSG #channel :Hello world     # Send a message to a channel
PRIVMSG username :Hello           # Send a private message

# Server interaction
PING :server                      # Server sends this to check if you're alive
PONG :server                      # You must respond with this
QUIT :Reason                      # Disconnect from the server

Messages received from the server follow a specific format. A typical channel message looks like this:

:nick!user@host PRIVMSG #channel :This is the message text

The prefix (everything before the first space, starting with a colon) identifies who sent the message. The command comes next, followed by parameters. Understanding this format is crucial because your bot will need to parse every incoming line to figure out what is happening and how to respond.

Building the Minimal Bot

// From zero to connected in under 100 lines

Let us start by building the simplest possible IRC bot. This initial version will connect to TwistedNET using SSL, register a nickname, join a channel, and respond to a basic command. We will use only Python's built-in socket and ssl modules.

#!/usr/bin/env python3
"""A minimal IRC bot for TwistedNET."""

import socket
import ssl

# Configuration
SERVER = "irc.twistednet.org"
PORT = 6697  # SSL port
NICK = "MyPyBot"
CHANNEL = "#twisted"
REALNAME = "Python IRC Bot"

def create_ssl_connection(server, port):
    """Create an SSL-wrapped socket connection to the IRC server."""
    raw_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ssl_context = ssl.create_default_context()
    wrapped_socket = ssl_context.wrap_socket(
        raw_socket,
        server_hostname=server
    )
    wrapped_socket.connect((server, port))
    return wrapped_socket

def send_raw(sock, message):
    """Send a raw IRC message, appending CRLF."""
    sock.send(f"{message}\r\n".encode("utf-8"))
    print(f">> {message}")

def main():
    # Connect with SSL
    print(f"Connecting to {SERVER}:{PORT} with SSL...")
    irc = create_ssl_connection(SERVER, PORT)
    print("Connected!")

    # Register with the server
    send_raw(irc, f"NICK {NICK}")
    send_raw(irc, f"USER {NICK} 0 * :{REALNAME}")

    # Buffer for incomplete lines
    buffer = ""

    while True:
        # Receive data from server
        data = irc.recv(4096).decode("utf-8", errors="replace")
        if not data:
            print("Disconnected from server.")
            break

        buffer += data
        lines = buffer.split("\r\n")
        buffer = lines.pop()  # Keep incomplete line in buffer

        for line in lines:
            if not line:
                continue
            print(f"<< {line}")

            # Respond to PING to stay connected
            if line.startswith("PING"):
                pong_response = line.replace("PING", "PONG", 1)
                send_raw(irc, pong_response)
                continue

            # Join channel after welcome message (numeric 001)
            if " 001 " in line:
                send_raw(irc, f"JOIN {CHANNEL}")
                continue

            # Parse PRIVMSG for commands
            if "PRIVMSG" in line:
                parts = line.split(" ", 3)
                if len(parts) >= 4:
                    sender = parts[0].split("!")[0][1:]
                    target = parts[2]
                    message = parts[3][1:]

                    # Respond to !hello
                    if message.strip() == "!hello":
                        reply_to = target if target.startswith("#") else sender
                        send_raw(irc, f"PRIVMSG {reply_to} :Hello, {sender}!")

if __name__ == "__main__":
    main()

Save this as bot.py and run it with python3 bot.py. You should see the connection handshake scroll by, and your bot will join the channel. Type !hello in the channel using your IRC client, and the bot will greet you by name.

Understanding the Message Parser

// Breaking down IRC messages like a pro

The minimal bot above works, but its message parsing is fragile. Let us build a proper parser that handles any IRC message correctly. According to RFC 2812, an IRC message has this structure:

[:prefix] command [params] [:trailing]

Here is a robust parsing function that handles all cases:

def parse_irc_message(raw_line):
    """Parse a raw IRC message into its components.

    Returns a dict with keys: prefix, command, params, trailing
    """
    prefix = ""
    trailing = ""

    # Extract prefix if present
    if raw_line.startswith(":"):
        prefix, raw_line = raw_line[1:].split(" ", 1)

    # Extract trailing parameter (everything after " :")
    if " :" in raw_line:
        raw_line, trailing = raw_line.split(" :", 1)

    # Split remaining into command and params
    parts = raw_line.split()
    command = parts[0] if parts else ""
    params = parts[1:] if len(parts) > 1 else []

    return {
        "prefix": prefix,
        "nick": prefix.split("!")[0] if "!" in prefix else prefix,
        "command": command,
        "params": params,
        "trailing": trailing,
    }

# Example usage:
line = ":[email protected] PRIVMSG #twisted :!hello world"
msg = parse_irc_message(line)
print(msg["nick"])      # "Ghost"
print(msg["command"])   # "PRIVMSG"
print(msg["params"])    # ["#twisted"]
print(msg["trailing"])  # "!hello world"

Building a Full-Featured Bot

// Adding commands, error handling, and reconnection

Now that we understand the fundamentals, let us build a production-quality IRC bot using object-oriented design. This version includes multiple commands, automatic reconnection, admin authentication, channel logging, and clean error handling.

#!/usr/bin/env python3
"""TwistedNET IRC Bot - Full featured example."""

import socket
import ssl
import time
import datetime
import re
import urllib.request
import json

class IRCBot:
    def __init__(self, server, port, nick, channels, admins=None):
        self.server = server
        self.port = port
        self.nick = nick
        self.channels = channels
        self.admins = admins or []
        self.sock = None
        self.connected = False
        self.buffer = ""
        self.start_time = time.time()
        self.commands = {
            "!help": self.cmd_help,
            "!ping": self.cmd_ping,
            "!uptime": self.cmd_uptime,
            "!time": self.cmd_time,
            "!about": self.cmd_about,
            "!weather": self.cmd_weather,
        }

    def connect(self):
        """Establish an SSL connection to the IRC server."""
        raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        raw_sock.settimeout(300)
        ctx = ssl.create_default_context()
        self.sock = ctx.wrap_socket(raw_sock, server_hostname=self.server)
        self.sock.connect((self.server, self.port))
        self.connected = True
        print(f"[+] Connected to {self.server}:{self.port}")

        self.send(f"NICK {self.nick}")
        self.send(f"USER {self.nick} 0 * :TwistedNET Python Bot")

    def send(self, message):
        """Send a raw message to the server."""
        if self.sock and self.connected:
            self.sock.send(f"{message}\r\n".encode("utf-8"))

    def say(self, target, message):
        """Send a PRIVMSG to a channel or user."""
        self.send(f"PRIVMSG {target} :{message}")

    def parse(self, raw_line):
        """Parse a raw IRC line into components."""
        prefix = nick = ""
        trailing = ""
        if raw_line.startswith(":"):
            prefix, raw_line = raw_line[1:].split(" ", 1)
            nick = prefix.split("!")[0]
        if " :" in raw_line:
            raw_line, trailing = raw_line.split(" :", 1)
        parts = raw_line.split()
        return {
            "prefix": prefix, "nick": nick,
            "command": parts[0],
            "params": parts[1:],
            "trailing": trailing
        }

    def handle_message(self, msg):
        """Process a parsed IRC message."""
        cmd = msg["command"]

        if cmd == "PING":
            self.send(f"PONG :{msg['trailing']}")
        elif cmd == "001":
            for chan in self.channels:
                self.send(f"JOIN {chan}")
        elif cmd == "PRIVMSG":
            self.handle_privmsg(msg)
        elif cmd == "433":
            # Nickname in use - append underscore
            self.nick += "_"
            self.send(f"NICK {self.nick}")

    def handle_privmsg(self, msg):
        """Handle incoming PRIVMSG commands."""
        sender = msg["nick"]
        target = msg["params"][0] if msg["params"] else return
        text = msg["trailing"].strip()
        reply_to = target if target.startswith("#") else sender

        # Check for registered commands
        cmd_word = text.split()[0].lower() if text else ""
        if cmd_word in self.commands:
            self.commands[cmd_word](sender, reply_to, text)

        # Admin commands
        if sender in self.admins:
            if cmd_word == "!quit":
                self.send("QUIT :Admin shutdown")
                self.connected = False
            elif cmd_word == "!join":
                args = text.split()
                if len(args) > 1:
                    self.send(f"JOIN {args[1]}")

    # --- Command handlers ---

    def cmd_help(self, sender, reply_to, text):
        self.say(reply_to, "Available commands: !help, !ping, !uptime, "
                           "!time, !about, !weather <city>")

    def cmd_ping(self, sender, reply_to, text):
        self.say(reply_to, f"{sender}: Pong!")

    def cmd_uptime(self, sender, reply_to, text):
        elapsed = int(time.time() - self.start_time)
        hours, remainder = divmod(elapsed, 3600)
        minutes, seconds = divmod(remainder, 60)
        self.say(reply_to,
                 f"Uptime: {hours}h {minutes}m {seconds}s")

    def cmd_time(self, sender, reply_to, text):
        now = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
        self.say(reply_to, f"Current time: {now}")

    def cmd_about(self, sender, reply_to, text):
        self.say(reply_to,
                 "I'm a Python IRC bot running on TwistedNET! "
                 "Source: https://twistednet.org/blog/irc-bot-python")

    def cmd_weather(self, sender, reply_to, text):
        args = text.split(maxsplit=1)
        if len(args) < 2:
            self.say(reply_to, "Usage: !weather <city>")
            return
        city = args[1]
        try:
            url = f"https://wttr.in/{city}?format=3"
            req = urllib.request.Request(url, headers={
                "User-Agent": "TwistedNET-Bot/1.0"
            })
            resp = urllib.request.urlopen(req, timeout=5)
            self.say(reply_to, resp.read().decode().strip())
        except Exception:
            self.say(reply_to, f"Could not fetch weather for '{city}'")

    def run(self):
        """Main loop with automatic reconnection."""
        while True:
            try:
                self.connect()
                while self.connected:
                    data = self.sock.recv(4096).decode(
                        "utf-8", errors="replace"
                    )
                    if not data:
                        break
                    self.buffer += data
                    lines = self.buffer.split("\r\n")
                    self.buffer = lines.pop()
                    for line in lines:
                        if line:
                            msg = self.parse(line)
                            self.handle_message(msg)
            except (socket.error, ssl.SSLError, OSError) as e:
                print(f"[!] Connection error: {e}")
            finally:
                if self.sock:
                    self.sock.close()
                self.connected = False
            if not self.connected:
                print("[*] Reconnecting in 30 seconds...")
                time.sleep(30)


if __name__ == "__main__":
    bot = IRCBot(
        server="irc.twistednet.org",
        port=6697,
        nick="PyBot",
        channels=["#twisted", "#bots"],
        admins=["YourNick"]
    )
    bot.run()

Adding URL Title Fetching

// Automatically display page titles when URLs are shared

One of the most useful features you can add to a channel bot is URL title fetching. When someone pastes a link in the channel, the bot automatically fetches the page and displays its title. This saves everyone from having to click links just to see what they are about.

import re
import html
import urllib.request

def fetch_url_title(url, timeout=5):
    """Fetch the <title> of a given URL."""
    try:
        req = urllib.request.Request(url, headers={
            "User-Agent": "Mozilla/5.0 (compatible; IRCBot/1.0)"
        })
        resp = urllib.request.urlopen(req, timeout=timeout)
        # Only process HTML content
        content_type = resp.headers.get("Content-Type", "")
        if "text/html" not in content_type:
            return None
        # Read first 16KB only
        data = resp.read(16384).decode("utf-8", errors="replace")
        match = re.search(r"<title[^>]*>(.+?)</title>", data, re.IGNORECASE | re.DOTALL)
        if match:
            title = html.unescape(match.group(1)).strip()
            title = " ".join(title.split())  # Normalize whitespace
            return title[:200]
    except Exception:
        pass
    return None

# In your handle_privmsg method, add:
urls = re.findall(r"https?://\S+", text)
for url in urls[:2]:  # Limit to 2 URLs per message
    title = fetch_url_title(url)
    if title:
        self.say(reply_to, f"[ {title} ]")

Error Handling and Reconnection

// Making your bot resilient

A production IRC bot needs to handle network failures gracefully. Connections drop, servers restart, and netsplits happen. Your bot should detect these situations and automatically reconnect without crashing. The full-featured bot above already includes basic reconnection, but here are some additional hardening techniques.

Implement exponential backoff for reconnection attempts. Instead of waiting a fixed 30 seconds between retries, increase the delay each time the connection fails consecutively. This prevents hammering the server when there is a genuine outage.

def run_with_backoff(self):
    """Run the bot with exponential backoff on reconnection."""
    retry_delay = 5   # Start at 5 seconds
    max_delay = 300    # Cap at 5 minutes

    while True:
        try:
            self.connect()
            retry_delay = 5  # Reset on successful connection
            self.loop()
        except Exception as e:
            print(f"[!] Error: {e}")
        finally:
            if self.sock:
                self.sock.close()

        print(f"[*] Reconnecting in {retry_delay}s...")
        time.sleep(retry_delay)
        retry_delay = min(retry_delay * 2, max_delay)

You should also handle the case where your bot's nickname is already in use. The IRC server will send numeric reply 433 (ERR_NICKNAMEINUSE). Our bot already handles this by appending an underscore, but you could also implement GHOST via NickServ if your nick is registered.

Deploying Your Bot on a VPS

// Keep it running 24/7

Running the bot on your local machine works for testing, but for a permanent presence on TwistedNET, you will want to deploy it on a VPS (Virtual Private Server). A small VPS with 512MB of RAM is more than enough. Here is how to set it up as a systemd service on a Linux server:

# Create a systemd service file
sudo nano /etc/systemd/system/ircbot.service

Add the following content:

[Unit]
Description=TwistedNET IRC Bot
After=network.target

[Service]
Type=simple
User=botuser
WorkingDirectory=/home/botuser/ircbot
ExecStart=/usr/bin/python3 /home/botuser/ircbot/bot.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Then enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable ircbot
sudo systemctl start ircbot

# Check status
sudo systemctl status ircbot

# View logs
journalctl -u ircbot -f

Bot Etiquette on TwistedNET

// Be a good citizen

Running a bot on an IRC network comes with responsibility. TwistedNET welcomes well-behaved bots, but there are some guidelines you should follow to be a good network citizen:

>

Rate limit your output. Never flood a channel with rapid messages. Add a delay between messages if your bot sends multiple lines. A good rule is no more than 3 messages per 5 seconds.

>

Ask permission from channel ops. Before putting your bot in a channel, ask the channel operators if it is welcome. Not every channel wants bot activity.

>

Identify your bot clearly. Set a descriptive realname and make it obvious that your client is a bot, not a human user. Some networks have a bot user mode you can set.

>

Handle errors silently. If an external API call fails, do not spam the channel with error tracebacks. Send a brief, user-friendly error message or stay silent.

>

Respect user privacy. Do not log private messages or store user data without consent. TwistedNET values privacy above everything, and your bot should too.

Start Building Today

You now have everything you need to build, customize, and deploy an IRC bot on TwistedNET. Clone the code, modify the commands, add your own features, and bring your bot to life. The community is ready to see what you create.

Need help along the way? Join #twisted and ask. Our operators and community members are always happy to help fellow developers.

CONNECT TO TWISTEDNET

irc.twistednet.org:6697 (SSL)