Escribi mi primer script de Eggdrop en 2000, un lunes por la noche, con el manual de Tcl abierto en una ventana de Netscape Navigator y el editor vi en otra. El script era terrible. Hacia tres cosas: saludaba a quien entraba, baneaba palabras prohibidas, y se caia cada vez que alguien ponia un caracter especial en su nick. Pero funcionaba lo suficiente como para que el canal dejara de ser un caos. Veinte anos despues, sigo escribiendo Tcl para Eggdrop. No porque sea el mejor lenguaje — sino porque para este caso concreto, nada lo ha reemplazado del todo.
Por que Tcl sigue siendo relevante
Tcl (Tool Command Language) es un lenguaje interpretado creado en 1988 por John Ousterhout. Fuera de Eggdrop, casi nadie lo usa para proyectos nuevos. Dentro de Eggdrop, es el unico lenguaje de scripting soportado nativamente. Puedes cargar modulos que anaden soporte para Python o Perl, pero el nucleo de Eggdrop habla Tcl y la inmensa mayoria de scripts existentes estan en Tcl.
Las ventajas de Tcl para bots IRC son reales. El manejo de strings es nativo y eficiente — IRC es un protocolo de texto puro. Los regex estan integrados sin importar paquetes. La sintaxis es minima: todo es un comando, los argumentos se separan por espacios, las llaves agrupan. Puedes aprender lo suficiente para escribir un script funcional en una tarde.
Las desventajas tambien son reales. No hay un gestor de paquetes. El tipado es debil y silencioso — un error de tipo no lanza una excepcion, produce un resultado inesperado. La documentacion esta dispersa entre el wiki de Eggdrop, el manual de Tcl, y tutoriales de hace quince anos que pueden o no seguir siendo correctos. Pero si aceptas estas limitaciones, Tcl hace bien su trabajo.
Estructura de un script Eggdrop
Un script de Eggdrop es un archivo .tcl que se carga al iniciar el bot. La estructura tipica:
# myscript.tcl — Descripcion breve
# Autor: tunick
# Version: 1.0
# Requiere: Eggdrop 1.9+
# === Configuracion ===
set myscript(version) "1.0"
set myscript(prefix) "!"
set myscript(logfile) "logs/myscript.log"
# === Bindings ===
bind pub - "${myscript(prefix)}help" myscript:help
bind join - "*" myscript:greet
bind pubm - "*" myscript:monitor
# === Procedimientos ===
proc myscript:help {nick uhost handle chan text} {
putserv "PRIVMSG $chan :Comandos: !help, !info, !stats"
}
proc myscript:greet {nick uhost handle chan} {
if {$nick eq $::botnick} { return }
putserv "PRIVMSG $chan :Bienvenido $nick."
}
proc myscript:monitor {nick uhost handle chan text} {
# Logica de monitorizacion
}
# === Inicializacion ===
putlog "myscript.tcl v$myscript(version) cargado."
# Para cargar en eggdrop.conf:
# source scripts/myscript.tcl
El patron es siempre el mismo: configuracion arriba, bindings en medio, procedimientos abajo, mensaje de carga al final. Los nombres de procedimiento suelen prefijarse con el nombre del script para evitar colisiones con otros scripts. Si tu script se llama trivia, tus procedimientos se llaman trivia:start, trivia:answer, trivia:score.
namespace eval para encapsular tu script si quieres ser limpio. Evita contaminar el namespace global con variables. En la practica, pocos scripts lo hacen, pero es una buena costumbre.El sistema de bindings
Los bindings son el mecanismo central de Eggdrop. Un binding asocia un evento IRC con un procedimiento Tcl. Cuando el evento ocurre, Eggdrop llama al procedimiento con los argumentos apropiados.
# Sintaxis: bind
# Tipos mas usados:
bind pub - "!cmd" mi:cmd ;# Mensaje publico con comando
bind pubm - "*" mi:pubm ;# Cualquier mensaje publico (wildcard)
bind msg - "!cmd" mi:msg ;# Mensaje privado con comando
bind join - "*" mi:join ;# Alguien entra a un canal
bind part - "*" mi:part ;# Alguien sale de un canal
bind sign - "*" mi:quit ;# Alguien hace quit
bind kick - "*" mi:kick ;# Alguien es kickeado
bind nick - "*" mi:nick ;# Cambio de nick
bind mode - "*" mi:mode ;# Cambio de modo
bind ctcp - "VERSION" mi:ver ;# CTCP VERSION
bind time - "00 * * * *" mi:midnight ;# Cada medianoche
bind dcc - "cmd" mi:dcc ;# Comando DCC (partyline)
# Flags:
# - = cualquier usuario
# o = solo ops del bot
# m = solo masters del bot
# n = solo owners del bot
# f = solo friends
# Argumentos por tipo de binding:
# pub: nick uhost handle chan text
# pubm: nick uhost handle chan text
# msg: nick uhost handle text
# join: nick uhost handle chan
# part: nick uhost handle chan msg
# sign: nick uhost handle chan reason
# kick: nick uhost handle chan target reason
# nick: nick uhost handle chan newnick
# time: min hour day month year
La diferencia entre pub y pubm es sutil pero importante. pub solo dispara cuando el mensaje empieza con el comando exacto. pubm dispara con cualquier mensaje que coincida con el patron — incluyendo mensajes que contienen el patron en cualquier posicion. Usa pub para comandos explicitos y pubm para monitorizacion.
# Ejemplo: anti-flood basico con bindings
set flood_data [dict create]
bind pubm - "*" antiflood:check
proc antiflood:check {nick uhost handle chan text} {
global flood_data
set key "$nick|$chan"
set now [clock seconds]
if {![dict exists $flood_data $key]} {
dict set flood_data $key [list 1 $now]
return
}
lassign [dict get $flood_data $key] count first
if {($now - $first) > 10} {
dict set flood_data $key [list 1 $now]
return
}
incr count
dict set flood_data $key [list $count $first]
if {$count >= 5} {
putserv "KICK $chan $nick :Flood detectado (5 lineas en 10s)"
putserv "MODE $chan +b *!*@[lindex [split $uhost @] 1]"
dict unset flood_data $key
# Quitar ban despues de 5 minutos:
utimer 300 [list putserv "MODE $chan -b *!*@[lindex [split $uhost @] 1]"]
}
}
Variables y estado
Tcl tiene variables locales (dentro de un proc) y variables globales. En Eggdrop, las variables globales persisten mientras el bot este corriendo. Si el bot se reinicia, se pierden — a menos que las guardes en fichero.
# Variables globales:
set mivar "valor"
# Acceder a global desde un proc:
proc mi:proc {args} {
global mivar
puts $mivar
}
# Arrays (muy usados en scripts Eggdrop):
set config(prefix) "!"
set config(maxwarn) 3
set config(logchan) "#admin"
# Acceder a array global:
proc mi:proc {args} {
global config
putserv "PRIVMSG $config(logchan) :Evento detectado"
}
# Diccionarios (Tcl 8.5+, recomendado sobre arrays para datos dinamicos):
set userdata [dict create]
dict set userdata "nick1" warns 0
dict set userdata "nick1" lastmsg [clock seconds]
dict incr userdata "nick1" warns
El user record system de Eggdrop es otra forma de almacenar estado. Cada usuario conocido por el bot tiene un handle con flags y campos personalizados. Puedes usar setuser y getuser para guardar datos persistentes asociados a usuarios:
# Guardar dato en user record:
setuser $handle XTRA "myscript.score" "150"
# Recuperar dato:
set score [getuser $handle XTRA "myscript.score"]
# Esto persiste entre reinicios del bot
# porque forma parte de la base de datos de usuarios
Timers y tareas programadas
# Timer unico (se ejecuta una vez):
# utimer = segundos, timer = minutos
utimer 60 {putserv "PRIVMSG #canal :Han pasado 60 segundos"}
timer 5 {putserv "PRIVMSG #canal :Han pasado 5 minutos"}
# Timer repetitivo:
proc mi:repetir {} {
putserv "PRIVMSG #canal :Ping periodico"
utimer 300 mi:repetir ;# Re-programar para dentro de 5 min
}
utimer 300 mi:repetir ;# Iniciar el ciclo
# Binding por tiempo (cron-like):
bind time - "00 * * * *" mi:cada_hora ;# Cada hora en punto
bind time - "30 12 * * *" mi:mediodia ;# Cada dia a las 12:30
bind time - "00 00 * * *" mi:medianoche ;# Cada medianoche
bind time - "00 00 1 * *" mi:primer_mes ;# El primero de cada mes
# Cancelar un timer:
set timerid [utimer 60 mi:algo]
killutimer $timerid
# Listar timers activos:
utimers ;# Devuelve lista de timers pendientes
Los timers bind time usan el formato minuto hora dia mes dia-de-semana, similar a cron pero con asteriscos para wildcard. Son utiles para tareas de mantenimiento: limpiar datos viejos, rotar logs, publicar estadisticas diarias.
utimer y un minuto para timer. No los uses para tareas que requieran precision temporal. Para IRC, esta precision es mas que suficiente.Acceso a ficheros y bases de datos
# Leer un fichero completo:
proc leer_fichero {path} {
if {![file exists $path]} { return "" }
set fd [open $path r]
set data [read $fd]
close $fd
return $data
}
# Escribir a un fichero:
proc escribir_fichero {path contenido} {
set fd [open $path w]
puts $fd $contenido
close $fd
}
# Anadir a un fichero (log):
proc log_evento {msg} {
global myscript
set fd [open $myscript(logfile) a]
puts $fd "[clock format [clock seconds] -format {%Y-%m-%d %H:%M:%S}] $msg"
close $fd
}
# Leer linea por linea (util para listas):
proc cargar_lista {path} {
set resultado [list]
set fd [open $path r]
while {[gets $fd linea] >= 0} {
if {$linea ne "" && ![string match "#*" $linea]} {
lappend resultado $linea
}
}
close $fd
return $resultado
}
# SQLite (si el modulo esta compilado):
package require sqlite3
sqlite3 db "data/myscript.db"
db eval {
CREATE TABLE IF NOT EXISTS scores (
nick TEXT PRIMARY KEY,
puntos INTEGER DEFAULT 0
)
}
db eval {INSERT OR REPLACE INTO scores VALUES ('nick1', 100)}
set puntos [db eval {SELECT puntos FROM scores WHERE nick='nick1'}]
Para datos simples (listas de palabras, configuraciones), los ficheros de texto son suficientes. Para datos relacionales o que necesitan busquedas eficientes, SQLite es la opcion correcta. No intentes implementar una base de datos con ficheros planos — ya lo he visto y siempre termina mal.
Peticiones HTTP y APIs
# Peticion HTTP basica (Tcl 8.6+):
package require http
package require tls
http::register https 443 ::tls::socket
proc fetch_url {url} {
set token [http::geturl $url -timeout 10000]
set data [http::data $token]
set code [http::ncode $token]
http::cleanup $token
if {$code != 200} {
return ""
}
return $data
}
# Ejemplo: bot de clima
bind pub - "!clima" bot:clima
proc bot:clima {nick uhost handle chan text} {
if {$text eq ""} {
putserv "PRIVMSG $chan :Uso: !clima "
return
}
set ciudad [http::formatQuery $text]
# Usar una API publica de clima:
set url "https://wttr.in/${ciudad}?format=3"
set resultado [fetch_url $url]
if {$resultado ne ""} {
putserv "PRIVMSG $chan :$resultado"
} else {
putserv "PRIVMSG $chan :No pude obtener el clima para $text"
}
}
# JSON parsing (Tcl 8.6+ con tcllib):
package require json
set parsed [json::json2dict $jsonstring]
set valor [dict get $parsed "key"]
Las peticiones HTTP desde Eggdrop son sincronas por defecto — el bot se bloquea mientras espera la respuesta. Para un bot en canales con mucho trafico, esto puede ser un problema si la API es lenta. La solucion es usar el modo asincrono de http::geturl con un callback, pero la complejidad del codigo se multiplica. Para la mayoria de los casos, un timeout de 5-10 segundos es aceptable.
Ejemplos practicos completos
# === EJEMPLO 1: Sistema de advertencias ===
# Los ops pueden dar warns. Al tercer warn, kick+ban temporal.
set warn(max) 3
set warn(bantime) 3600 ;# 1 hora en segundos
set warn(data) [dict create]
bind pub o "!warn" warn:add
bind pub o "!unwarn" warn:remove
bind pub - "!warns" warn:check
proc warn:add {nick uhost handle chan text} {
global warn
set target [lindex [split $text] 0]
if {$target eq ""} {
putserv "PRIVMSG $chan :Uso: !warn \[razon\]"
return
}
set razon [join [lrange [split $text] 1 end]]
if {$razon eq ""} { set razon "Sin razon especificada" }
set key [string tolower $target]
set count 0
if {[dict exists $warn(data) $key]} {
set count [dict get $warn(data) $key]
}
incr count
dict set warn(data) $key $count
if {$count >= $warn(max)} {
putserv "KICK $chan $target :$warn(max) advertencias alcanzadas: $razon"
set host [getchanhost $target $chan]
if {$host ne ""} {
set banmask "*!*@[lindex [split $host @] 1]"
putserv "MODE $chan +b $banmask"
utimer $warn(bantime) [list putserv "MODE $chan -b $banmask"]
}
dict unset warn(data) $key
putserv "PRIVMSG $chan :$target baneado por $razon (warn $count/$warn(max))"
} else {
putserv "PRIVMSG $chan :$target advertido ($count/$warn(max)): $razon"
}
}
proc warn:remove {nick uhost handle chan text} {
global warn
set target [string tolower [lindex [split $text] 0]]
if {[dict exists $warn(data) $target]} {
dict unset warn(data) $target
putserv "PRIVMSG $chan :Advertencias de $target borradas."
}
}
proc warn:check {nick uhost handle chan text} {
global warn
set target [lindex [split $text] 0]
if {$target eq ""} { set target $nick }
set key [string tolower $target]
set count 0
if {[dict exists $warn(data) $key]} {
set count [dict get $warn(data) $key]
}
putserv "PRIVMSG $chan :$target tiene $count/$warn(max) advertencias."
}
# === EJEMPLO 2: RSS feed a canal ===
# Publica nuevas entradas de un feed RSS cada 15 minutos.
package require http
package require tls
http::register https 443 ::tls::socket
set rss(url) "https://example.com/feed.xml"
set rss(chan) "#noticias"
set rss(seen) [list]
set rss(max_seen) 50
proc rss:check {} {
global rss
set data [fetch_url $rss(url)]
if {$data eq ""} {
utimer 900 rss:check
return
}
# Parsing XML basico (funciona para RSS simple):
set items [regexp -all -inline {- .*?
} $data]
foreach item $items {
regexp {(?:]+>)?(.+?)(?: )} $item -> title
regexp {(.+?)} $item -> link
if {[lsearch -exact $rss(seen) $link] == -1} {
putserv "PRIVMSG $rss(chan) :RSS: $title — $link"
lappend rss(seen) $link
if {[llength $rss(seen)] > $rss(max_seen)} {
set rss(seen) [lrange $rss(seen) end-$rss(max_seen) end]
}
}
}
utimer 900 rss:check
}
utimer 10 rss:check
putlog "rss.tcl cargado — monitorizando $rss(url)"
Preguntas frecuentes
Puedo usar Python en vez de Tcl con Eggdrop?
Si, desde Eggdrop 1.9 hay un modulo experimental de Python. Pero la integracion no es tan madura como Tcl — faltan bindings, la documentacion es escasa, y la mayoria de scripts existentes estan en Tcl. Si quieres Python como lenguaje principal, considera usar Sopel o Limnoria directamente en vez de Eggdrop.
Como depuro un script que no funciona?
Usa putlog para imprimir mensajes al log del bot en cada paso critico. Revisa el partyline (DCC chat con el bot) donde aparecen los errores Tcl. Los errores de sintaxis suelen ser llaves desbalanceadas o comillas faltantes. El error mas comun en scripts de principiante es olvidar global al acceder a variables globales desde un proc.
Un script puede crashear todo el bot?
Un error Tcl no crashea Eggdrop — Eggdrop captura el error, lo logea, y sigue corriendo. Lo que si puede pasar es un loop infinito que congele al bot, o un script que envie tanto texto que el servidor lo desconecte por flood. Usa siempre rate limiting cuando envies mensajes y evita recursion sin limites.