658 lines
28 KiB
Python
658 lines
28 KiB
Python
import socket
|
||
import ssl
|
||
import base64
|
||
import os
|
||
import threading
|
||
import importlib
|
||
import requests
|
||
from requests.adapters import HTTPAdapter
|
||
from requests.packages.urllib3.util.retry import Retry
|
||
import queue
|
||
import time
|
||
from plugins import news
|
||
from plugins import radio
|
||
from plugins.database import store_message
|
||
import traceback
|
||
from plugins.database import init_db as init_db_messages
|
||
from plugins.auth import init_db as init_db_auth
|
||
|
||
# Inicializar ambas bases de datos
|
||
init_db_messages() # Base de datos de mensajes
|
||
init_db_auth()
|
||
|
||
ADMIN_PASSWORD = os.getenv("IRC_ADMIN_PASSWORD")
|
||
password = os.getenv("IRC_BOT_PASSWORD")
|
||
if not password:
|
||
raise ValueError("Error: La variable de entorno IRC_BOT_PASSWORD no está definida.")
|
||
|
||
class IRCBot:
|
||
def __init__(self, server, port, nickname, username, password, channels,
|
||
command_prefix="!", control_socket="/tmp/ircbot.sock"):
|
||
self.server = server
|
||
self.port = port
|
||
self.nickname = nickname
|
||
self.username = username
|
||
self.password = password
|
||
self.channels = channels # Lista de canales
|
||
self.command_prefix = command_prefix
|
||
self.control_socket = control_socket
|
||
self.authenticated = False
|
||
self.joined = False
|
||
self.plugins = {}
|
||
self.admins = set() # Lista de administradores autenticados
|
||
self.msg_queue = queue.Queue()
|
||
self.running = True
|
||
|
||
# Inicia el hilo que envía los mensajes en cola (para throttling).
|
||
threading.Thread(target=self._message_worker, daemon=True).start()
|
||
|
||
# Carga plugins
|
||
self.load_plugins()
|
||
|
||
# Monitoreo en un hilo separado (ejemplo: radio, news)
|
||
threading.Thread(target=radio.monitor_liquidsoap, daemon=True).start()
|
||
news_plugin = news.NewsPlugin() # Instancia de la clase
|
||
threading.Thread(target=news_plugin.announce_news, args=(self.send_raw, self), daemon=True).start()
|
||
|
||
# Conexión TLS
|
||
context = ssl.create_default_context()
|
||
self.sock = socket.create_connection((self.server, self.port))
|
||
self.sock = context.wrap_socket(self.sock, server_hostname=self.server)
|
||
|
||
print(f" Conectado a {self.server}:{self.port}")
|
||
|
||
# SASL
|
||
self.send_raw("CAP REQ :sasl")
|
||
self.send_raw(f"NICK {self.nickname}")
|
||
self.send_raw(f"USER {self.username} 0 * :{self.nickname}")
|
||
|
||
# Iniciar socket UNIX (control externo)
|
||
threading.Thread(target=self.control_socket_server, daemon=True).start()
|
||
|
||
# Iniciar escucha del servidor IRC
|
||
self.listen()
|
||
|
||
def load_plugins(self, plugin_dir="plugins"):
|
||
"""Carga o recarga dinámicamente los plugins desde el directorio especificado."""
|
||
if not os.path.exists(plugin_dir):
|
||
os.makedirs(plugin_dir)
|
||
|
||
self.plugins = {} # Reinicia la lista de plugins
|
||
|
||
for file in os.listdir(plugin_dir):
|
||
if file.endswith(".py") and file != "__init__.py":
|
||
name = file[:-3] # Nombre sin `.py`
|
||
try:
|
||
module = importlib.import_module(f"{plugin_dir}.{name}")
|
||
|
||
# Plugins especiales con lógica personalizada
|
||
if name == "grab" and hasattr(module, "GrabPlugin"):
|
||
self.plugins["grab"] = module.GrabPlugin()
|
||
print(f" Plugin especial '{name}' instanciado.")
|
||
continue
|
||
|
||
if name == "alias" and hasattr(module, "AliasPlugin"):
|
||
alias_plugin = module.AliasPlugin(self.send_raw, self)
|
||
self.plugins["alias"] = alias_plugin
|
||
|
||
# Registrar alias como comandos
|
||
dynamic_cmds = alias_plugin.get_dynamic_commands()
|
||
for alias_name, alias_function in dynamic_cmds.items():
|
||
self.plugins[alias_name] = alias_function
|
||
print(f" Alias dinámico registrado: {alias_name}")
|
||
continue
|
||
|
||
if name == "uptime" and hasattr(module, "UptimePlugin"):
|
||
self.plugins["uptime"] = module.UptimePlugin(self)
|
||
print(f" Plugin especial '{name}' instanciado con `bot`.")
|
||
continue
|
||
|
||
if name == "give" and hasattr(module, "GivePlugin"):
|
||
self.plugins["give"] = module.GivePlugin(self.plugins)
|
||
print(f" Plugin especial '{name}' instanciado con referencia a otros plugins.")
|
||
continue
|
||
|
||
# Instanciar clases convencionales como XyzPlugin
|
||
class_name = f"{name.capitalize()}Plugin"
|
||
if hasattr(module, class_name):
|
||
self.plugins[name] = getattr(module, class_name)()
|
||
print(f" Plugin '{name}' instanciado como clase ({class_name}).")
|
||
continue
|
||
|
||
# Si tiene run() directamente, cargar como módulo plano
|
||
if hasattr(module, "run"):
|
||
self.plugins[name] = module
|
||
print(f" Plugin '{name}' cargado como módulo.")
|
||
continue
|
||
|
||
print(f" ⚠️ El plugin '{name}' no tiene 'run()' ni clase '{class_name}', se omitirá.")
|
||
|
||
except Exception as e:
|
||
error_trace = traceback.format_exc()
|
||
print(f" ❌ Error al cargar el plugin '{name}': {e}\n{error_trace}")
|
||
|
||
print(" ✅ Todos los plugins han sido cargados correctamente.")
|
||
|
||
|
||
|
||
def send_raw(self, message):
|
||
"""Envía un mensaje al servidor IRC inmediatamente (para conexiones, etc.)."""
|
||
try:
|
||
if self.sock and not self.sock._closed:
|
||
print(f">> {message}")
|
||
self.sock.send((message + "\r\n").encode("utf-8"))
|
||
else:
|
||
print("️ Socket no disponible, intentando reconectar...")
|
||
self.connect()
|
||
except Exception as e:
|
||
print(f" Error al enviar mensaje: {e}")
|
||
self.handle_reconnect()
|
||
|
||
def send_queued_message(self, target, message):
|
||
"""Coloca un mensaje en cola para enviarlo con retraso (throttling)."""
|
||
max_length = 400 # Máx. caracteres para evitar flood
|
||
if len(message) > max_length:
|
||
parts = [message[i:i + max_length] for i in range(0, len(message), max_length)]
|
||
for part in parts:
|
||
self.msg_queue.put(f"PRIVMSG {target} :{part}")
|
||
else:
|
||
self.msg_queue.put(f"PRIVMSG {target} :{message}")
|
||
|
||
def _message_worker(self):
|
||
"""Hilo que gestiona la salida de mensajes con throttling."""
|
||
while self.running:
|
||
try:
|
||
message = self.msg_queue.get()
|
||
self.send_raw(message)
|
||
time.sleep(1) # Ajusta el delay si el servidor lo requiere
|
||
except Exception as e:
|
||
print(f" Error al enviar mensaje de la cola: {e}")
|
||
|
||
def join_channels(self):
|
||
"""Unirse a todos los canales configurados."""
|
||
for channel in self.channels:
|
||
self.send_raw(f"JOIN {channel}")
|
||
print(f" Unido a {channel}")
|
||
self.joined = True
|
||
|
||
def listen(self):
|
||
"""Escucha mensajes del servidor y maneja comandos."""
|
||
print(" Escuchando mensajes del servidor...")
|
||
|
||
while True:
|
||
try:
|
||
data = self.sock.recv(4096).decode("utf-8", errors="ignore").strip()
|
||
if not data:
|
||
print(" El servidor cerró la conexión.")
|
||
break
|
||
|
||
for line in data.split("\r\n"):
|
||
if not line:
|
||
continue
|
||
|
||
print(f"<< {line}") # Debug
|
||
|
||
if line.startswith("PING"):
|
||
self.send_raw(f"PONG {line.split()[1]}")
|
||
|
||
if "CAP" in line and "ACK :sasl" in line:
|
||
self.send_raw("AUTHENTICATE PLAIN")
|
||
|
||
if "AUTHENTICATE +" in line:
|
||
auth_string = base64.b64encode(
|
||
f"{self.username}\0{self.username}\0{self.password}".encode()
|
||
).decode()
|
||
self.send_raw(f"AUTHENTICATE {auth_string}")
|
||
|
||
if " 900 " in line or " 903 " in line:
|
||
print(" SASL autenticado correctamente.")
|
||
self.authenticated = True
|
||
self.send_raw("CAP END")
|
||
|
||
if ((" 376 " in line) or (" 422 " in line)) and self.authenticated and not self.joined:
|
||
self.join_channels()
|
||
|
||
# Mensajes PRIVMSG
|
||
if "PRIVMSG" in line:
|
||
self.handle_message(line)
|
||
|
||
except Exception as e:
|
||
print(f" Error en la conexión: {e}")
|
||
break
|
||
|
||
def handle_message(self, raw_line):
|
||
"""
|
||
Procesa los PRIVMSG del IRC y maneja autenticación para plugins restringidos.
|
||
"""
|
||
parts = raw_line.split()
|
||
if len(parts) < 4:
|
||
return
|
||
|
||
sender_info = parts[0]
|
||
command_type = parts[1]
|
||
target = parts[2]
|
||
msg_raw = " ".join(parts[3:])[1:] # Quitar el ':' inicial
|
||
sender_nick = sender_info.split("!")[0][1:]
|
||
|
||
# **Verificar si el mensaje es un privado (PM)**
|
||
if target.lower() == self.nickname.lower():
|
||
print(f"DEBUG: Recibido PM de {sender_nick}: {msg_raw}") # Log de depuración
|
||
self.handle_private_message(sender_nick, msg_raw)
|
||
return
|
||
|
||
# ️ Evitar almacenar comandos sensibles como `.auth login usuario contraseña`
|
||
SENSITIVE_COMMANDS = ["auth", "login", "register"]
|
||
if msg_raw.startswith(self.command_prefix):
|
||
command_line = msg_raw[len(self.command_prefix):].strip()
|
||
tokens = command_line.split()
|
||
if tokens and tokens[0] in SENSITIVE_COMMANDS:
|
||
print(f"️ Mensaje de {sender_nick} omitido (comando sensible): {msg_raw}")
|
||
else:
|
||
store_message(sender_nick, target, msg_raw) # Guardar en SQLite si NO es un comando sensible
|
||
else:
|
||
store_message(sender_nick, target, msg_raw) # Guardar en SQLite
|
||
|
||
# **Guardar última frase del usuario para `.grab`**
|
||
if "grab" in self.plugins:
|
||
try:
|
||
self.plugins["grab"].store_last_message(sender_nick, msg_raw)
|
||
except Exception as e:
|
||
print(f"️ Error al almacenar mensaje en grab: {e}")
|
||
|
||
# **Procesar comandos si empiezan con el prefijo**
|
||
if command_type == "PRIVMSG" and msg_raw.startswith(self.command_prefix):
|
||
command_line = msg_raw[len(self.command_prefix):].strip()
|
||
tokens = command_line.split()
|
||
if not tokens:
|
||
return
|
||
|
||
cmd = tokens[0] # Comando (ej: "give", "alias", "grab", "ping")
|
||
args = tokens[1:]
|
||
|
||
# **Verificar si el usuario es admin y saltar autenticación**
|
||
if sender_nick in self.admins:
|
||
print(f" {sender_nick} es admin, ejecutando '.{cmd}' sin restricciones.")
|
||
else:
|
||
# **Verificar si el comando requiere autenticación**
|
||
restricted_plugins = ["grab"]
|
||
if cmd in restricted_plugins:
|
||
if "auth" in self.plugins and not self.plugins["auth"].is_authenticated(sender_nick):
|
||
self.send_queued_message(target, f" {sender_nick}, debes autenticarte para usar '.{cmd}'")
|
||
return
|
||
|
||
# **Si el comando existe en `self.plugins`, ejecutarlo**
|
||
if cmd in self.plugins:
|
||
plugin_or_func = self.plugins[cmd]
|
||
try:
|
||
if callable(plugin_or_func):
|
||
response = plugin_or_func(sender_nick, *args)
|
||
else:
|
||
response = plugin_or_func.run(sender_nick, *args)
|
||
|
||
if isinstance(response, str) and response:
|
||
for line in response.split("\n"):
|
||
self.send_queued_message(target, line)
|
||
|
||
except Exception as e:
|
||
error_trace = traceback.format_exc()
|
||
print(f" Error en '{cmd}': {e}\n{error_trace}")
|
||
self.send_queued_message(target, f"️ Error en '{cmd}': {str(e)}")
|
||
|
||
elif cmd == "help":
|
||
if args:
|
||
cmd_help = args[0]
|
||
if cmd_help in self.plugins and hasattr(self.plugins[cmd_help], "help"):
|
||
desc = self.plugins[cmd_help].help()
|
||
self.send_queued_message(target, desc)
|
||
else:
|
||
self.send_queued_message(target, f" No hay ayuda disponible para '.{cmd_help}'")
|
||
else:
|
||
# Listar comandos disponibles
|
||
disponibles = " ".join([f"`.{p}`" for p in self.plugins.keys()])
|
||
lines = [
|
||
" **Comandos disponibles:**",
|
||
disponibles,
|
||
" ℹ️ Usa .help <comando> para más detalles."
|
||
]
|
||
for l in lines:
|
||
self.send_queued_message(target, l)
|
||
|
||
else:
|
||
# Comando no encontrado
|
||
self.send_queued_message(target, f" No existe el comando '.{cmd}'")
|
||
|
||
|
||
def handle_private_message(self, sender, msg):
|
||
"""Maneja mensajes privados, autenticación de administradores y comandos de plugins."""
|
||
|
||
print(f" Mensaje privado recibido de {sender}: {msg}") # DEBUG
|
||
|
||
# **Manejar autenticación de administradores con `/msg bot op password`**
|
||
if msg.startswith("op "):
|
||
password_provided = msg.split(" ", 1)[1]
|
||
|
||
print(f" Intento de autenticación de {sender} con contraseña: {password_provided}") # DEBUG
|
||
|
||
if password_provided == ADMIN_PASSWORD:
|
||
self.admins.add(sender)
|
||
self.send_raw(f"PRIVMSG {sender} : Has sido autenticado como administrador.")
|
||
print(f" {sender} ahora es administrador.") # DEBUG
|
||
else:
|
||
self.send_raw(f"PRIVMSG {sender} : Contraseña incorrecta.")
|
||
print(f" {sender} intentó autenticarse con una contraseña incorrecta.") # DEBUG
|
||
return
|
||
|
||
# **Si el mensaje comienza con el prefijo de comandos, procesarlo**
|
||
if msg.startswith(self.command_prefix):
|
||
command_line = msg[len(self.command_prefix):].strip()
|
||
tokens = command_line.split()
|
||
if not tokens:
|
||
return
|
||
|
||
cmd = tokens[0] # Extraer el comando (`auth`, `grab`, etc.)
|
||
args = tokens[1:] # Extraer los argumentos
|
||
|
||
if cmd in self.plugins:
|
||
try:
|
||
print(f" Ejecutando {cmd} en PM con args={args}") # Debug
|
||
response = self.plugins[cmd].run(sender, *args) # FIX: Solo un `sender`
|
||
|
||
if response:
|
||
self.send_raw(f"PRIVMSG {sender} :{response}")
|
||
return
|
||
except Exception as e:
|
||
self.send_raw(f"PRIVMSG {sender} : Error en '{cmd}': {str(e)}")
|
||
print(f" Error en '{cmd}': {e}")
|
||
return
|
||
|
||
# **Si el usuario es administrador, manejar comandos especiales**
|
||
if sender in self.admins:
|
||
parts = msg.split()
|
||
if len(parts) == 0:
|
||
return # Si el mensaje está vacío, salir
|
||
|
||
command = parts[0] # Extraer el comando principal
|
||
print(f"️ {sender} ejecutando comando admin: {command}")
|
||
|
||
if command == "status":
|
||
self.send_raw(f"PRIVMSG {sender} :Bot activo | Plugins cargados: {len(self.plugins)}")
|
||
|
||
elif command == "join" and len(parts) > 1:
|
||
self.send_raw(f"JOIN {parts[1]}")
|
||
self.send_raw(f"PRIVMSG {sender} :Unido a {parts[1]}")
|
||
|
||
elif command == "part" and len(parts) > 1:
|
||
self.send_raw(f"PART {parts[1]}")
|
||
self.send_raw(f"PRIVMSG {sender} :Salió de {parts[1]}")
|
||
|
||
elif command == "list_plugins":
|
||
plist = ", ".join(self.plugins.keys()) if self.plugins else "No hay plugins cargados."
|
||
self.send_raw(f"PRIVMSG {sender} :Plugins cargados: {plist}")
|
||
|
||
elif command == "load" and len(parts) > 1:
|
||
plugin_name = parts[1]
|
||
try:
|
||
module = importlib.import_module(f"plugins.{plugin_name}")
|
||
self.plugins[plugin_name] = module
|
||
self.send_raw(f"PRIVMSG {sender} :Plugin {plugin_name} cargado.")
|
||
print(f" {sender} cargó el plugin {plugin_name}")
|
||
except Exception as e:
|
||
self.send_raw(f"PRIVMSG {sender} :Error al cargar plugin {plugin_name}: {e}")
|
||
|
||
elif command == "unload" and len(parts) > 1:
|
||
plugin_name = parts[1]
|
||
if plugin_name in self.plugins:
|
||
del self.plugins[plugin_name]
|
||
self.send_raw(f"PRIVMSG {sender} :Plugin {plugin_name} descargado.")
|
||
print(f" {sender} descargó el plugin {plugin_name}")
|
||
else:
|
||
self.send_raw(f"PRIVMSG {sender} :Plugin {plugin_name} no encontrado.")
|
||
|
||
elif command == "reload_plugins":
|
||
self.load_plugins()
|
||
self.send_raw(f"PRIVMSG {sender} :Plugins recargados correctamente.")
|
||
|
||
elif command == "quit":
|
||
self.send_raw("QUIT :Apagando bot...")
|
||
self.send_raw(f"PRIVMSG {sender} :Bot desconectado.")
|
||
os._exit(0)
|
||
|
||
elif command == "restart":
|
||
self.send_raw("QUIT :Reiniciando bot...")
|
||
self.send_raw(f"PRIVMSG {sender} :Bot reiniciándose.")
|
||
os.execv(__file__, [])
|
||
|
||
elif command == "reconnect":
|
||
self.send_raw("QUIT :Reconectando...")
|
||
self.send_raw(f"PRIVMSG {sender} :Reconectando al servidor IRC.")
|
||
self.sock.close()
|
||
self.__init__(self.server, self.port, self.nickname, self.username, self.password, self.channels)
|
||
|
||
elif command == "nick" and len(parts) > 1:
|
||
self.send_raw(f"NICK {parts[1]}")
|
||
self.send_raw(f"PRIVMSG {sender} :🆔 Nick cambiado a {parts[1]}")
|
||
|
||
elif command == "msg" and len(parts) > 2:
|
||
channel = parts[1]
|
||
message = " ".join(parts[2:])
|
||
self.send_raw(f"PRIVMSG {channel} :{message}")
|
||
self.send_raw(f"PRIVMSG {sender} :Mensaje enviado a {channel}")
|
||
|
||
elif command == "raw" and len(parts) > 1:
|
||
raw_cmd = " ".join(parts[1:])
|
||
self.send_raw(raw_cmd)
|
||
self.send_raw(f"PRIVMSG {sender} :Comando IRC enviado: {raw_cmd}")
|
||
|
||
elif command == "kick" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"KICK {channel} {user} :Expulsado por {sender}")
|
||
self.send_raw(f"PRIVMSG {sender} :{user} ha sido expulsado de {channel}")
|
||
|
||
elif command == "ban" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} +b {user}")
|
||
self.send_raw(f"PRIVMSG {sender} :{user} ha sido baneado en {channel}")
|
||
|
||
elif command == "unban" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} -b {user}")
|
||
self.send_raw(f"PRIVMSG {sender} :{user} ha sido desbaneado en {channel}")
|
||
|
||
elif command == "op" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} +o {user}")
|
||
self.send_raw(f"PRIVMSG {sender} :{user} ahora tiene OP en {channel}")
|
||
|
||
elif command == "deop" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} -o {user}")
|
||
self.send_raw(f"PRIVMSG {sender} :{user} ya no tiene OP en {channel}")
|
||
|
||
elif command == "help":
|
||
comandos = [
|
||
" **Comandos disponibles:**",
|
||
"- `status` → Estado del bot y plugins",
|
||
"- `join <#canal>` → Unirse a un canal",
|
||
"- `part <#canal>` → Salir de un canal",
|
||
"- `list_plugins` → Listar plugins activos",
|
||
"- `load <plugin>` → Cargar un plugin",
|
||
"- `unload <plugin>` → Descargar un plugin",
|
||
"- `reload_plugins` → Recargar todos los plugins",
|
||
"- `quit` → Apagar el bot",
|
||
"- `restart` → Reiniciar el bot",
|
||
"- `reconnect` → Reconectar al servidor IRC",
|
||
"- `nick <nuevo_nick>` → Cambiar el apodo del bot",
|
||
"- `msg <#canal> <mensaje>` → Enviar mensaje a un canal",
|
||
"- `raw <comando>` → Enviar un comando IRC crudo",
|
||
"- `kick <#canal> <usuario>` → Expulsar usuario",
|
||
"- `ban <#canal> <usuario>` → Banear usuario",
|
||
"- `unban <#canal> <usuario>` → Desbanear usuario",
|
||
"- `op <#canal> <usuario>` → Dar OP a un usuario",
|
||
"- `deop <#canal> <usuario>` → Quitar OP a un usuario",
|
||
"- `logout` → Cerrar sesión de admin",
|
||
"🆘 Usa `.help <comando>` para obtener ayuda detallada.",
|
||
]
|
||
for line in comandos:
|
||
self.send_queued_message(sender, line)
|
||
|
||
elif command == "logout":
|
||
self.admins.discard(sender)
|
||
self.send_raw(f"PRIVMSG {sender} :Has cerrado sesión como administrador.")
|
||
print(f" {sender} ha cerrado sesión.")
|
||
else:
|
||
self.send_raw(f"PRIVMSG {sender} :Comando no reconocido. Usa 'help' para ver opciones.")
|
||
else:
|
||
self.send_raw(f"PRIVMSG {sender} :No tienes permisos. Usa 'op <contraseña>' para autenticarte.")
|
||
|
||
def control_socket_server(self):
|
||
"""Crea un socket UNIX para recibir comandos desde consola o socat."""
|
||
if os.path.exists(self.control_socket):
|
||
os.remove(self.control_socket)
|
||
|
||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||
server.bind(self.control_socket)
|
||
server.listen(1)
|
||
print(f" Control socket activo en {self.control_socket}")
|
||
|
||
while True:
|
||
conn, _ = server.accept()
|
||
with conn:
|
||
data = conn.recv(1024).decode().strip()
|
||
if not data:
|
||
continue
|
||
|
||
print(f" Comando recibido desde consola: {data}")
|
||
parts = data.split()
|
||
if not parts:
|
||
continue
|
||
|
||
cmd = parts[0]
|
||
|
||
if cmd == "join" and len(parts) > 1:
|
||
self.send_raw(f"JOIN {parts[1]}")
|
||
print(f" Unido a {parts[1]}")
|
||
|
||
elif cmd == "part" and len(parts) > 1:
|
||
self.send_raw(f"PART {parts[1]}")
|
||
print(f" Salió de {parts[1]}")
|
||
|
||
elif cmd == "list_channels":
|
||
ch_list = ", ".join(self.channels)
|
||
conn.sendall(f"Canales actuales: {ch_list}\n".encode())
|
||
|
||
elif cmd == "load" and len(parts) > 1:
|
||
plug = parts[1]
|
||
try:
|
||
module = importlib.import_module(f"plugins.{plug}")
|
||
self.plugins[plug] = module
|
||
print(f" Plugin {plug} cargado")
|
||
conn.sendall(f"Plugin {plug} cargado\n".encode())
|
||
except Exception as e:
|
||
print(f" Error al cargar plugin {plug}: {e}")
|
||
conn.sendall(f"Error al cargar plugin {plug}: {e}\n".encode())
|
||
|
||
elif cmd == "unload" and len(parts) > 1:
|
||
plug = parts[1]
|
||
if plug in self.plugins:
|
||
del self.plugins[plug]
|
||
print(f"️ Plugin {plug} descargado")
|
||
conn.sendall(f"Plugin {plug} descargado\n".encode())
|
||
else:
|
||
conn.sendall(f"Plugin {plug} no encontrado\n".encode())
|
||
|
||
elif cmd == "list_plugins":
|
||
plist = ", ".join(self.plugins.keys()) if self.plugins else "No hay plugins cargados."
|
||
conn.sendall(f"Plugins cargados: {plist}\n".encode())
|
||
|
||
elif cmd == "quit":
|
||
self.send_raw("QUIT :Apagando bot...")
|
||
conn.sendall(" Bot desconectado.\n".encode())
|
||
os._exit(0)
|
||
|
||
elif cmd == "msg" and len(parts) > 2:
|
||
channel = parts[1]
|
||
message = " ".join(parts[2:])
|
||
self.send_raw(f"PRIVMSG {channel} :{message}")
|
||
conn.sendall(f" Mensaje enviado a {channel}\n".encode())
|
||
|
||
elif cmd == "raw" and len(parts) > 1:
|
||
raw_command = " ".join(parts[1:])
|
||
self.send_raw(raw_command)
|
||
conn.sendall(f"️ Comando IRC enviado: {raw_command}\n".encode())
|
||
|
||
elif cmd == "kick" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"KICK {channel} {user} :Expulsado")
|
||
conn.sendall(f" {user} ha sido expulsado de {channel}\n".encode())
|
||
|
||
elif cmd == "ban" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} +b {user}")
|
||
conn.sendall(f" {user} ha sido baneado de {channel}\n".encode())
|
||
|
||
elif cmd == "unban" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} -b {user}")
|
||
conn.sendall(f" {user} ha sido desbaneado de {channel}\n".encode())
|
||
|
||
elif cmd == "op" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} +o {user}")
|
||
conn.sendall(f"⭐ {user} ahora tiene OP en {channel}\n".encode())
|
||
|
||
elif cmd == "deop" and len(parts) > 2:
|
||
channel = parts[1]
|
||
user = parts[2]
|
||
self.send_raw(f"MODE {channel} -o {user}")
|
||
conn.sendall(f"️ {user} ya no tiene OP en {channel}\n".encode())
|
||
else:
|
||
# Envía el texto tal cual
|
||
self.send_raw(data)
|
||
|
||
def handle_reconnect(self):
|
||
"""Reintenta la conexión al servidor IRC tras una desconexión."""
|
||
print(" 🔁 Intentando reconectar...")
|
||
|
||
try:
|
||
context = ssl.create_default_context()
|
||
self.sock = socket.create_connection((self.server, self.port))
|
||
self.sock = context.wrap_socket(self.sock, server_hostname=self.server)
|
||
|
||
# Reautenticación y rejoin
|
||
self.authenticated = False
|
||
self.joined = False
|
||
|
||
self.send_raw("CAP REQ :sasl")
|
||
self.send_raw(f"NICK {self.nickname}")
|
||
self.send_raw(f"USER {self.username} 0 * :{self.nickname}")
|
||
|
||
print(" ✅ Reconexión iniciada correctamente.")
|
||
except Exception as e:
|
||
print(f" ❌ Error durante la reconexión: {e}")
|
||
time.sleep(5)
|
||
self.handle_reconnect() # Reintento recursivo (con cuidado)
|
||
|
||
|
||
|
||
|
||
if __name__ == "__main__":
|
||
bot = IRCBot(
|
||
server="irc.libera.chat",
|
||
port=6697,
|
||
nickname="terryflaps",
|
||
username="terryflaps",
|
||
password=password,
|
||
channels=["#testtest", "#terryflaps"],
|
||
command_prefix="."
|
||
)
|
||
|