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.
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.
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: |
|---|
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.





