Mittwoch, 29. Mai 2013

IRC-Bot mit NodeJS

Viele Admins und auch andere sind immernoch im IRC unterwegs. Schön wäre es doch, wenn ein Bot einem die Arbeit abnimmt, verschiedene Webrequests vornimmt und dann das Ergebnis in einen Channel postet. So kann man frei von E-Mails sich nach eigenen Vorlieben über bestimmte Zustände informieren lassen.

Was wird erstellt?

Ein IRC-Client, der auch bestimmte Befehle reagieren kann. Dieser ruft eine Webseite auf und liefert das Ergebnis als Chatnachricht zurück.

Erkenntnisse

Funktionsweise vom IRC. Einblicke in NodeJS als Server-Client-Gespann.

Vorarbeiten

Der Client verbindet sich über eine gesicherte Verbindung (SSL) mittels TLS.

Wir brauchen:
  • NodeJS (installiert oder auch nur die ausführbare Datei)
    http://nodejs.org/download/
  • IRC Account auf einem Server
  • Webseite, die einem was interessantes liefern

Konstrukt

Hier sind einige Elemente verbaut.
Mehrer Hilfsfunktionen und viele anonyme Funktionen.
Das Konstrukt soll auch zeigen, was alles möglich mit Javascript ist.

Zuerst generieren wir uns ein Objekt mit den Server-Zugangsdaten.
Der ganze Server ist einer Funktion eingebettet.

Die Hilfsbereiche sind zum Teil direkt in der Daten-Funktion eingebunden und zum Teil in einem Hilfsobjekt ausgelagert. Das soll zeigen, dass mehrer Methoden möglich sind.

Ablauf

  • Verbindung zum Server
  • Login
  • Registrieren von Event-Handler für den eingehenden Datenverkehr
  • Auswerten der eingehenden Texte
    • Aufrufen von entsprechenden Funktionen
    • Antworten in den Chat senden

