Aus Linux-Magazin 10/2005

Messenger-Plugin verschafft Vorteil durch Webseiten-Überwachung (Seite 2)

Gaim springt daraufhin die im Plugin definierten Funktionen »buddy_signed_on_callback« und »buddy_signed_off_callback« an. Sie prüfen, ob der gemeldete Benutzername dem in Zeile 22 festgesetzten entspricht. Wenn dem so ist, speichert die Funktion »buddy_signed_on_callback« die Gaim-eigene Benutzerstruktur in der globalen »$BUDDY«-Variablen. Sie ist nötig, wenn später dem Benutzer eine Nachricht zu senden ist.

Abbildung 1: Neue Fragen sind eingetroffen. Jetzt hat der Ersthelfer Chancen, Punkte zu sammeln.

Abbildung 1: Neue Fragen sind eingetroffen. Jetzt hat der Ersthelfer Chancen, Punkte zu sammeln.

Eigenregie

Falls sich der Benutzer gerade anmeldet, setzt »buddy_signed_on_callback()« das Flag »$ACTIVE« auf 1, falls er sich abmeldet, setzt »buddy_signed_off_callback()« es auf 0. »$ACTIVE« steuert im jede Sekunde aufgerufenen »refresh()«-Callback (Zeile 124), ob tatsächlich eine POE-Zeitscheibe abläuft oder nichts passiert. Falls »refresh()« die POE-Methode »run()« aufriefe, würde diese nie mehr zurückkehren. Stattdessen ruft Zeile 130 »run_one_timeslice()« auf, die eine aufgestaute Task abhandelt und – ohne auf Events zu warten – zurückkehrt.

Da der Code in jeder Zeitscheibe nur einen kleinen Teil des Requests bearbeitet, kann der ganze HTTP-Request schon 20 »refresh()«-Zyklen dauern. Das spielt keine große Rolle. Wichtig ist nur, dass die CPU während des Callbacks nicht auf externe Ereignisse wartet, etwa die Antwort auf einen HTTP-Request. POE erledigt dies mit der Komponente POE::Component::Client::HTTP zuverlässig.

Abbildung 2: Gaim kommuniziert mit dem Perl-Plugin, das wiederum einen POE-Zustandsautomaten (Perl Object Environment) kontrolliert.

Abbildung 2: Gaim kommuniziert mit dem Perl-Plugin, das wiederum einen POE-Zustandsautomaten (Perl Object Environment) kontrolliert.

Der in Zeile 72 definierte Anfangszustand »_start« leitet nur den nächsten Zustand »http_start« ein. Dort startet die mit »ua« gekennzeichnete Komponente »POE::Component::Client::HTTP«, das, sobald das Ergebnis vorliegt, den Zustand »http_ready« anspringt. Bevor »http_start« sich beendet, beantragt es noch einen POE-Timeout 10 Minuten später, der wieder den Zustand »http_start« auslöst, um das nächste Mal die Webseite einzuholen.

Der Handler des Zustands »http_ready« erhält in »$_[ARG1]« eine Referenz auf ein Array, dessen erstes Element (ein Objekt vom Typ HTTP::Response) das Ergebnis des Webrequest speichert. Weitere Erläuterungen zu POEs etwas ungewöhnlicher Art der Übergabe von Parametern geben [3] und [4].

Um aus der Perlmonks-Webseite die Links und Texte der Questions-Sektion herauszufieseln, implementiert die Funktion »qparse« ab Zeile 190 einen HTML-Parser. Andere Sektionen, die mit den gesuchten Fragen nichts zu tun haben, etwa Discussion oder Meditations, soll Pmwatcher.pl ignorieren.

Listing 1:
»pmwatcher.pl«

