File Coverage

blib/lib/Net/Chat/Jabber.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Net::Chat::Jabber;
2              
3             =head1 NAME
4              
5             Net::Chat::Jabber - Jabber protocol adapter for Net::Chat::Daemon
6              
7             =head1 API
8              
9             =over 4
10              
11             =cut
12              
13 1     1   680 use Net::Jabber qw(Client);
  0            
  0            
14             our $VERSION = '0.1';
15             our @ISA = qw(Net::Jabber::Client);
16              
17             use Net::Jabber::JID;
18             use Time::HiRes;
19              
20             use strict;
21             use warnings;
22              
23             # my $DEFAULT_SERVER = "jabber.org";
24             my $DEFAULT_SERVER = undef; # Have not gotten permission from jabber.org
25             my $DEFAULT_PASSWORD = "nopassword";
26             my $DEFAULT_RESOURCE = "default";
27              
28             # Internal routine to display a log message depending on the loglevel
29             # setting.
30             sub _log {
31             my $self = shift;
32             my $message;
33             my $level = 0;
34             if (@_ == 1) {
35             $message = shift;
36             } else {
37             ($level, $message) = @_;
38             }
39             my $allow_level = $self->{loglevel} || 0;
40             return if $level > $allow_level;
41             print $message, "\n";
42             }
43              
44             =item B()
45              
46             class - the name of the class we're creating
47              
48             jid - a string giving the JID, or a JID object
49              
50             %options
51              
52             password - the password to provide during authentication. TODO: if
53             this is not provided but a password is needed, some sort of
54             authCallback is needed.
55              
56             loglevel - logs with level higher than this are not displayed.
57             Defaults to 0.
58              
59             =cut
60              
61             sub new {
62             my ($class, $app, $jid, %options) = @_;
63             $jid = __default_jid($jid, $DEFAULT_SERVER, $DEFAULT_RESOURCE);
64              
65             my $self = $class->SUPER::new();
66             @$self{keys %options} = values %options;
67              
68             $self->{jid} = $jid;
69             $self->{password} ||= $DEFAULT_PASSWORD;
70             $self->{user} ||= $jid->GetUserID;
71             $self->{server} ||= $jid->GetServer;
72             $self->{resource} ||= $jid->GetResource();
73              
74             $self->_log("[$self->{user}] pid=$$");
75              
76             $self->_init_callbacks($app);
77              
78             return $self;
79             }
80              
81             sub __default_jid {
82             my ($jid, $server, $resource) = @_;
83             $jid = new Net::Jabber::JID($jid);
84             $jid->SetServer($server) if defined($server) && ! $jid->GetServer;
85             $jid->SetResource($resource) if defined($resource) && ! $jid->GetResource;
86             return $jid;
87             }
88              
89             =item B()
90              
91             Connect to the server, attempting to register if the specified user is
92             not yet registered.
93              
94             =cut
95              
96             sub connect {
97             my ($self) = @_;
98              
99             $self->Connect(hostname => $self->{server}) or return;
100              
101             my @identification = (username => $self->{user},
102             password => $self->{password},
103             resource => $self->{resource});
104             my @result = $self->AuthSend(@identification);
105             $self->_log(0, "auth status for $self->{user} ($$): $result[0] - $result[1]");
106              
107             if ($result[0] eq "401") {
108             @result = $self->RegisterSend(@identification);
109             $self->_log(0, "register status: " . join(" - ", @result));
110              
111             if ($result[0] eq "ok") {
112             @result = $self->AuthSend(@identification);
113             $self->_log(0, "auth status for $self->{user} ($$): $result[0] - $result[1]");
114             }
115             }
116              
117             $self->PresenceSend();
118             return 1;
119             }
120              
121             =item B()
122              
123             Reestablish a broken connection.
124              
125             =cut
126              
127             sub reconnect {
128             my ($self) = @_;
129             $self->connect();
130             }
131              
132             =item B($jid)
133              
134             Subscribe to messages coming from $jid.
135              
136             =cut
137              
138             sub subscribe {
139             my ($self, $jid) = @_;
140             $jid = __default_jid($jid, $self->{server});
141             $self->Subscription(type => "subscribe", to => $jid->GetJID("full"));
142             }
143              
144             # Internal routine to initialize callbacks. Converts Jabber-specific
145             # callbacks into a simplified set. Which would be useful, if I were to
146             # document what that supposedly simplified set is.
147             sub _init_callbacks {
148             my ($self, $app) = @_;
149              
150             $self->SetMessageCallBacks(normal => sub {
151             local $app->{message} = $_[1];
152             $self->_onMessage($app, @_);
153             });
154              
155             $self->SetMessageCallBacks(chat => sub {
156             local $app->{message} = $_[1];
157             $self->_onMessage($app, @_);
158             });
159              
160             $self->SetPresenceCallBacks(available => sub {
161             for my $cb (@{ $app->{callbacks}{available} }) {
162             return if ($cb->(@_)); # First true value handles
163             }
164             });
165              
166             $self->SetPresenceCallBacks(unavailable => sub {
167             for my $cb (@{ $app->{callbacks}{unavailable} }) {
168             return if ($cb->(@_)); # First true value handles
169             }
170             });
171              
172             $self->SetMessageCallBacks(error => sub {
173             for my $cb (@{ $app->{callbacks}{error} }) {
174             return if ($cb->(@_)); # First true value handles
175             }
176             my $error = $_[1];
177             my $from = $error->GetFrom();
178             my $subject = $error->GetSubject();
179             my $body = $error->GetBody();
180             $self->_log(-1, "($$) unnoticed error from $from: ($subject) $body");
181             });
182             }
183              
184             =item B($to,$message,options...)
185              
186             Send the message text $message to $to. Available options:
187              
188             subject: set the subject of the message (rarely used)
189              
190             thread: mark the message as a reply in the given thread
191              
192             attachments: an array of attachments, where each attachment
193             is either a chunk of text, or an XML tree.
194              
195             =cut
196              
197             sub post {
198             my ($self, $to, $message, %options) = @_;
199             $to = __default_jid($to, $self->{server});
200             my $subject = $options{subject} || ref($self) . " message";
201             my @args = ();
202             push(@args, thread => $options{thread}) if defined $options{thread};
203             my $thr = ($options{thread} ? " thr=$options{thread}" : "");
204             $self->_log(1, "($self->{user} -> $to$thr) $message");
205              
206             my $msg = new Net::Jabber::Message;
207             $msg->SetMessage(to => $to->GetJid("full"),
208             subject => $subject,
209             body => $message,
210             @args);
211              
212             my @attachments = @{ $options{attachments} || [] };
213             if (@attachments > 0) {
214             my $attaches_node = $msg->{TREE}->add_child("attachments"); # FIXME {TREE}
215             foreach my $attachment (@attachments) {
216             my $attach_node = $attaches_node->add_child("attachment");
217             if (! ref $attachment) {
218             $attach_node->add_child("type", 'data');
219             $attach_node->add_child("data", $attachment);
220             } else {
221             while (my ($tag, $value) = each %$attachment) {
222             $attach_node->add_child($tag, $value);
223             }
224             }
225             }
226             }
227              
228             $self->Send($msg);
229             }
230              
231             =item B(to,message,options...)
232              
233             Send out a request, but do not wait for the reply.
234              
235             =cut
236              
237             sub send_request {
238             my ($self, $to, $message, %options) = @_;
239             $options{thread} ||= "tid-" . Time::HiRes::time();
240             $options{subject} ||= ref($self) . " request";
241             $self->_log(1, "($self->{user}) starting transaction with thread $options{thread}");
242             $self->start_transaction($options{thread}, $options{onReply});
243             $self->post($to, $message, %options);
244             }
245              
246             =item B(to,message,options...)
247              
248             Make a synchronous request. Returns the body of the reply message.
249              
250             =cut
251              
252             sub request {
253             my ($self, $to, $message, %options) = @_;
254             my $thread = $options{thread} ||= "tid-" . Time::HiRes::time();
255             my $reply;
256             $options{onReply} = sub { $reply = shift; };
257             $self->send_request($to, $message, %options);
258             while (1) {
259             defined $self->Process() or die "jabber network error";
260             last if defined $reply;
261             }
262              
263             return $reply->GetBody();
264             }
265              
266             # Internal routine that gets called on every message, before it gets
267             # categorized as a request, reply, or whatever.
268             sub _onMessage {
269             my ($self, $app, $sid, $message, %extra) = @_;
270              
271             $self->_log(1, "($$) got message from " . $message->GetFrom() . ": " . $message->GetBody());
272              
273             # First, check whether it has a thread id of the syntax used for
274             # request/reply pairs
275             my $thread = $message->GetThread();
276             if (defined($thread) && $thread =~ /^tid-/) {
277             $self->_log(2, " found thread $thread");
278             if (exists $self->{active}{$thread}) {
279             $self->_log(2, " ending current transaction");
280             my $cb = $self->end_transaction($thread);
281             if (UNIVERSAL::isa($cb, 'CODE')) {
282             return $cb->($message, $thread, %extra);
283             } else {
284             return $app->onReply($message, $thread, %extra);
285             }
286             } else {
287             $self->_log(2, " no current transaction, must be request");
288             return $app->onRequest($message, %extra);
289             }
290             } else {
291             $self->_log(2, " no thread");
292             return $app->onMessage($message, %extra);
293             }
294             }
295              
296             =item B($transaction_id, $onReply)
297              
298             Start a transaction. A transaction is identified by the given id,
299             and... blah blah blah this is very important but I don't remember
300             what I did here.
301              
302             =cut
303              
304             sub start_transaction {
305             my ($self, $trans_id, $onReply) = @_;
306             $onReply ||= 1;
307             $self->{active}{$trans_id} = $onReply;
308             }
309              
310             =item B($transaction_id)
311              
312             Normally called automatically. Terminates a transaction and erases
313             the transaction callback.
314              
315             =cut
316              
317             sub end_transaction {
318             my ($self, $trans_id) = @_;
319             if (exists $self->{active}{$trans_id}) {
320             my $cb = delete $self->{active}{$trans_id};
321             $self->remove_callback('message', $trans_id);
322             return $cb;
323             } else {
324             $self->_log(-1, "tried to end nonexistent transaction '$trans_id'");
325             return;
326             }
327             }
328              
329             =item B($transaction_id)
330              
331             Return the number of active karfloomer hangers for the given
332             transaction. The method name is awful; this is counting karfloomer
333             hangers for a given transaction, not the number of transactions. FIXME
334             when I figure this all out.
335              
336             =cut
337              
338             sub count_transactions {
339             my ($self) = @_;
340             return scalar(keys %{ $self->{active} });
341             }
342              
343             =item B()
344              
345             Wait until no more active transactions are outstanding.
346              
347             =cut
348              
349             sub barrier {
350             my ($self) = @_;
351              
352             $self->_log(1, "[$self->{user}] ...pausing...");
353             while (1) {
354             my $nactive = $self->count_transactions();
355             last if $nactive == 0;
356             $self->_log(0, "[$self->{user}] ...pausing, $nactive active trans");
357             last if ! defined $self->Process(5);
358             }
359             }
360              
361             =item B()
362              
363             Check whether any messages are available.
364              
365             =cut
366              
367             sub poll {
368             my ($self) = @_;
369             $self->Process(0);
370             }
371              
372             =item B([$timeout])
373              
374             Wait $timeout seconds for more messages to come in. If $timeout is not
375             given or undefined, block until a message is received.
376              
377             Return value: 1 = data received, 0 = ok but no data received, undef = error
378              
379             =cut
380              
381             sub wait {
382             my $self = shift;
383             $self->Process(@_);
384             }
385              
386             1;
387              
388             =back
389              
390             =head1 SEE ALSO
391              
392             Net::Chat::Daemon, Net::Jabber, Net::XMPP
393              
394             =head1 AUTHOR
395              
396             Steve Fink Esfink@cpan.orgE
397              
398             Send bug reports directly to me. Include the module name in the
399             subject of the email message.
400              
401             =head1 COPYRIGHT AND LICENSE
402              
403             Copyright 2004 by Steve Fink
404              
405             This library is free software; you can redistribute it and/or modify
406             it under the same terms as Perl itself.
407              
408             =cut