-+  Associazione
-+  Documenti
 |-  Modern Perl
 |-  Bibliografia
 |-  Articoli
 |-  Talk
 |-  Perlfunc
 |-  F.A.Q.
 |-  F.A.Q. iclp
-+  Eventi
-+  Community
-+  Blog
-+  Link
Corso di Perl



 

Emiliano Bruni
Verificare la presenza di un nick su IRC

1. Genesi 

2. La ricerca della soluzione 

3. Prepariamo l'ambiente 

4. Sviluppiamo il programma 

Genesi

Sono spesso sulla rete IRC di Azzurra, e quindi sono spesso rintracciabile lì dai miei vari amici e conoscenti. Mi sono detto allora, ma se qualcuno viene sul mio sito e mi vuole fare una domanda banale, perchè costringerlo a mandarmi un mail o a scrivere sul mio forum? Perchè non farlo entrare direttamente su IRC per contattarmi? Ovviamente la cosa più banale era quella di mettere, da qualche parte, l'informazione che io sono spesso rintracciabile su certi canali e che il mio nickname era emi.

Questa sarebbe stata una soluzione banale, ma si sa, noi programmatori dobbiamo sempre complicarci la vita, per cui mi sono chiesto se non era possibile creare qualcosa che dicesse immediatamente al visitatore se in quel momento ero online e su quale canale mi trovavo. Da questo ragionamento è nato questo semplice script, irc_check.pl, le cui informazioni vengono poi utilizzate per creare una sezione "Parla con me" sul mio sito web, che indica quando sono online e quando no, come potete vedere nello screenshot accanto.

 La ricerca della soluzione

Per interfacciarsi con IRC usualmente sarebbe necessario andarsi a guardare gli RFC del protocollo ossia quei documenti che definiscono gli standard di Internet e, in questo caso, le specifiche con cui un client IRC e un server IRC debbono dialogare. In realtà questo problema è risolto egregiamente dal nostro grande amico: il CPAN.

Il modulo Net::IRC è la soluzione perfetta per la scrittura di qualsiasi automatismo sia necessario implementare su IRC.

Risolto il problema del protocollo, rimaneva il dilemma della modalità con cui avrei verificato la presenza del mio nick. La prima idea, quella di creare un bot, è stata subito scartata sia per non incappare nelle politiche restrittive del network di azzurra che vieta i bot non autorizzati, sia perchè mi sembrava proprio esagerato creare un bot e occupare quindi risorse solo per verificare lo stato del mio nick.

Tenendo conto del fatto che non era proprio necessario una verifica istantanea della mia presenza su IRC e che era quindi possibile sopportare una certa tolleranza nel ritardo tra un check e l'altro, ho deciso di fare uno script da lanciare in cron ogni tot minuti che simulasse l'ingresso di un utente sulla rete IRC e che eseguisse le seguenti operazioni:

  1. connessione alla rete di azzurra

  2. verifica della presenza del nick emi tramite il comando ISON

  3. se il nick non viene trovato, disconnettersi dalla rete

  4. recupero dei canali su cui il nick è presente tramite il comando WHOIS

  5. recupero dei topic di ognuno di questi canali tramite il comando LIST

  6. disconnessione dalla rete

Prima di terminare il programma deve, in qualche modo, creare un report dello stato del nick emi di modo che sia possibile interrogare questo report per stampare, per esempio su una pagina web, lo stato di online o offline del nick.

 Prepariamo l'ambiente

L'unica cosa di cui abbiamo bisogno sul computer è un interprete Perl e il modulo Net::IRC. Per quest'ultimo basta aprire una shell di sistema e digitare il comando

[root@K7 www]# perl -MCPAN -e 'install Net::IRC'

che scarica e installa automaticamente il modulo. Oppure è possibile scaricare manualmente il sorgente, decomprimerlo, compilarlo e installarlo con la procedura

[root@K7 www]# wget 
http://search.cpan.org/CPAN/authors/id/J/JM/JMUHLICH/Net-IRC-0.75.tar.gz
[root@K7 www]# tar zxvf Net-IRC-0.75.tar.gz
[root@K7 www]# cd Net-IRC-0.75
[root@K7 www]# perl Makefile.PL
[root@K7 www]# make
[root@K7 www]# make test
[root@K7 www]# make install

 Sviluppiamo il programma

Iniziamo allora a vedere, riga per riga, il nostro programma.