001 #!/usr/bin/perl -w
002 ###########################################
003 # pmwatcher - Gaim plugin to watch
004 #             perlmonks.com
005 ###########################################
006 use strict;
007 use Gaim;
008 use HTML::TreeBuilder;
009 use URI::URL;
010 use CGI qw(a);
011 use Cache::FileCache;
012 use POE qw(Component::Client::HTTP);
013 use HTTP::Request::Common;
014 
015 our $FETCH_INTERVAL = 600;
016 our $FETCH_URL = "http://perlmonks.com/" .
017                  "?node=Newest%20Nodes";
018 
019 our $ACTIVE = 0;
020     # Call plugins every second
021 our $UPDATE = 1;
022 our $USER   = "mikeschilli";
023 our $BUDDY  = undef;
024 our $PLUGIN = undef;
025 
026 our %PLUGIN_INFO = (
027   perl_api_version => 2,
028   name        => "pmwatcher",
029   summary     => "Perlmonks Watch Plugin",
030   version     => "1.0",
031   description => "Reports latest postings "
032                  . "on perlmonks.com",
033   author      => "Mike Schilli " .
034                  "<m@perlmeister.com>",
035   load        => "plugin_load",
036 );
037 
038 our $cache = new Cache::FileCache({
039     namespace  => "pmwatcher",
040 });
041 
042 ###########################################
043 sub plugin_init {
044 ###########################################
045   return %PLUGIN_INFO;
046 }
047 
048 ###########################################
049 sub plugin_load {
050 ###########################################
051   my($plugin) = @_;
052 
053   Gaim::signal_connect(
054     Gaim::BuddyList::handle(),
055     "buddy-signed-on", $plugin,
056     &buddy_signed_on_callback,
057   );
058 
059   Gaim::signal_connect(
060     Gaim::BuddyList::handle(),
061     "buddy-signed-off", $plugin,
062     &buddy_signed_off_callback,
063   );
064 
065   POE::Component::Client::HTTP->spawn(
066       Alias     => "ua",
067       Timeout   => 60,
068   );
069 
070   POE::Session->create(
071     inline_states => {
072       _start     => sub {
073         $poe_kernel->yield('http_start');
074       },
075       http_start => sub {
076         Gaim::debug_info("pmwatcher",
077           "Fetching $FETCH_URLn");
078         $poe_kernel->post("ua", "request",
079             "http_ready", GET $FETCH_URL);
080         $poe_kernel->delay('http_start',
081                           $FETCH_INTERVAL);
082       },
083       http_ready => sub {
084         Gaim::debug_info("pmwatcher",
085           "http_ready $FETCH_URLn");
086         my $resp= $_[ARG1]->[0];
087         if($resp->is_success()) {
088           pm_update($resp->content());
089         } else {
090           Gaim::debug_info("pmwatcher",
091             "Can't fetch $FETCH_URL: " .
092             $resp->message());
093         }
094       },
095     }
096   );
097 
098   Gaim::timeout_add($plugin, $UPDATE,
099                     &refresh);
100   $PLUGIN = $plugin;
101 }
102 
103 ###########################################
104 sub buddy_signed_on_callback {
105 ###########################################
106   my ($buddy, $data) = @_;
107 
108   return if $buddy->get_alias ne $USER;
109   $ACTIVE = 1;
110   $BUDDY  = $buddy;
111 }
112 
113 ###########################################
114 sub buddy_signed_off_callback {
115 ###########################################
116   my ($buddy, $data) = @_;
117 
118   return if $buddy->get_alias ne $USER;
119   $ACTIVE = 0;
120   $BUDDY  = undef;
121 }
122 
123 ###########################################
124 sub refresh {
125 ###########################################
126 
127   Gaim::debug_info("pmwatcher",
128              "Refresh (ACTIVE=$ACTIVE)n");
129   if($ACTIVE) {
130       $poe_kernel->run_one_timeslice();
131   }
132 
133   Gaim::timeout_add($PLUGIN, $UPDATE,
134                     &refresh);
135 }
136 
137 ###########################################
138 sub pm_update {
139 ###########################################
140   my($html_text) = @_;
141 
142   if(my @nws = latest_news($html_text)) {
143       my $c = Gaim::Conversation::IM::new(
144                 $BUDDY->get_account(),
145                 $BUDDY->get_name());
146 
147       $c->send("$_n") for @nws;
148   }
149 }
150 
151 ###########################################
152 sub latest_news {
153 ###########################################
154   my($html_string) = @_;
155 
156   my $start_url =
157       URI::URL->new($FETCH_URL);
158 
159   my $max_node;
160 
161   my $saved = $cache->get("max-node");
162   $saved = 0 unless defined $saved;
163 
164   my @aimtext = ();
165 
166   for my $entry (@{qparse($html_string)}) {
167       my($text, $url) = @$entry;
168 
169       my($node) = $url =~ /(d+)$/;
170       if($node > $saved) {
171           Gaim::debug_info("pmwatcher",
172             "*** New node $text ($url)");
173           $url = a({href => $url}, $url);
174           push @aimtext,
175                "<b>$text</b>n$url";
176       }
177 
178       $max_node = $node if
179         !defined $max_node or
180          $max_node < $node;
181   }
182 
183   $cache->set("max-node", $max_node)
184       if $saved < $max_node;
185 
186   return @aimtext;
187 }
188 
189 ###########################################
190 sub qparse {
191 ###########################################
192   my($html_string) = @_;
193 
194   my $start_url =
195     URI::URL->new($FETCH_URL);
196 
197   my @questions = ();
198 
199   my $parser = HTML::TreeBuilder->new();
200   my $tree = $parser->parse($html_string);
201 
202   my($questions) = $tree->look_down(
203     "_tag", "a",
204     "name", "toc-Questions");
205 
206   if(! $questions) {
207      Gaim::debug_info("pmwatcher",
208      "Couldn't find Questions section");
209     return undef;
210   }
211 
212   my $node = $questions->parent();
213   while($node->tag() ne "table") {
214     $node = $node->right();
215   }
216 
217   for my $tr ($node->look_down(
218                            "_tag", "tr")) {
219     for my $a ($tr->look_down(
220                             "_tag", "a")) {
221       my $href = $a->attr('href');
222       my $text = $a->as_text();
223       my $url = URI::URL->new($href,
224                               $start_url);
225 
226       push @questions,
227            [$text, $url->abs()];
228           # Process only the question
229           # node, not the author's node
230       last;
231     }
232   }
233 
234   $tree->delete();
235   return @questions;
236 }

Nadel im Heuhaufen

Das Perl-Modul HTML::TreeBuilder erzeugt aus einem Webdokument einen Baum von HTML-Elementen. In diesem navigiert »qparse()« zunächst zu einem »<A>«-Element, das »toc-Questions« im Namensattribut enthält. Vom gefundenen Knoten des Typs HTML::Element geht der Weg mit der Methode »parent()« eine Etage höher im Baum. Dort sucht die »while«-Schleife ab Zeile 213 nach einem »<table>«-Element, in dem es auf der gleichen Hierarchie-Ebene mit »right()« nach rechts fährt.

Die Tabelle enthält in der ersten Spalte die Fragen, in der zweiten den Link zum Fragesteller. Daher fährt die erste For-Schleife ab Zeile 217 alle »<tr>«-Elemente an und die innere Schleife sucht darin »<a>«-Links.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben