Mi primer bot IRC lo escribí en Tcl para Eggdrop en 1999. Funcionaba, pero el lenguaje me daba dolor de cabeza cada vez que necesitaba depurar algo. Cuando descubrí Python por el 2004, lo primero que hice fue reescribir ese bot. Tardé una tarde. Con Tcl había tardado dos semanas. Desde entonces, Python es mi elección para cualquier bot IRC que necesite escribir rápido y mantener sin querer arrancarme el pelo.
Por qué Python para bots IRC
Python tiene tres ventajas claras para bots IRC. Primera: la librería estándar incluye todo lo que necesitas para un bot básico. Sockets, SSL, parsing de texto — todo viene de serie. No necesitas instalar nada para empezar.
Segunda: la legibilidad. Un bot IRC es esencialmente un parser de texto que responde a patrones. Python maneja strings de forma natural y elegante. Expresiones regulares, split, strip, format strings — todo es limpio y predecible.
Tercera: el ecosistema. Si necesitas algo más avanzado — bases de datos, APIs HTTP, machine learning para moderación — Python tiene librerías para todo. Tu bot puede empezar simple y crecer sin cambiar de lenguaje.
Vamos a construir un bot IRC en tres versiones, de menos a más sofisticado. Cada versión resuelve un problema real.
Versión 1: socket raw, sin dependencias
Empezamos con un bot que usa solo la librería estándar de Python. Cero dependencias externas. Esto es útil para entender el protocolo IRC a nivel de cable y para situaciones donde no puedes instalar paquetes.
import socket
import ssl
import time
class BotIRC:
def __init__(self, server, port, nick, canales, use_tls=True):
self.server = server
self.port = port
self.nick = nick
self.canales = canales
self.sock = None
self.use_tls = use_tls
def conectar(self):
raw = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.use_tls:
ctx = ssl.create_default_context()
self.sock = ctx.wrap_socket(raw, server_hostname=self.server)
else:
self.sock = raw
self.sock.connect((self.server, self.port))
self.enviar(f"NICK {self.nick}")
self.enviar(f"USER {self.nick} 0 * :Bot IRC Python")
def enviar(self, mensaje):
self.sock.send(f"{mensaje}\r\n".encode("utf-8"))
print(f">> {mensaje}")
def unirse_canales(self):
for canal in self.canales:
self.enviar(f"JOIN {canal}")
time.sleep(1)
def procesar(self, linea):
# Responder PING para no ser desconectado
if linea.startswith("PING"):
self.enviar(f"PONG {linea.split()[1]}")
return
# Detectar fin de MOTD para unirse a canales
if " 376 " in linea or " 422 " in linea:
self.unirse_canales()
return
# Parsear mensajes PRIVMSG
if "PRIVMSG" in linea:
partes = linea.split(" PRIVMSG ")
if len(partes) == 2:
origen = partes[0].split("!")[0][1:]
destino, texto = partes[1].split(" :", 1)
self.on_mensaje(origen, destino, texto)
def on_mensaje(self, nick, canal, texto):
if texto.strip() == "!hola":
self.enviar(f"PRIVMSG {canal} :Hola {nick}, aquí andamos.")
elif texto.strip() == "!hora":
ahora = time.strftime("%H:%M:%S")
self.enviar(f"PRIVMSG {canal} :Son las {ahora} UTC.")
def ejecutar(self):
self.conectar()
buffer = ""
while True:
datos = self.sock.recv(4096).decode("utf-8", errors="replace")
if not datos:
break
buffer += datos
while "\r\n" in buffer:
linea, buffer = buffer.split("\r\n", 1)
print(f"<< {linea}")
self.procesar(linea)
if __name__ == "__main__":
bot = BotIRC(
server="irc.libera.chat",
port=6697,
nick="MiPythonBot",
canales=["#test-bot"],
use_tls=True
)
bot.ejecutar()
Son unas 60 líneas. Hace lo esencial: conecta, responde PING, se une a canales, y responde comandos. El buffer manual con \r\n es necesario porque TCP no garantiza que recibas líneas completas. Puedes recibir media línea o tres líneas juntas. Eso es algo que muchos tutoriales ignoran y luego el bot se rompe aleatoriamente.
Versión 2: con la librería irc
El socket raw está bien para aprender, pero para un bot de producción quieres algo que maneje reconexión, parsing del protocolo, y los mil casos especiales del IRC. La librería irc de Python hace exactamente eso.
# Instalar: pip install irc
import irc.bot
import irc.strings
class MiBot(irc.bot.SingleServerIRCBot):
def __init__(self, server, port, nick, canales, password=None):
factory = irc.connection.Factory(wrapper=irc.connection.ssl.SSLFactory())
irc.bot.SingleServerIRCBot.__init__(
self,
[(server, port, password)],
nick,
nick,
connect_factory=factory
)
self.canales_destino = canales
def on_nicknameinuse(self, conexion, evento):
nuevo = conexion.get_nickname() + "_"
conexion.nick(nuevo)
def on_welcome(self, conexion, evento):
for canal in self.canales_destino:
conexion.join(canal)
print(f"Unido a {canal}")
def on_pubmsg(self, conexion, evento):
texto = evento.arguments[0]
nick = evento.source.nick
canal = evento.target
if texto.startswith("!"):
cmd = texto.split()[0][1:]
args = texto.split()[1:] if len(texto.split()) > 1 else []
self.manejar_comando(conexion, canal, nick, cmd, args)
def manejar_comando(self, con, canal, nick, cmd, args):
comandos = {
"hola": lambda: f"Hola {nick}, todo en orden.",
"ping": lambda: "pong",
"info": lambda: "Bot IRC en Python | Versión 2.0",
"ayuda": lambda: "Comandos: !hola, !ping, !info, !ayuda"
}
respuesta = comandos.get(cmd, lambda: None)()
if respuesta:
con.privmsg(canal, respuesta)
if __name__ == "__main__":
bot = MiBot(
server="irc.libera.chat",
port=6697,
nick="MiPythonBot",
canales=["#test-bot"]
)
bot.start()
La librería irc se encarga del parsing de mensajes, la gestión de la conexión, reconexión automática, y un sistema de eventos limpio. Tú solo defines métodos como on_pubmsg, on_welcome, on_join, y la librería los llama cuando ocurre el evento correspondiente.
Personalmente prefiero esta versión para bots que necesitan estar en producción. El parsing manual de la versión 1 funciona, pero hay decenas de casos especiales en el protocolo IRC que la librería ya maneja por ti.
Añadir comandos personalizados
Un bot sin comandos útiles es solo ruido en un canal. Vamos a crear un sistema de comandos extensible que puedas ampliar sin tocar el núcleo del bot.
import json
import urllib.request
import time
class SistemaComandos:
def __init__(self):
self.comandos = {}
self.cooldowns = {}
def registrar(self, nombre, funcion, cooldown=5):
self.comandos[nombre] = {"fn": funcion, "cooldown": cooldown}
def ejecutar(self, nombre, nick, canal, args):
if nombre not in self.comandos:
return None
# Control de cooldown por usuario
clave = f"{nick}:{nombre}"
ahora = time.time()
cd = self.comandos[nombre]["cooldown"]
if clave in self.cooldowns and ahora - self.cooldowns[clave] < cd:
return None # Silencio si está en cooldown
self.cooldowns[clave] = ahora
return self.comandos[nombre]["fn"](nick, canal, args)
# Comandos de ejemplo
def cmd_dado(nick, canal, args):
import random
caras = int(args[0]) if args and args[0].isdigit() else 6
resultado = random.randint(1, caras)
return f"{nick} lanza un d{caras} y saca {resultado}"
def cmd_clima(nick, canal, args):
if not args:
return "Uso: !clima <ciudad>"
ciudad = " ".join(args)
try:
url = f"https://wttr.in/{ciudad}?format=%t+%C&lang=es"
req = urllib.request.Request(url, headers={"User-Agent": "BotIRC/1.0"})
resp = urllib.request.urlopen(req, timeout=5)
return f"Clima en {ciudad}: {resp.read().decode().strip()}"
except Exception:
return f"No pude obtener el clima de {ciudad}"
def cmd_uptime(nick, canal, args, inicio=time.time()):
segundos = int(time.time() - inicio)
horas = segundos // 3600
minutos = (segundos % 3600) // 60
return f"Llevo {horas}h {minutos}m conectado."
# Registro
sistema = SistemaComandos()
sistema.registrar("dado", cmd_dado, cooldown=3)
sistema.registrar("clima", cmd_clima, cooldown=10)
sistema.registrar("uptime", cmd_uptime, cooldown=5)
El sistema de cooldown es fundamental. Sin él, un usuario puede spamear comandos y hacer que tu bot floodee el canal, lo que resulta en un kick o ban inmediato. Cinco segundos de cooldown por usuario y comando es un valor razonable para empezar.
Versión 3: asyncio para bots modernos
Si tu bot necesita hacer llamadas HTTP, consultar bases de datos, o manejar múltiples conexiones, el modelo síncrono se queda corto. Un bot síncrono que hace una petición HTTP lenta bloquea todo el procesamiento de mensajes IRC. Con asyncio, las operaciones de I/O no bloquean nada.
import asyncio
import ssl
class BotAsyncIRC:
def __init__(self, server, port, nick, canales):
self.server = server
self.port = port
self.nick = nick
self.canales = canales
self.reader = None
self.writer = None
async def conectar(self):
ctx = ssl.create_default_context()
self.reader, self.writer = await asyncio.open_connection(
self.server, self.port, ssl=ctx
)
await self.enviar(f"NICK {self.nick}")
await self.enviar(f"USER {self.nick} 0 * :Bot Async")
async def enviar(self, mensaje):
self.writer.write(f"{mensaje}\r\n".encode("utf-8"))
await self.writer.drain()
async def bucle(self):
await self.conectar()
buffer = ""
while True:
datos = await self.reader.read(4096)
if not datos:
break
buffer += datos.decode("utf-8", errors="replace")
while "\r\n" in buffer:
linea, buffer = buffer.split("\r\n", 1)
await self.procesar(linea)
async def procesar(self, linea):
if linea.startswith("PING"):
await self.enviar(f"PONG {linea.split()[1]}")
elif " 376 " in linea or " 422 " in linea:
for canal in self.canales:
await self.enviar(f"JOIN {canal}")
await asyncio.sleep(1)
elif "PRIVMSG" in linea:
# Procesar en tarea separada para no bloquear
asyncio.create_task(self.manejar_mensaje(linea))
async def manejar_mensaje(self, linea):
partes = linea.split(" PRIVMSG ")
if len(partes) != 2:
return
origen = partes[0].split("!")[0][1:]
destino, texto = partes[1].split(" :", 1)
if texto.strip() == "!lento":
# Simula operación lenta sin bloquear
await asyncio.sleep(3)
await self.enviar(f"PRIVMSG {destino} :Listo, {origen}.")
def ejecutar(self):
asyncio.run(self.bucle())
if __name__ == "__main__":
bot = BotAsyncIRC("irc.libera.chat", 6697, "AsyncBot", ["#test"])
bot.ejecutar()
La clave está en asyncio.create_task. Cada mensaje se procesa en su propia tarea asíncrona. Si un comando tarda 3 segundos en responder, el bot sigue procesando otros mensajes mientras tanto. En la versión síncrona, esos 3 segundos bloquearían todo.
He usado este patrón en bots que consultan APIs externas para dar información de servidores de juegos. La latencia de la API no afecta al procesamiento de comandos básicos del canal. Funciona bien incluso con 10-15 peticiones simultáneas.
Despliegue y supervisión
Escribir el bot es la mitad. La otra mitad es mantenerlo funcionando 24/7 sin babysitting manual.
# Archivo: /etc/systemd/system/ircbot.service
[Unit]
Description=Bot IRC Python
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=ircbot
Group=ircbot
WorkingDirectory=/opt/ircbot
ExecStart=/opt/ircbot/venv/bin/python bot.py
Restart=always
RestartSec=30
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
# Comandos de gestión
sudo systemctl enable ircbot # Iniciar al arranque
sudo systemctl start ircbot # Iniciar ahora
sudo systemctl status ircbot # Ver estado
journalctl -u ircbot -f # Ver logs en tiempo real
journalctl -u ircbot --since today # Logs de hoy
systemd se encarga de reiniciar el bot si se cae. El RestartSec=30 espera 30 segundos antes de reiniciar para no spamear reconexiones al servidor IRC. PYTHONUNBUFFERED=1 asegura que los prints aparezcan inmediatamente en el journal.
Para logging más serio, usa el módulo logging de Python con rotación de archivos. No dejes que los logs crezcan indefinidamente o acabarás con un disco lleno a las 3 de la mañana.
Errores comunes y cómo evitarlos
No manejar la codificación. IRC es un desastre de codificaciones. Puedes recibir UTF-8, Latin-1, CP1252 o algo indefinido en el mismo canal. Usa errors="replace" en el decode para no reventar con bytes raros. Tu bot no puede caerse porque alguien escribió un carácter especial.
Flood al servidor. Si envías mensajes demasiado rápido, el servidor te desconecta por excess flood. Implementa un rate limiter. Dos mensajes por segundo es un límite seguro para la mayoría de redes. Tres si tienes suerte.
No reconectar. La conexión IRC se va a caer. Es cuestión de cuándo, no de si. Tu bot necesita reconexión automática con backoff exponencial. Espera 30 segundos la primera vez, 60 la segunda, 120 la tercera, hasta un máximo de 300. Después de reconectar, resetea el contador.
Ignorar PING/PONG. Si no respondes al PING del servidor en ~120 segundos, te desconecta por ping timeout. Es el error más básico y más frecuente. Siempre, siempre, siempre responde PING.
Confiar en la entrada del usuario. Si tu bot ejecuta un comando basado en texto del canal sin sanitizar, alguien va a inyectar algo malicioso. Nunca uses eval() ni exec() con texto de IRC. Nunca pases texto de IRC directamente a una shell. Valida y sanitiza todo.
.env en .gitignore es lo mínimo.Preguntas frecuentes
¿Qué versión de Python necesito?
Python 3.7 o superior para la versión con asyncio. Las versiones 1 y 2 funcionan con Python 3.6+. No uses Python 2 — dejó de recibir soporte en 2020 y muchas librerías ya no lo soportan.
¿Mi bot necesita estar registrado en NickServ?
Depende de la red. Libera.chat requiere registro para unirse a la mayoría de canales. Otras redes no. Si registras el nick, autentifica con SASL en vez de enviar un PRIVMSG a NickServ — es más seguro porque se hace durante la conexión, antes de que seas visible en canales.
¿Puedo hacer un bot que esté en varias redes a la vez?
Sí. Con asyncio, creas una tarea por red. Cada tarea maneja su propia conexión. Con la librería irc, puedes usar MultiServerIRCBot. O simplemente ejecutas varias instancias del bot, una por red, como procesos separados. Esta última opción es más simple de mantener y depurar.