1    #!/usr/bin/perl

Iniziamo ovviamente con la dichiarazione iniziale che questo è uno script in Perl. Se questo script ha i permessi di esecuzione, il sistema userà il compilatore presente in /usr/bin/perl per compilare ed eseguire lo script. Questa prima riga viene, in gergo, detta she-bang, dal nome dei primi due caratteri che la compongono (contrazione di sharp-bang).

Le righe 2..4 caricano tre librerie nel programma.

2    use strict;

Questa, benchè richiamata sintatticamente come un modulo, è in realtà una direttiva del Perl che, attivando alcuni controlli in fase di compilazione, costringe il programmatore ad essere più attento nella scrittura del codice. Per esempio, per poter utilizzare una variabile è obbligatorio prima dichiararla. Questo aiuta, per esempio, a evitare errori di battitura nello scrivere le variabili. Questi, di solito, non producono errori in fase di compilazione ma in fase di esecuzione e non sono di immediata soluzione.

Per esempio, senza questa direttiva il codice seguente viene correttamente compilato ed eseguito ma il risultato non e' quello che ci si aspetta

$pippo = 5;
print $pipo;

Aggiungendo la direttiva strict il programma produce un errore nella riga di inizializzazione della variabile $pippo, in quanto essa non è precedentemente definita. Riscrivendo il programma in questo modo:

use strict;
my $pippo;
$pippo = 5;
print $pipo;

immediatamente ci si accorge dell'errore di digitazione nell'ultima riga in quanto lì il programma produce un errore essendo la variabile $pipo non precedentemente dichiarata.

3    use Net::IRC;

Questo modulo implementa la classe che incapsulerà la sintassi del protocollo IRC permettendoci di instaurare un dialogo con un server IRC e eventualmente con i canali e i client della rete senza sapere come, in realtà, funziona il protocollo IRC. Vedremo nelle righe 20..24 come si inizializza un oggetto di questa classe.

4    use Data::Dumper;

Cos'è questo modulo che siamo andati a caricare? Il modulo Data::Dumper è uno dei più utili del Perl. Questo modulo prende una variabile scalare e restituisce il contenuto della variabile. Dato che in Perl una variabile scalare è praticamente tutto, da un numero, ad una stringa, ad un riferimento ad un array, ad un riferimento ad un hash, ad una classe, questo modulo restituisce una versione "visiva" dei dati della variabile.

Facciamo un esempio e prendiamo uno scalare contenente un riferimento ad un array cosi fatto

$integers = [1,2,3,4,5,6,7];

Se usiamo il modulo Data::Dumper e ne stampiamo il risultato esso sarà

$VAR1 = [1,2,3,4,5,6,7];

che, a parte il nome della variabile, è proprio il contenuto della stessa.

Va da se che questo modulo è utilissimo in fase di debug in quanto ci permette di vedere, in qualsiasi momento, il contenuto di una qualsiasi struttura e quindi i valori in essa contenuti.

Ma, la cosa bella è che questo modulo può essere tranquillamente usato per serializzare una variabile ossia scrivere questa variabile in un qualche posto, tipo un file, per riutilizzarla successivamente inalterata, magari in un altro programma. Questo è possibile perchè quello che il Data::Dumper stampa altro non è che il codice Perlnecessario a inizializzare una variabile che ha gli stessi valori della variabile sorgente e questo è proprio la definizione di serializzazione di una variabile. Mentre altri linguaggi usano varie forme, per esempio binarie, per la serializzazione, il Perl (come ad esempio anche il Lisp) può serializzare le variabili generando il codice necessario a definirle e quindi la funzione di deserializzazione in Perl, altri non è se non la funzione eval() che elabora una stringa di caratteri come se fosse codice Perl.

Se facciamo

eval(Data::Dumper::Dumper($integers))

non avremo fatto altro che definire una nuova variabile $VAR1 che è, come valori e tipo, identica a $integers. Vedremo nella funzione output_log alla riga 64 e successive il caso reale di utilizzo di questo modulo per la serializzazione dei dati applicato al nostro esempio.

5    use vars qw($find_user $output $status);

Questo ulteriore modulo è, in realtà, un'altra direttiva che definisce una o più variabili con visibilità globale ossia delle variabili definite in tutte le parti del programma, sia nel package main che nelle sue subroutine. In parole più semplici, la variabile $find_user, che identificherà il nickname da trovare, è definita in ogni punto del programma ed avrà sempre lo stesso valore.

