Einführung in Ereignisorientierte IRC-Botprogrammierung mit AWK
Dieses Dokument stellt eine Möglichkeit an IRC mittels AWK zu schreiben,
ohne direkt mit der TCP/IP Schnittstelle von gawk, der GNU
Implementierung und Erweiterung von AWK Beschäftigen zu müssen. Hierzu
wird die irc.awk
quasi-Bibliothek benutzt.
Es werden minimale Kenntnisse AWK angenommen. Falls man nicht glaubt
diese zu besitzen, kann man The AWK Programming Language von Kernighan
und Weinberger lesen, oder auch The GNU Awk User’s Guide bzw. gawk(1)
.
Die meisten Konzepte sollten jedoch einem Bekannt vorkommen, wenn man
mit C-Artiger Syntax zu tun hatte, wie auch mit Sprachen ohne striktem
Typsystem. Die einzige große Unterschied ist das Konzept von
pattern-action-Statments, dh. das Programm ist untergliedert in bedingte
Blöcke (action), welche jeweils nur ausgeführt werden wenn ihre
Bedingung (pattern) einschlägt. Dieses wird im Folgenden ausgenutzt
werden um Ereignis-Orientiert (JOIN
, PRIVMSG
, …) Programmieren zu
können.
irc.awk
wurde unter CC0 veröffentlicht, und darf somit frei
weiter-verteilt und verändert werden. Verbesserungen in Form von
Vorschlägen und/oder Code werden stets gerne gesehen (Kontakt).
Die jetztige Fassung des Dokuments ist für Version 0.1 vorgesehen.
irc.awk
kann hier heruntergeladen werden
1 Vorwort, Vorab
irc.awk
ist nicht stabil. Einzelne Funktionen oder „best
practices“ werden sich mit hoher Wahrscheinlichkeit ändern.
(Es wird nochmal darauf hingewiesen das irc.awk
nur für GNU AWK
geschrieben wurde, und sich in keiner weise an die POSIX Spezifikation
haltet. Die Bibliothek würde mit GAWK 4.1 und neuer getestet.)
2 Einführung irc.awk
Der Konzeptuelle Aufbau eines Programms, welches irc.awk
benutzt,
besteht aus drei Teilen:
- Setup
- Kanäle Beitreten
- Ereignissabarbeitung
2.1 Setup
Die Setup-Phase besteht hauptsächlich, bzw. wird beendet durch das
erfolgreiche Aufrufen der irc_setup
Funktion. Zuvor ist es durchaus
möglich, dass das Programm von der Standard Eingabe Daten einließt,
Netzwerkverbindungen initialisiert, Parallel-Programme startet oder
Berechnungen durchführt. Jedoch kann gesagt werden: Für die meisten
fällte, reicht das Folgende aus:
BEGIN { irc_setup("irc.server.com", 0, "nick_314"); }
Dieses initialisiert eine Verbindung zu irc.server.com
auf dem
Standard (nicht-SSL) Port, und versucht den Namen „nick_314“ zu
beanspruchen. Genaueres wird in den später folgenden Beispielen, wie
auch im Funktionenindex besprochen.
2.2 Kanäle Beitreten
Von allen Schritten, erweist sich dieses als der Einfachste. Das Beitreten sollte selten mehr erfordern als
irc_join("#kanal");
aufzuführen. Wie man bereits bemerkt haben mag, fangen alle
Funktionen von irc.awk
mit dem irc_
Präfix an. Dieses soll helfen
Namenskonflikte mit eigenen Funktionen zu vermeiden, da AWK nur ein
einfachen Namensraum besitzt (analog zu C oder alten Lisps).
Es sollte darauf Acht gegeben werden: Durch aufrufen der Funktione
irc_join
kann es dazu kommen, das andere Ereignisse verpasst
werden. Intern ließt diese Funktion so lange weiter, bis es sicher
ist, im Raum zu sein. Dieses kann sich wahrscheinlich mit zukünftigen
Versionen ändern.
2.3 Ereignissabarbeitung
Dieser Teil steht zum Vorherigen in weitaus kritischerer Relevanz,
was die tatsächliche, kennzeichnende Funktionalität des Bots angeht,
als die vorherigen Schritte. Unter Ereignissen versteht irc.awk
Sachen wie das Empfangen einer Nachricht, das Betreten oder Verlassen
eines Klienten in, bzw. aus, eines Kanals, das explizite Ansprechen
des Bots, etc.
Ereignisse werden als gewöhnliche AWK Patterns dargestellt, zu welchem Zweck Hilfsvariablen benutzt werden. Hier ein Beispiel:
MSG ~ /color/ { irc_msg("it's \"colour\""); }
Für jede Nachricht in dem color erwähnt wird, würde der Bot
Erwidern (→ irc_msg
) it’s „colour“. Das Ereignis wird definiert
durch das Erwähnen von color in einer textuellen Nachricht (→ MSG
),
und die Behandlung folgt im nächstem Block.
Mit den von irc.awk
bereitgestellten Hillfsfunktionen, kann dieses
auch so ausgedrückt werden:
READ("color") { irc_msg("it's \"colour\""); }
Bedienungen können auch mit beliebigen anderen Kombiniert werden,
da diese gewöhnliche AWK ausdrücke sind, nur mit dem Unterschied das
irc.awk
die Hillfsvariablen vor jedem „Zeilendurchlauf“ setzt, um die
Ereignisse besser darzustellen.
An dieser Stelle muss eine „unglückliche“ Besonderheit erwähnt
werden: Da AWK aus dem UNIX™-Ecosystem entstammt, und zur
Verarbeitung von Textströmen gedacht ist, verarbeitet AWK mit irc.awk
auch nur Ereignisse, dh. „Patterns“ und „Actions“ beim Einlesen eines
„Records“ (durch die Variable RS
delimitrierte Felder). Daher braucht
ein IRC-Bot das irc.awk
ein „Taktgeber“. Das einfachste, das zu
diesem Zweck genutzt werden kann, ist das Programm yes(1)
, welches
lediglich, ohne zu enden, y
auf einzelnen Zeilen ausgibt. Für
verschiedene Zwecke mag verschiedenes mehr Sinn machen, jedoch ist
yes
für die aller meisten Zwecke (wieder) das Richtige.
3 Beispiele
Es folgend nun Abschnitte mit vollkommenen Beispielen. Diese sollten jeweils so ausgeführt werden:
yes | awk -f irc.awk -f [script].awk
Falls irc.awk
in AWKLIBPATH
zu finden ist, kann auch
yes | awk -l irc -f [script].awk
benutzt werden.
3.1 Greeter
Das erste Beispiel, soll das Konzept der Ereignissorieniertung verdeutlichen. Hierzu wird ein Bot beschrieben, der Folgendes macht:
- tritt dem Kanal
#test
bei alsbot
. Der Server sollirc.server.com
sein. - erwidert „hi, [benutzername]“ wenn jemand eine Nachricht schreibt die mit „hi“ anfängt.
- sobald ein Nutzer dem Kanal beitritt, grüßt
bot
diesen mit „willkommen in [kanalname]“ - wenn
bot
angesprochen wird, verlässt dieser den Raum und bricht die Verbindung ab.
Der erste Schritt ist leicht getan. Zu Anfang (→ BEGIN
) wird irc_setup
und
irc_join
aufgerufen. Die Bedeutung der Argumente sollte ersichtlich
sein, wird aber auch später bei Öffentliche Funktionen erklärt:
BEGIN { irc_setup("irc.server.com", 0, "bot"); irc_join("#test"); }
nun sollte alles Aufgestellt sein. Und es werden die oben genannten Ereignisse beschrieben.
Für das erste Ereignisse, müssen Nachrichten gelesen werden. Man kann
überprüfen ob eine Nachricht angekommen ist, indem die MSG
Hilfsvariable überprüft wird. Dieses enthält eine Zeichenkette,
welche mit einer regulären Aussage überprüft werden kann:
MSG && MSG ~ /^hi\W/ { irc_msg("hi, " WHO); }
Es würde also überprüft ob eine Nachricht vorliegt und ob diese Nachricht mit einem „hi“ anfängt (gefolgt von einem nicht-Wort-Zeichen).
irc_msg
schickt die Zeichenkette, welche als Argument übergeben
würde, als Argument an die Person oder Gruppe weiter, die das
derzeitige Ereignis ausgelöst haben. Die Variable WHO
wird dessen
Nickname enthalten, welches hier auf "hi, "
konkateniert wurde.
Ähnlich wird auch das Ereignis „Beitreten“ abgehandelt. In diesem
Fall wird die JOIN
Variable benutzt, welches im Gegensatz zu MSG
nur
ein Wahrheitswert enthällt.
JOIN { irc_msg("greetings, welcome to " TO); }
Hier wurde, ähnlich zum vorherigen Beispiel irc_msg
benutzt, aber
dieses mal mit der Variable TO
. Dieses sollte #test
enthalten, da der
Bot nur diesem Kanal beigetreten ist. Allgemein enthält es eine
Zeichenkette die besagt, wo das jetzige Ereignis ausgelöst wurde.
Zuletzt, wird der Fall betrachtet das der Bot angesprochen wurde. Dieses wird weitestgehend die gleiche Form haben wie die erste Ereignisbehandlung (überprüfen auf Existenz einer Nachricht, sowie dessen Muster), nur sich im Action unterscheiden:
MSG && MSG ~ /^bot:/ { exit; }
Beachte: Keine Funktion mit irc_
-Präfix musste benutzt werden. Dieses
ist weil irc.awk
dafür sorgt, dass bevor das Programm sich beendet
(→ AWK Statement exit
) es die Verbindung sauber abbricht.
Und das war es. Hoffentlich würden hiermit die Grundzüge eines
irc.awk
Bots nachvollziehbar.
3.2 Latein
Das nächste Beispiel wird eine andere Form haben. Das Ziel wird sein Latein-Kenntnisse der Nutzer zu verbessern, indem diese auf falsches Deklinieren des Nominativ Singular in Nominativ Plural (nur für die 2. Deklination, und i-Stämme der 3.) hingewiesen werden (→ Wikipedia).
Der erste Unterschied wird bei der Initialisierung zu finden sein. Was bei Greeter zwei Schritte waren, wird nun in eines Zusammengefasst:
irc_setup("irc.server.com", 0, "latein", "#latein #sprachen");
Folgendes ist neu:
- Kein
BEGIN
Block.irc_setup
lässt sich nur einmal erfolgreich Aufrufen pro Durchlauf, und danach ignoriert es einfach alle Aufrufe sofort. Dieses bedeutet dasirc_setup
einfach auf eine Zeile gelegt werden kann (als Pattern), und es nur beim ersten Durchlauf ein Effekt hat. - Ein viertes Argument (Autojoin) wurde übergeben (
"#latein #sprachen"
). In diesem sind zwei Kanal-Namen zu sehen, mit einem Leerzeichen voneinander getrennt. Der Bot wird versuchen beiden Kanälen beizutreten, ohne explizites Aufrufen vonirc_join
.
Zum Erkennen der Nachrichten die mit Fehlern dekliniert wurden, wird die Heuristik „[singular wortstamm] + e“ Verwendet, und als hinreichend Abgesegnet.
Im Unterschied zum vorherigen Beispiel, wird die Hillfsfunktion
READ
benutzt. Dieses gibt „Wahr“ zurück, wenn das als Argument
übergebene Muster mit einer Text-Nachricht übereinstimmt.
READ("\\w+use") { correct("use", "i"); } READ("\\w+ise") { correct("ise", "es"); } READ("\\w+ume") { correct("ume", "a"); }
Anstatt selbst eine irc_
Funktion aufzurufen, wird die hier
definierte correct
benutzt, welches hier definiert wird.
function correct(wrong, plural) { match(MSG, ("\\w+" wrong), mat); singular = gensub("e$", "", "g", mat[0]); corrected = gensub(wrong "$", plural, "g", mat[0]); irc_msg("Ahem, die korrekte Pluralform von \"" singular "\" ist \"" corrected "\".") }
Wie die AWK Funktionen funktionieren wird hier beiseite gelassen. Das
relevant ist dass MSG
(in match
), obwohl nicht übergeben, weiterhin
benutzt werden kann.
3.3 Fortune
Das nächste Beispiel reagiert auf den Pseudobefehl !fortune
, indem es
ein Sinnspruch (im weitestem Sinne) zurückschreibt, welches durch
fortune(6)
generiert wird.
Zunächst die Initialisierung:
BEGIN { prog = "fortune -s" irc_setup("irc.server.com", "ssl", "fortune", "!"); }
Die Variable prog
enthält ein Shell-Befehl, mit dem die Sinnsprüche
generiert werden, und ist fürs erste nicht interessant. irc_setup
wird wie bisher aufgerufen, mit folgenden Abänderungen:
- Anstatt
0
wird"ssl"
als Port-Argument übergeben. Dieses bedeutet das wir versuchen werden eine verschlüsselte Verbindung (Port 6697) aufzubauen. Das vierte Argument enthält lediglich ein
!
. Wenn die Autojoin Liste mit einem Ausrufezeichen anfängt, reagiert unser Bot aufINVITE
Nachrichten, und tritt sofort dem eingeladenem Kanal bei.Es ist weiterhin möglich nach dem
!
feste Kanalnamen anzugeben.
Das Ausgeben des Sinnspruchs ist weitestgehend uninteressant für dieses Dokument. Die Funktionsweise wird in der GAWK Dokumentation erklärt.
READ("^!fortune") { while ((prog | getline line) > 0) irc_msg(line) close(prog) }
Das einzige nennenswerte ist das irc_msg
mehrfach aufgerufen wird.
Alternativ kann der Bot erweitert/verändert werden, so lange von der Standard Eingabe zu lesen, und alle Zeilen als Sprüche im Speicher zu speichern, bis eine leere Zeile kommt, und dann eine Verbindung zum Server aufbaut.
3.4 Logger
Das letzte Beispiel agiert nicht mit dem Chat, sondern mit der „lokalen Außenwelt“. Es soll einem Kanal beitreten, welches durch ein Argument angegeben wird auf der Konsole, und zur Standard Ausgabe HTML-Code ausgeben, welches ein Log darstellt, was geschrieben wurde.
Wie immer wird zu Anfang eine Verbindung aufgebaut
BEGIN { if (ARGC < 1) { print "usage: logger.awk [channel]" > "/dev/stderr"; exit; } else { irc_setup("irc.server.com", 0, "notalogger", ARGV[0]); }
Die ARGV
und ARGC
werden benutzt um Kommandozeilen Parameter
einzulesen, wie in einem C oder Java Programm, nur enthällt ARGV[0]
nicht den Namen des Befehls, sondern das erste Argument
Nachdem eine Verbindung aufgebaut wurde, wird eine HTML (unsaubere) Präambel generiert.
print "<!DOCTYPE html>" print "<meta charset=\"utf-8\" />" print "<link rel=\"stylesheet\" href=\"/logger.css\" />" print "<h1>" ARGV[0] " Log</h1>" print "<table>" }
Es wird angenommen das logger.css
existiert, und ästhetische Beihilfe
leistet.
Nun kann für jedes relevante Ereignis eine Tabellenzeile (→ tr
)
ausgegeben. Das einfachste Beispiel sollten Nachrichten sein:
MSG { print "<tr>" print "<td>" WHO "</td>" print "<td>" MSG "</td>" print "<td>" strftime("%X") "</td>" print "</tr>" }
Jede Zeile enthält also drei Spalten mit dem Nickname, der Nachricht und einem Zeitstempel.
Leicht abgewandelt werden JOIN
, PART
, QUIT
und KICK
Nachrichten
verarbeitet:
JOIN { print "<tr><td></td>" print "<td>" WHO " ist beigetreten</td>" print "<td>" strftime("%X") "</td>" print "</tr>" } PART { print "<tr><td></td>" print "<td>" WHO " hat den Kanal verlassen (\"" EMSG "\")</td>" print "<td>" strftime("%X") "</td>" print "</tr>" } QUIT { print "<tr><td></td>" print "<td>" WHO " hat seine Verbindung abgebrochen (\"" EMSG "\")</td>" print "<td>" strftime("%X") "</td>" print "</tr>" } KICK { print "<tr><td></td>" print "<td>" WHO " wurde aus dem Kanal geschmissen (\"" EMSG "\")</td>" print "<td>" strftime("%X") "</td>" print "</tr>" }
Das einzige neue ist EMSG
, welches eine Event Message enthält, wieso
zb. eine Nutzer den Raum verlassen hat.
Interessierte können versuchen den Code so zu erweitern (ggf mittels AWK Funktionen), dass verwandte Nachrichten (gleiche Benutzer, Mentions, etc.) einen semantischen Zusammenhang im HTML Code haben.
4 Variablen- und Funktionenindex
4.1 Öffentliche Funktionen
irc_setup(host, port, nick, autojoin, user, realname, passwd)
- Dieses ist die Initialisierungsfunktion, welches aufgerufen
werden muss, um die Verbindung zu einem Server
aufzustellen. Dabei sind mindestens 3 Argumente zu übergeben:
host
, welches die Adresse des IRC Servers angibt (irc.fau.de
,freenode.net
, …)port
gibt den Port des zuvor angegebene Servers an, mittels dessen eine Verbindung aufgebaut werden soll. Hierbei sind zwei Werte interessant. Wird0
angegeben, ersetztirc_setup
dieses mit6667
, wird (als String)ssl
angegeben, wird dieses mit6697
ersetzt.nick
setzt den Nickname des Bots fest.Alle anderen Argumente sind optional:
autojoin
soll eine String von Leerzeichen-Unterteilten Kanal sein (eg.#linux &chat #news
). Ist das erste Zeichen ein Ausrufezeichen (!
), so wird dieser Bot aufINVITE
anfragen reagieren, und sofort in den eingeladenen Kanal eintreten.user
undrealname
entsprechen den IRC Konzepten von „realname“ und „username“. Genaueres kann hier oder hier nachgelesen werden.passwd
muss nur dann angegeben werden, falls der Server ein Passwort benötigt, um eine Verbindung aufzubauen.
irc_setup
kann nur einmal (erfolgreich) aufgerufen werden. Alle
nachfolgenden Aufrufe werden ignoriert. Diese Tatsache kann
ausgenutzt werden um BEGIN
Blöcke zu vermeiden, wie beim Beispiel
„Latein“ gesehen.
irc_join(chan, passwd)
- Beim Aufrufen dieser Funktion, versuch
der Bot in den Kanal
chan
beizutreten. Falls ein Passwort notwendig ist, muss dieses inpasswd
angegeben werden. irc_send(to, msg)
- Diese Funktion schickt eine Nachricht
msg
anto
, welches ein Nutzer oder auch Kanal sein darf. irc_msg(msg)
- Als Vereinfachung von
irc_send
, nimmtirc_msg
immer als Wert vonto
den Namen des Nutzers oder Kanals, welches die jetzige Ereignissbehandlung ausgelößt wurde. irc_quit(quitmsg, msg, len)
Zum beendigen des Bots, kann diese Funktion aufgerufen werden. Dieses ist jedoch meist nicht Notwendig, da sie Implizit aufgerufen wird nach dem Aufführen des AWK
exit
Statements. Dieses Verhalten kann durch Veränderung der__irc_noquit
Variable verändert werden.Zu beachten ist,
msg
undlen
sind lokale Variablen die nicht gesetzt werden sollten.quitmsg
kann eine „Quit Message“ angeben, falls dieses nicht gemacht wird, wird eine zufällige ausgewählt.
4.1.1 Hillfsfunktionen für Ereignisse
Während die tatsächlichen Zustände in den Hillfsvariablen stehen, können diese Hillfsfunktionen es einem einfacher machen mit Ereignissarten einheitlicher zu hantieren.
READ(pat, to)
- Wurde eine textuelle Nachricht empfahlen, in dem
das Muster
pat
zu erkennen ist? Ggf. mit Eingrenzung, das diese Nachricht zuto
(Kanal) geschickt geworden sein musste. FROM(pat, to)
- Wurde ein Ereignis von jemanden Ausgelöst, in dessen
Nickname das Muster
pat
zu erkennen ist? Ebenfalls eingrenzbar in Einflussbereich durchto
. JOINED(pat, to)
- Ist ein Nutzer, dessen Nickname mit
pat
Musterüberstimmungen enthällt gematcht? Ebenfalls eingrenzbar in Einflussbereich durchto
. MENTIONED(to)
- Wurde der Bot direkt Angesprochen (opt.: im Kanal
to
).
4.2 Öffentliche Variablen
irc_autojoin_invite
- hat diese Variable einen nicht-nil Wert,
betritt der Bot jeden Kanal sobald eine
INVITE
Nachricht hierfür empfangen wurde. irc_rooms
Dieses 2D Feld enthält Metadaten über Kanäle. Hierbei gibt die erste Stufe den Kanalnamen (eg.
#linux
), die zweite Stufe die Information (topic
) an.Derzeit wird diese Variable Kaum genutzt.
4.2.1 Hillfsvariablen für Ereignisse
TO
- In dieser Variable steht in welchem Kanal das jetzige Ereignis gerichtet gewesen war. Es kann auch den Namen des Bots entsprechen, falls dieses eine private Nachricht enthalten hat.
WHO
- Die Variable
WHO
sollte nur den Nickname des Nutzers enthalten, von dem aus das Ereignis ausgelöst wurde. MSG
- Hier enthalten sind „gewöhnliche“ Nachrichten, wie etwa
durch
PRIVMSG
überbracht werden. Hat diese Variable einen wahren Wahrheitswert, hat der Bot eine Text-Nachricht erhalten. EMSG
(Event Message)- Falls eine Nachricht existiert, diese aber
nicht direkt textueller Natur ist (bspw.
PART
-ing Grund) wird dieses inEMSG
gespeichert. Es ist daher nachzuvollziehen, dassEMSG
kein konkretes Ereignis symbolisieren kann, sondern nur eine auxiliäre Funktion hat. NOTICE
- Analog zu
MSG
, nur fürNOTICE
anstattPRIVMSG
Nachrichten. INVITE
- In
INVITE
sollte der Name des Kanals stehen, in das der Bot eingeladen wurde, falls zu diesem Ereignis eine Einladung stattfand. JOIN
,PART
,QUIT
,KICK
- all diese Variablen sind Wahrheitswerte
welche für die analog Benannten IRC Befehle stehen. Sie werden
jeweils einen wahren Wahrheitswert annehmen, wenn zu diesem
Ereignis in irgendeinem Kanal irgendjemand ge-
JOIN
-t, ge-PART
-et, ge-QUIT
-et oder ge-KICK
-t wurde.
4.3 Anmerkungen zu „Nicht-Öffentliche“ Werten
Da AWK lediglich ein einfaches Namensraumsystem besitzt, können die
internen Mechanismen nicht vor dem Bibliotheksnutzer versteckt
werden. Daher wurde die Konvention benutzt alle „internen“ Variablen
und Funktionen mit __irc_...
zu Präfixieren. Diese sollten von
Nutzern, wenn möglich, nicht direkt genutzt werden, da dadurch der
interne Zustand durcheinander geraten könnte, aber auch kein Verlass
ist auf Stabilität.