Code

 var config = {  
      user: {  
           nick: 'sasobotli',  
           user: 'Saso_T_2013',  
           real: 'saso Bot - SN2013',  
           pass: '20cC12#'  
      },  
      server: {  
           addr: 'testirc.MEINSERVERNAME.de',  
           port: 6697  
      },  
      chans: ['#team-chat']  
      //chans: ['#chan1','#chan2']   
      , keywords:{}  
 }  
 // http://www.ietf.org/rfc/rfc1459.txt  
 irc3(); // startet den IRC-Bot  
 function irc3() {  
      // SSL Verbindung  
      var tls = require('tls');   
      var cleartextStream = tls.connect(  
                config.server.port,   
                config.server.addr,   
                null,  
                // anonyme Funktion, die das Anmelden auf dem Server macht   
                function(){  
                     irc.stream = cleartextStream;  
                     // ein paar Log-Ausgaben, um mehr zu verstehen   
                     console.log("client connected mit "+  
                          cleartextStream.remoteAddress+":"+  
                          cleartextStream.remotePort);  
                     console.log("meine adresse: ",   
                          cleartextStream.address());  
                     console.log('client connected',   
                          cleartextStream.authorized ? 'authorized' : 'unauthorized');  
                     // nun senden wir unsere Zugangsdaten  
                     irc.senden('PASS '+config.user.pass);  
                     irc.senden('NICK '+config.user.nick);  
                     irc.senden('USER '+config.user.user+  
                          ' localhost '+config.server.addr+  
                          ' '+config.user.real);  
                     // in alle channels anmelden  
                     for (var a=0;a<config.chans.length;a++) {       
                          irc.senden('JOIN '+config.chans[a]);  
                     }            
                     // kommandozeile einbinden,   
                     // so können wir auch direkt mit dem Server sprechen :)   
                     process.stdin.setEncoding('utf8');  
                     process.stdin.pipe(cleartextStream);  
                     process.stdin.resume();  
                }  
           );  
      // das Encoding setzen  
      cleartextStream.setEncoding('utf8');  
      // nun registrieren wir eine Funktion,  
      // die auf eingehende Daten reagieren soll  
      cleartextStream.on('data', function(data){  
           console.log("incoming");  
           // daten zeilenweise abarbeiten  
           data = data.split('\n');  
           for (var i = 0; i < data.length; i++) {  
                // leere zeilen überspringen  
                if (data[i].trim() == "")  
                     continue;  
                console.log('RECV -', data[i]);  
                if (data !== '') {  
                     // anforderungen / request vom Server sich zu melden  
                     var pos = data[i].indexOf(":");  
                     var key = data[i].substr(0,pos).trim().toUpperCase();  
                     var value = data[i].substr(pos+1).trim();  
                     if (key != "") {  
                          switch (key) {                      
                               case "PING":  
                                    irc.senden('PONG :'+value);       
                                    break;  
                               default:   
                                    console.log("Nicht abgefangen: "+key);  
                          }  
                     } else {  
                          // antwort oder info vom Server  
                          var teile = value.split(" ");  
                          //console.log(teile);  
                          switch(teile[1]) {  
                               case '353': // name reply, für NAMES,   
                                    // also alle Chatter in einem Raum erhalten  
                                    //console.log("Namen eingetroffen");  
                                    var namen = value.split(":")[1].trim().split(" ");  
                                    //console.log("gefunden: "+namen.length, namen);  
                                    for (var a=0;a<namen.length;a++){  
                                         var oname = namen[a].replace(/@/,"");  
                                         var admin = false;  
                                         if (namen[a].match(/@/))  
                                              admin = true;  
                                         // irc ist unten als Objekt definiert  
                                         irc.users[oname] = {'name':oname, 'admin':admin, 'nachrichten':[]};  
                                    }  
                                    //console.log(irc.users);  
                                    break;  
                               case 'JOIN': // Raum beigetreten  
                                    var raum = teile[2].substr(1);  
                                    // ein paar Begrüssungen  
                                    var sp = ['Hallo', 'Hey', 'hi', 'Wie gehts?',   
                                         'huhu','Toll, dass du da bist'];  
                                    // eine Begrüssung per Zufall auswählen  
                                    var pos = Math.round(Math.random()*(sp.length-1));  
                                    // begrüssen  
                                    irc.senden("PRIVMSG "+raum+" "+sp[pos]);  
                                    // alle Namen im Raum abrufen  
                                    irc.senden("NAMES "+raum);  
                                    break;  
                               case 'PRIVMSG':  
                                    // nachricht eingetroffen  
                                    // wir speicher wann zuletzt was gesagt wurde   
                                    irc.zaehler.lastword = time();  
                                    // ermittel ob direkt oder raum chat                                     
                                    var sender = teile[0];  
                                    var empfaenger = teile[2];  
                                    var user_sender = sender.split("!")[0];  
                                    var antwort = teile.slice(3)  
                                              .join(" ").trim()  
                                              .substr(1); // ohne führendes :  
                                    // wir speichern den User in unsere Liste  
                                    if (!irc.users[user_sender]) {   
                                         irc.users[user_sender] = {  
                                              'name':user_sender,   
                                              'admin':false,   
                                              'nachrichten':[], keywords:{}};  
                                    }  
                                    // speichern die Nachricht in seine History  
                                    irc.users[user_sender].nachrichten.push(antwort);  
                                    //console.log("nachricht eingegangen. Von "+sender, antwort);  
                                    // wurden wir direkt angesproche? also privatchat?  
                                    if (empfaenger.toLowerCase() ==   
                                              config.user.nick.toLowerCase()) {  
                                         var meineantwort = "danke, selber";  
                                         // senden unsere Antwort  
                                         irc.senden('PRIVMSG '+user_sender+" "+meineantwort);  
                                    } else {  
                                         // channel message eingetroffen  
                                         //irc.senden('PRIVMSG '+empfaenger+" Roger that");  
                                         console.log("wuerden senden: "+'PRIVMSG '+empfaenger+" Roger that");  
                                    }  
                                    // hier können wir nun das Gesagt analysieren und   
                                    // dementsprechend antworten  
                                    // hat jemand geschrieben: sag was,   
                                    // starten wir den Sprüchemodus   
                                    if (antwort.toLowerCase() == "sag was"){  
                                         console.log("sprueche intervall modus aktiv");  
                                         // einen Spruch holen und senden  
                                         irc.sagwas();  
                                    }  
                                    if (antwort.toLowerCase() == "sag sofort was"){                                          
                                         irc.sagwas(true);  
                                    }  
                                    if (antwort.toLowerCase() == "stopp"){  
                                         console.log("sprueche modus deaktiviert");  
                                         irc.zaehler.aktiv = false;  
                                    }  
                                    if (antwort.toLowerCase() == "saso start"){  
                                         console.log("sprueche modus aktiviert");  
                                         irc.zaehler.aktiv = true;  
                                    }  
                                    // wenn jemand fragt, woher die Sprüche kommen  
                                    // geben wir die URL als Antwort  
                                    // diese Bereich kann man gut ausbauen und   
                                    // dem Bot Befehle zum Auswerten von anderen Diensten   
                                    // geben. So dass er die Ergebnisse hier postet  
                                    if (antwort.toLowerCase().match(/woher\s+sind\s+die/)){  
                                         irc.senden("PRIVMSG "+empfaenger+" http://sprueche.woxikon.de");  
                                    }  
                                    // wenn jemand die History haben will ...  
                                    if (antwort.toLowerCase().match(/history:/)) {  
                                         // könnten wir hier diese auswerten und posten...  
                                    }  
                                    break;  
                          }  
                     }  
                }  
           }  
      });  
      cleartextStream.on('end', function(){  
           console.log("session beendet");  
           process.exit(0);  
      });       
      // Objekt, dass uns als zwischenspeicher dient  
      // und auch hilfsfunktionen enthält  
      var irc = {  
           zaehler: {  
                'aktiv':false,  
                'lastword':time()  
                },  
           stream: null,  
           // sendet die Nachrichten an den IRC-Server  
           senden: function(text) {  
                console.log("SEND -", text);  
                this.stream.write(text+'\n');  
                if (text.match(/^JOIN/i)) {  
                     // hole die benutzer  
                     this.senden("NAMES"+text.substr(4));  
                }       
           },  
           // hilfsfunktion, die später für das Chatten genutzt wird  
           sagwas: function(force){  
                if (!force) {  
                     // zufall, wann wieder ein neuer Spruch gesagt wird  
                     var nextcall = Math.round(Math.random()*(10))+3; // minuten  
                     setTimeout(irc.sagwas, nextcall*1000*60);  
                     console.log("nächster spruch ca. in "+nextcall+" minuten");  
                }   
                // nur wenn vor 2 minuten niemand was gesagt hat  
                if (!force && irc.zaehler.lastword > time()-(2*1000*60))  
                     return;  
                if (!force && !irc.zaehler.aktiv)  
                     return;  
                irc.zaehler.lastword = time();  
                // ein paar sprüche seiten  
                var urls = [  
                     {'url':'http://sprueche.woxikon.de/poesiealbum','satz':'Das hier find ich gut:'},  
                     {'url':'http://sprueche.woxikon.de/emo', 'satz':'kennt ihr den?'}];                      
                var pos = Math.round(Math.random()*(urls.length-1));  
                var spruchurl = urls[pos];  
                // hier holen wir die Sprüche ab und   
                // senden diese dann direkt als Message   
                URLLaden(spruchurl.url, function(html) {  
                     // suchen im HTML nach den Sprüchen  
                     var treffer = html.match(/<div\s+class="jingle-content">([\S\s]*?)<\/div>/gi);  
                     var sprueche = [];  
                     for (var a=0;a<treffer.length;a++) {  
                          //console.log(treffer[a]);  
                          sprueche.push(treffer[a].replace(/(<\/?[^>]+>)/gi, ''));  
                     }  
                     // wenn es einige Treffer gibt, wählen wir per zufall einen aus  
                     if (sprueche.length>0) {  
                          //console.log(sprueche);  
                          var pos = Math.round(Math.random()*(sprueche.length-1));  
                          var spruch = sprueche[pos].trim().replace(/\s/," ");  
                          //console.log("kennt ihr den? "+spruch);  
                          irc.senden('PRIVMSG '+config.chans[0]+" "+spruchurl.satz+" "+spruch);  
                     }  
                });                                               
           },  
           users: {}  
      }  
 }  
 function time(onlyseconds) {  
      var datum = new Date();  
      var milliseconds = datum.getTime();  
      if (onlyseconds)  
           return intval(milliseconds/1000);  
      return milliseconds;  
 }  
 // diese Funktion, lädt eine HTML Seite  
 function URLLaden(url, cbfkt) {       
      if (url.match("://")) {  
           var http;  
           if (url.match("s://")){  
                http = require('https'); // die URL verlangt SSL  
           } else {  
                http = require('http');  
           }  
           http.get(url, function(res) {  
            //console.log("Got response: " + res.statusCode);  
            var html = "";  
            res.on("data", function(chunk){  
                 html += chunk; // inhaltteile der Seite zusammen kleben  
            });  
            res.on("end", function(){  
                 // alle Teile sind angekommen und wir haben die HTML-Seite  
                 // cbfkt ist eine anonyme Fkt, die wir aufrufen  
                 // als ergebnis, senden wir das HTML  
                 cbfkt(html);   
            });  
            res.on("close", function(err) {  
                 // verbindung beendet  
                 cbfkt(html);  
                 console.log("Error holen "+url);  
            });  
           }).on('error', function(e) {  
                // verbindung getrennt  
            console.log("Got error: " + e.message);  
           });  
      } else {  
           // falls die URL auf eine lokale Datei zeigt, laden wir diese halt  
           fs.readFile(url, function(err, data){  
                if (err){  
                     console.log(err);  
                } else {  
                     cbfkt(data);  
                }  
           });             
      }  
 }  

Weiterführende Lektüre:

Fazit

So kann man sehr schnell einen eigenen Überwachungsserver schreiben. Dieser meldet einfach den Status im IRC-Chat. Natürlich kann man das beliebig aufbohren. Man sieht aber wieder, wie schnell und unkompliziert man mit NodeJS Lösungen erstellen kann.

Viel Spass

Saso

1 Kommentar:

  1. IRC Server aufsetzen incl. SSL-Verschlüsselung aufsetzen: http://sebastianhemel.blogspot.de/2013/05/eigenen-irc-server-aufsetzen.html

    AntwortenLöschen