La seconda variabile $output definirà il percorso del file dove andremo a serializzare lo stato del nick.

Queste prime due variabili in realtà, avrebbero potuto avere validità locale nell'ambito delle singole funzioni in cui sono coinvolte. Per esempio, $output serve solo nella funzione output_log e avrebbe potuto essere definita solo in quel contesto. Si è scelto una tipo globale in modo da definire queste variabili diciamo di "configurazione" tutte nello stesso punto all'inizio del programma (linee 10 e 11)

L'ultima variabile globale $status rappresenterà lo stato del nostro nick e sarà quella che dovrà esssere serializzata. Questa è una "vera" variabile globale in quanto sara inizializzata nel main (linea 12) ma ulteriori informazioni verranno aggiunte alla variabile durante le varie chiamate su IRC (linee 40, 42 e 59) e infine essa verrà serializzata in 67.

E qui si vede l'utilità di una variabile globale. Il fatto che in 67 troviamo la variabile con i dati che sono stati valorizzati nel corso dell'esecuzione del programma è dovuto solo ed esclusivamente al fatto che questa sia stata definita come globale.

6    my $irc_server     = 'irc.azzurra.org';
7    my $irc_port       = 6667;
8    my $irc_nick       = 'emi_finder';
9    my $irc_name       = 'Emi, r u there?';
10    $find_user         = 'emi';
11    $output            = '/www/ebruni.it/www/irc_status.txt';

In queste righe definiamo e inizializziamo alcune variabili di configurazione. L'utente che utilizzerà il nostro script dovrà solo modificare questa sezione per adattarla alla propria situazione. Il resto del programma non dovrebbe essere toccato.

Queste variabili, di ovvio significato, definiscono rispettivamente il server IRC della nostra rete preferita, la porta del server, il nickname utilizzato da questo script quando si collegherà alla rete e la sua descrizione.

Si valorizzano quindi le due variabili globali del nickname da monitorare e il file per la serializzazione.

12    $status           = {timestamp => scalar(localtime)};

Qui iniziamo a valorizzare la variabile che conterrà, alla fine, i dati sullo stato del nostro nick. La variabile è inizializzata come un riferimento ad un hash '{}' di elementi chiave => valore. Come primo, aggiungiamo un elemento chiamato timestamp con l'orario in cui lo script è stato eseguito. Questo ci servirà, eventualmente, per sapere in che momento è stato lanciato questo script l'ultima volta e quindi in che momento è stata fatta la rilevazione del nostro nick e quindi a  che momento risalgono i dati rilevati.

13    my %event_lookup  = (
14                          'endofmotd'        => 376,
15                          'ison'             => 303,
16                          'whoischannels'    => 319,
17                          'list' => 322,
18    );

Qui c'è l'unica parte relativa al protocollo IRC. Ogni messaggio server-to-client è definito da un numero che identifica il tipo di messaggio. Qui identifico una variabile di ricerca che mi permetterà di usare dei nomi invece che dei numeri.

Per esempio, la fine del MOTD (message of the day) è identificato dal numero 376 ma da ora posso ora anche usare la variabile $event_lookup{'endofmotd'} al posto del numero.

19    my $irc = new Net::IRC;
20    my $conn = $irc->newconn(
21                                Nick     => $irc_nick,
22                                Server   => $irc_server,
23                                Port     => $irc_port,
24                                Ircname  => $irc_name);

Inizializzo il client IRC. Queste righe corrispondono a lanciare un normale client IRC con GUI tipo Xchat e configurare i suoi parametri per entrare in IRC.

25    $conn->add_global_handler($event_lookup{endofmotd}, \&on_connect);
26    $conn->add_handler($event_lookup{ison},\&on_ison_reply);
27    $conn->add_handler($event_lookup{whoischannels},\&on_whois_reply);
28    $conn->add_handler($event_lookup{list},\&on_list_reply);

Dato che a rispondere ai messaggi di IRC non ci sarà un uomo ma una macchina, devo istruire il programma su cosa dire in certi determinati eventi. In particolare ci interesserà gestire risposte agli eventi di tipo endofmotd, ison, whoischannels e list.

Vedremo a breve perchè proprio questi ma ora ci preme osservare come, a fronte dell'esecuzione di un dato evento, sia stata associata l'esecuzione di una data function che dovrà gestire l'evento.

