Exploring the roots and future of Internet Relay Chat

Scripting Eggdrop en Tcl : le guide pratique

Tcl est un langage que personne ne choisit volontairement en 2025. Sauf les gens qui ecrivent des scripts Eggdrop. Et pour cause : Eggdrop est scripted en Tcl depuis 1993, son API est en Tcl, sa communaute pense en Tcl. Si vous voulez programmer un Eggdrop, vous apprenez Tcl. C'est non negociable. La bonne nouvelle : Tcl est simple. Pas elegant, pas moderne, mais simple. En une journee, vous pouvez ecrire un script fonctionnel.

Les bases de Tcl pour Eggdrop

# Tcl en 5 minutes :

# Variables :
set nick "MonBot"
set compteur 0

# Lire une variable :
puts $nick
# -> MonBot

# Listes :
set canaux {#chan1 #chan2 #chan3}
lappend canaux "#chan4"
foreach canal $canaux {
  puts "Canal: $canal"
}

# Conditions :
if {$compteur > 10} {
  puts "Trop de messages"
} elseif {$compteur > 5} {
  puts "Attention"
} else {
  puts "OK"
}

# Procedures (fonctions) :
proc saluer {nom} {
  return "Bonjour $nom !"
}

# Expressions regulieres :
if {[regexp {^!commande (.+)} $texte match argument]} {
  # $match contient le texte complet
  # $argument contient le groupe capture
}

# Manipulation de chaines :
set resultat [string tolower $texte]
set mots [split $texte " "]
set premier [lindex $mots 0]

# La regle d'or de Tcl : tout est une chaine.
# Meme les nombres sont des chaines qui se comportent
# comme des nombres quand vous les utilisez dans expr.

La syntaxe de Tcl est minimale. Les accolades {} delimitent des blocs litteraux (pas d'interpretation). Les crochets [] evaluent une commande et retournent le resultat. Le signe $ lit une variable. C'est tout.

Bindings : reagir aux evenements

Un binding (liaison) connecte un evenement IRC a une procedure Tcl. C'est le mecanisme fondamental d'un script Eggdrop.

# Syntaxe : bind TYPE FLAGS PATTERN PROCEDURE

# Reagir a un message public :
bind pub - !ping cmd_ping
proc cmd_ping {nick uhost handle channel text} {
  putserv "PRIVMSG $channel :pong, $nick"
}

# Reagir a un message prive :
bind msg - !info cmd_info
proc cmd_info {nick uhost handle text} {
  putserv "PRIVMSG $nick :Je suis un Eggdrop v1.9"
}

# Reagir quand quelqu'un rejoint un canal :
bind join - * greet_user
proc greet_user {nick uhost handle channel} {
  if {$nick != $::botnick} {
    putserv "PRIVMSG $channel :Bienvenue $nick !"
  }
}

# Reagir a un kick :
bind kick - * on_kick
proc on_kick {nick uhost handle channel target reason} {
  putlog "$target a ete kicke de $channel par $nick ($reason)"
}

# Reagir a un changement de mode :
bind mode - "* +o" on_op
proc on_op {nick uhost handle channel mode target} {
  putlog "$target a recu +o sur $channel par $nick"
}

# Reagir a un CTCP :
bind ctcp - VERSION ctcp_version
proc ctcp_version {nick uhost handle dest keyword text} {
  putserv "NOTICE $nick :\001VERSION Eggdrop avec scripts custom\001"
  return 0
}

# Types de bindings disponibles :
# pub  -> Message public (!commande)
# msg  -> Message prive
# join -> Entree dans un canal
# part -> Sortie d'un canal
# kick -> Kick d'un canal
# nick -> Changement de nick
# mode -> Changement de mode
# ctcp -> Messages CTCP
# raw  -> Messages IRC bruts (numeriques)
# dcc  -> Commandes party line
# time -> Declenchement periodique

# Flags dans les bindings :
# -     -> N'importe qui
# o     -> Operateurs du bot
# m     -> Masters du bot
# n     -> Owners du bot
# +flag -> Utilisateur avec ce flag custom
Conseil : Les parametres passes aux procedures dependent du type de binding. Chaque type a sa propre signature. La documentation Eggdrop liste la signature exacte de chaque type. Ne devinez pas — verifiez.

Timers et taches planifiees

# Timer unique (execute une fois) :
# timer MINUTES COMMANDE
timer 5 {putserv "PRIVMSG #canal :5 minutes ecoulees"}

# Timer en secondes :
# utimer SECONDES COMMANDE
utimer 30 {putlog "30 secondes ecoulees"}

# Timer repetitif :
proc annonce_periodique {} {
  putserv "PRIVMSG #canal :Message automatique toutes les heures"
  timer 60 annonce_periodique
}
# Lancer la premiere fois :
timer 60 annonce_periodique

# Binding time (execute a une heure precise) :
# bind time FLAGS "MINUTE HEURE JOUR MOIS JOUR_SEMAINE" PROC
bind time - "00 09 * * *" bonjour_matin
proc bonjour_matin {minute heure jour mois jour_semaine} {
  putserv "PRIVMSG #canal :Bonjour ! Il est 9h."
}

# Annuler un timer :
set mon_timer [utimer 60 ma_commande]
killutimer $mon_timer

# Lister les timers actifs :
putlog "Timers: [timers]"
putlog "Utimers: [utimers]"

Attention au timer repetitif : si le script est recharge (.rehash), les timers actifs ne sont pas annules automatiquement. Vous pouvez vous retrouver avec des timers en double. Ajoutez une garde :

# Annuler les anciens timers avant d'en creer de nouveaux :
foreach t [utimers] {
  if {[lindex $t 1] eq "annonce_periodique"} {
    killutimer [lindex $t 2]
  }
}
timer 60 annonce_periodique

Manipuler des fichiers

# Lire un fichier ligne par ligne :
proc lire_fichier {chemin} {
  set fp [open $chemin r]
  set contenu [read $fp]
  close $fp
  return [split $contenu "\n"]
}

# Ecrire dans un fichier :
proc ecrire_fichier {chemin texte} {
  set fp [open $chemin w]
  puts $fp $texte
  close $fp
}

# Ajouter a un fichier (append) :
proc logger {chemin texte} {
  set fp [open $chemin a]
  set timestamp [clock format [clock seconds] -format "%Y-%m-%d %H:%M:%S"]
  puts $fp "$timestamp $texte"
  close $fp
}

# Exemple : systeme de citations
set quotes_file "scripts/quotes.txt"

bind pub - !addquote cmd_addquote
proc cmd_addquote {nick uhost handle channel text} {
  global quotes_file
  if {$text eq ""} {
    putserv "PRIVMSG $channel :Usage: !addquote "
    return
  }
  logger $quotes_file "$nick: $text"
  putserv "PRIVMSG $channel :Citation ajoutee."
}

bind pub - !quote cmd_quote
proc cmd_quote {nick uhost handle channel text} {
  global quotes_file
  if {![file exists $quotes_file]} {
    putserv "PRIVMSG $channel :Aucune citation."
    return
  }
  set lines [lire_fichier $quotes_file]
  set lines [lsearch -all -inline -not $lines ""]
  if {[llength $lines] == 0} {
    putserv "PRIVMSG $channel :Aucune citation."
    return
  }
  set random_line [lindex $lines [expr {int(rand() * [llength $lines])}]]
  putserv "PRIVMSG $channel :$random_line"
}

Requetes HTTP

# Tcl inclut le package http :
package require http
package require tls

# Requete GET simple :
proc http_get {url} {
  ::http::register https 443 [list ::tls::socket -autoservername true]
  set token [::http::geturl $url -timeout 10000]
  set data [::http::data $token]
  ::http::cleanup $token
  return $data
}

# Exemple : commande meteo avec une API
bind pub - !meteo cmd_meteo
proc cmd_meteo {nick uhost handle channel text} {
  if {$text eq ""} {
    putserv "PRIVMSG $channel :Usage: !meteo "
    return
  }
  set ville [::http::formatQuery $text]
  set url "https://wttr.in/${ville}?format=3"
  if {[catch {
    set resultat [http_get $url]
    putserv "PRIVMSG $channel :$resultat"
  } err]} {
    putserv "PRIVMSG $channel :Erreur: $err"
  }
}

# Parser du JSON (Tcl 8.6+) :
package require json
set data [::json::json2dict $json_string]
set valeur [dict get $data "cle"]
Attention : Les requetes HTTP dans un script Eggdrop sont bloquantes. Pendant que le bot attend la reponse, il ne traite pas les autres evenements. Pour des requetes frequentes, utilisez le mode asynchrone de ::http::geturl avec un callback. Sinon, un serveur lent peut geler votre bot.

Exemples pratiques

# === Anti-flood simple ===
set flood_data [dict create]

bind pub - * flood_check
proc flood_check {nick uhost handle channel text} {
  global flood_data
  set now [clock seconds]
  set key "$nick:$channel"

  if {![dict exists $flood_data $key]} {
    dict set flood_data $key [list $now]
    return
  }

  set times [dict get $flood_data $key]
  # Garder seulement les 10 dernieres secondes
  set times [lsearch -all -inline -not $times ""]
  set recent [list]
  foreach t $times {
    if {$now - $t < 10} {
      lappend recent $t
    }
  }
  lappend recent $now
  dict set flood_data $key $recent

  if {[llength $recent] > 5} {
    # Plus de 5 messages en 10 secondes
    putserv "KICK $channel $nick :Flood detecte"
    putserv "MODE $channel +b *!*@[lindex [split $uhost @] 1]"
    dict unset flood_data $key
  }
}

# === Seen (derniere activite) ===
set seen_file "scripts/seen.dat"
set seen_data [dict create]

# Charger les donnees au demarrage
if {[file exists $seen_file]} {
  set fp [open $seen_file r]
  set seen_data [read $fp]
  close $fp
}

bind pub - * seen_track
proc seen_track {nick uhost handle channel text} {
  global seen_data seen_file
  dict set seen_data [string tolower $nick] \
    [list [clock seconds] $channel "a dit: $text"]
}

bind pub - !seen cmd_seen
proc cmd_seen {nick uhost handle channel text} {
  global seen_data
  set target [string tolower $text]
  if {[dict exists $seen_data $target]} {
    set info [dict get $seen_data $target]
    set quand [clock format [lindex $info 0] -format "%Y-%m-%d %H:%M"]
    set ou [lindex $info 1]
    set quoi [lindex $info 2]
    putserv "PRIVMSG $channel :$text vu pour la derniere fois le $quand sur $ou ($quoi)"
  } else {
    putserv "PRIVMSG $channel :Je n'ai jamais vu $text."
  }
}

# Sauvegarder periodiquement
proc seen_save {} {
  global seen_data seen_file
  set fp [open $seen_file w]
  puts $fp $seen_data
  close $fp
  timer 30 seen_save
}
timer 30 seen_save

Debug et bonnes pratiques

  • putlog : Utilisez putlog "message" pour ecrire dans les logs d'Eggdrop. C'est votre outil de debug principal.
  • catch : Entourez les operations risquees (fichiers, HTTP) avec catch pour eviter que le script plante le bot entier.
  • Namespaces : Pour les scripts complexes, utilisez les namespaces Tcl pour eviter les collisions de noms entre scripts.
  • Rate limiting : Utilisez puthelp (basse priorite) au lieu de putserv (haute priorite) pour les messages non urgents. Eggdrop a une file d'attente interne qui evite le flood.
  • Rechargement : Testez vos scripts avec .tcl source scripts/monscript.tcl depuis la party line sans redemarrer le bot.
# Priorites d'envoi Eggdrop :
putserv "..."   # Haute priorite (mode, kick, ban)
puthelp "..."   # Basse priorite (messages informatifs)
putquick "..."  # Priorite moyenne

# Chaque file a sa propre limite de debit.
# putserv est envoye immediatement.
# puthelp attend si la file est pleine.
# Utilisez puthelp par defaut et putserv
# uniquement pour les actions urgentes.

Questions frequentes

Faut-il apprendre Tcl en 2025 ?

Seulement si vous utilisez Eggdrop. Tcl n'est pas un langage demande sur le marche du travail. Mais dans le contexte d'Eggdrop, c'est le seul choix. L'avantage : Tcl est simple et les scripts Eggdrop sont courts. Vous n'avez pas besoin de maitriser le langage en profondeur — les bases suffisent pour 90% des scripts.

Peut-on utiliser Python avec Eggdrop ?

Pas nativement. Il existe des modules experimentaux pour executer du Python depuis Eggdrop, mais ils sont instables. Si vous voulez programmer un bot en Python, utilisez Sopel ou Limnoria plutot qu'Eggdrop. Chaque outil avec son langage.

Ou trouver des scripts Eggdrop ?

egghelp.org a la plus grande archive. Le GitHub d'Eggdrop a aussi des scripts dans le repertoire scripts/. Attention aux scripts anciens : beaucoup datent des annees 2000 et utilisent des techniques obsoletes (DCC au lieu d'HTTP, pas de TLS, pas de gestion d'erreurs).