UP | HOME

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:

  1. Setup
  2. Kanäle Beitreten
  3. 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 als bot. Der Server soll irc.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:

  1. Kein BEGIN Block. irc_setup lässt sich nur einmal erfolgreich Aufrufen pro Durchlauf, und danach ignoriert es einfach alle Aufrufe sofort. Dieses bedeutet das irc_setup einfach auf eine Zeile gelegt werden kann (als Pattern), und es nur beim ersten Durchlauf ein Effekt hat.
  2. 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 von irc_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:

  1. Anstatt 0 wird "ssl" als Port-Argument übergeben. Dieses bedeutet das wir versuchen werden eine verschlüsselte Verbindung (Port 6697) aufzubauen.
  2. Das vierte Argument enthält lediglich ein !. Wenn die Autojoin Liste mit einem Ausrufezeichen anfängt, reagiert unser Bot auf INVITE 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. Wird 0 angegeben, ersetzt irc_setup dieses mit 6667, wird (als String) ssl angegeben, wird dieses mit 6697 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 auf INVITE anfragen reagieren, und sofort in den eingeladenen Kanal eintreten.
  • user und realname 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 in passwd angegeben werden.
irc_send(to, msg)
Diese Funktion schickt eine Nachricht msg an to, welches ein Nutzer oder auch Kanal sein darf.
irc_msg(msg)
Als Vereinfachung von irc_send, nimmt irc_msg immer als Wert von to 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 und len 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 zu to (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 durch to.
JOINED(pat, to)
Ist ein Nutzer, dessen Nickname mit pat Musterüberstimmungen enthällt gematcht? Ebenfalls eingrenzbar in Einflussbereich durch to.
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 in EMSG gespeichert. Es ist daher nachzuvollziehen, dass EMSG kein konkretes Ereignis symbolisieren kann, sondern nur eine auxiliäre Funktion hat.
NOTICE
Analog zu MSG, nur für NOTICE anstatt PRIVMSG 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.

Autor: Philip K.

Created: 15:15:46

Validate