29    $irc->start;

Ci colleghiamo a IRC.

30    sub on_connect {
31        my $self = shift;
32        $self->ison($find_user);
33    } 

Quando ci colleghiamo ad IRC con un client tipo XChat, vediamo che, prima di poter entrare in un canale, il server ci manda tutta una serie di informazioni. Una delle ultime informazioni è il MOTD che viene terminato dall'END OF MOTD.

Come definito nella riga 25, al ricevimento da parte del nostro client dell'END OF MOTD deve essere eseguita la funzione on_connect. A questa funzione viene passato un parametro che contiene un istanza del nostro client IRC che incapsula i metodi per comunicare con il server IRC.

Dato che, se siamo dentro a questa funzione, ci possiamo tranquillamente considerare connessi al server IRC, passiamo allo step 2 del flusso del nostro programma definito sopra e andiamo ad eseguire il comando ISON (linea 32 ).

34    sub on_ison_reply {
35        my $self = shift;
36        my $event = shift;
37        if ($event->{args}->[1] eq $find_user . " ") {
38            # user on line
39            $self->whois($find_user);
40            $status->{online} = 1;
41        } else {
42            $status->{online} = 0;
43            $self->quit;
44        }
45    } 

Una volta che il server ha risposto al nostro comando ISON, siamo dentro alla funzione on_ison_reply. Cosa ci restituisce il server a fronte di una richiesta ISON? Come potete vedere il risultato è il nick stesso richiesto se l'utente con quel nick esiste o una risposta 303 senza nick nel caso esso non sia presente nella rete.

La risposta del server, lato script, la troviamo nel secondo parametro passato alla funzione che abbiamo salvato nella variabile $event. Un Data::Dumper (vedete l'utilità :-), di questa variabile risultata essere:

$VAR1 = bless( {
                'nick'     => 'caltanet.azzurra.org',
                'host'     => '',
                'user'     => '',
                'from'     => 'caltanet.azzurra.org',
                'userhost' => '@',
                'format'   => 'server',
                'args'     => [
                                'emi_finder',
                                'emi '
                              ],
                'type'     => 'ison',
                'to'       => [
                                ''
                              ]
}, 'Net::IRC::Event' );

Come potete vedere questa variabile è un po' complessa: proviamo ad analizzarla ugualmente. Essa risulta essere un oggetto: bless assegna infatti un hash di proprietà ad una interfaccia che, in questo caso, scopriamo essere di tipo Net::IRC::Event. Leggiamo anche le proprietà valorizzate per questo oggetto: quello che ci interessa ora è la chiave args, che osserviamo essere un riferimento ad un array di elementi che non sono altro che uno split della riga di risposta del server (confrontatela con la risposta "raw" del server)

Se l'utente non esiste, ossia se l'elemento con indice 1 della chiave args dell'oggetto $event è vuoto, impostiamo allora la chiave "online" della variabile di stato a 0 e usciamo (linea 42 e 43), altrimenti impostiamo questa chiave a 1 e chiediamo al server di ottendere l'WHOIS del nickname (linea 39 e 40).

46    sub on_whois_reply {
47        my $self = shift;
48        my $event = shift; 

In risposta alla nostra richiesta di WHOIS il server ci risponde con tutta una serie di informazioni sul nick. Tra tutte queste risposte, l'unica che ci interessa è la risposta 319 che contiene l'elenco dei canali su cui l'utente è presente.

Come il nostro client riceve questa riga di risposta, in accordo con la nostra richiesta alla riga 27, viene eseguita la funzione on_whois_reply.

49     my @visible_channels = split(/ /,$event->{args}->[2]);

Anche in questo caso con un Data::Dumper stavolta della variabile $event->{args}

$VAR1 = [
            'emi_finder',
            'emi',
            '@#controguerra @#areanetworking @#geeks @#webgui @#pescaralug @#telug '
];

si scopre che la lista che ci interessa si trova nell'indice 2 dell'array per cui separandola in base agli spazi (split) si otterrà un array con il nome di un canale in ogni elemento.

50     foreach (@visible_channels) { 

Per ogni canale

51         s/^@//; 

rimuoviamo l'eventuale "@" presente su quei canali in cui l'utente è operatore ma che, per i nostri scopi, non è una informazione interessante

52         $self->list($_);
53     }

e, per avere informazioni sul canale, ne richiediamo il LIST.

54     $self->quit;
55 } 

Una volta eseguita questa serie di LISTsu tutti i canali, usciamo.

56 sub on_list_reply {
57     my $self = shift;
58     my $event = shift; 

A fronte di ogni comando LIST eseguito sopra il server ci rispondera ripetendo il nome del canale e il suo topic in un messaggio con codice 322 che, in accordo con quanto da noi specificato, verrà processato dalla funzione on_list_reply.

59     $status->{channels}->{$event->{args}->[1]} = $event->{args}->[3];
60 } 

Non perdiamoci proprio ora. Questa è la riga più complicata di tutto il programma ma solo perchè su di una unica riga sono state eseguite più operazioni. E' questo uno dei punti a favore del Perl.

Vediamo di scrivere questa riga in forma esplicita:

my $channels = $status->{channels};
my $channel_name = $event{args}->[1];
my $channel_topic = $event{args}->[3];
$channels->{$channel_name} = $channel_topic;

Quindi, prendiamo l'elemento con chiave channels della variabile di stato, recuperiamo l'elemento 1 e 3 dell'array di risposta del server e creiamo una chiave su $channels con chiave uguale al nome del canale e valore il topic.

Osservate che, alla prima chiamata della sub on_list_reply nell'hash $status non esiste alcuna chiave channels quindi $channels non  è definita. Mancherebbe quindi una riga in cui si definisce che $channels è un riferimento ad un hash

$channels = {}

ma questa cosa viene implicitamente fatta dalla associazione del topic con il nome del canale. Ovviamente, una volta che $channels, ossia $status->{channels}, è stata definita, se si tenta di valorizzare qualcosa che non sia una chiave di un hash si ottiene un errore. Per esempio, se dopo la definizione implicita di $channels come hashref si tenta di fare qualcosa del genere

$channels->[0] = ...

si otterrebbe un errore.

61 sub END {
62     &output_log;
63 } 

Alla riga 43 o 54 il programma, chiusa la connessione, uscirebbe. Ma ci manca ancora di scrivere la variabile di stato da qualche parte per poterla utilizzare successivamente.

A questo pensa la funzione END. Questa viene chiamata automaticamente dal Perl prima della "morte" del programma e prima che le variabili escano dall'ambito di validità. Qui eseguiamo la funzione che avrà il compito di scrivere il nostro report.

64 sub output_log {
65     $Data::Dumper::Varname = 'irc_status'; 

Il parametro $Varname presente nel namespace Data::Dumper definisce il nome della variabile che verrà stampata come variabile a cui viene associato il dump. Vedete, negli esempi del Data::Dumper sopra che di default il suo valore e' 'VAR'. Noi gli diamo ora un valore più significativo.

66     open(OUTPUT,">$output"); 

Apriamo un handle di nome OUTPUT che punta, in scrittura (>) al file definito all'inizio nella variabile $output.

67     print OUTPUT Data::Dumper::Dumper($status); 

Stampiamo dentro a $output il dump della variabile $status

68     close(OUTPUT);
69 }

Chiudiamo il file e questo è tutto.

Per i più curiosi, ecco un print della variabile $status durante una mia sessione IRC

$irc_status1 = {
            'channels' => {
                '#telug' => 'La chat del Teramo Linux Users Group.....',
                '#geeks' => 'Un canale pieno di buchi (geeks?!) ......',
                '#areanetworking' => ' Benvenuti su #areanetworking ! .',
                '#controguerra' => 'Gli abitanti di Controguerra, .....',
                '#webgui' => '',
                '#pescaralug' => 'PescaraLUG ....'
            },
            'online' => 1,
            'timestamp' => 'Sun Mar 27 15:01:06 2005'
};
ecco lo stesso print mentre non ero presente su IRC.

$irc_status1 = {
            'online' => 0,
            'timestamp' => 'Sun Mar 27 15:05:28 2005'
};

Nel prossimo articolo descriverò uno script basato sul modulo CGI che, utilizzando i dati presenti nel file $output, produce una pagina web che indica se siamo o non siamo online.

D:
Progetti e documenti in rilievo
Corso di Perl Progetto pod2it
D:
La ML di Perl.it
mongers@perl.it è la lista ufficiale di Perl Mongers Italia per porre quesiti di tipo tecnico, per rimanere aggiornato su meeting, incontri, manifestazioni e novità su Perl.it.
Iscriviti!
D:
Annunci Google