File Coverage

blib/lib/Net/Chat/Daemon.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Net::Chat::Daemon - run a daemon that is controlled via instant messaging
4              
5             =head1 ABSTRACT
6              
7             This package is intended to serve as a superclass for objects that
8             want to communicate via IM messages within a distributed network of
9             client nodes and a coordinator, without dealing with the complexities
10             or implementation details of actually getting the messages from place
11             to place.
12              
13             It pretends to be protocol-neutral, but for now and the conceivable
14             future will only work with a Jabber transport. (It directly uses the
15             message objects and things that Jabber returns.)
16              
17             Note that this package will NOT help you implement an instant
18             messaging server. This package is for writing servers that communicate
19             with other entities via instant messages -- servers written using this
20             package are instant messaging *clients*.
21              
22             =head1 SYNOPSIS
23              
24             package My::Server;
25             use base 'Net::Chat::Daemon';
26             sub handleHello {
27             return "hello to you too";
28             }
29             sub handleSave {
30             my ($filename, $file) = @_;
31             return "denied" unless $filename =~ /^[.\w]+$/;
32             open(my $fh, ">/var/repository/$filename") or return "failed: $!";
33             print $fh $file;
34             close $fh or return "failed: $!";
35             return "ok";
36             }
37             sub someMethod {
38             my ($self, @args) = @_;
39             .
40             .
41             .
42             }
43             sub new {
44             my ($class, $user, %options) = @_;
45             return $class->SUPER::new(%options,
46             commands => { 'callMethod' => 'someMethod',
47             'save' => \&handleSave });
48             }
49              
50             package main;
51             my $server = My::Server->new('myuserid@jabber.org');
52             $server->process();
53              
54             # or to do it all in one step, and retry connections for 5 minutes
55             # (300 seconds) before failing due to problems reaching the server:
56              
57             My::Server->run('myuserid@jabber.org', retry => 300);
58              
59             When you run this, you should be able to send a message to
60             userid@jabber.org saying "hello" and get a response back, or
61             "callMethod a b c" to call the method with the given arguments. To use
62             the "save" command, you'll need to use a command-line client capable
63             of sending attachments in the format expected by this server (it
64             currently does not use any standard file-sending formats). The
65             C command packaged with this module can do this via the C<-a>
66             command-line option.
67              
68             A note on the implementation: when I first wrote this, it was really
69             only intended to be used with Jabber. The code hasn't been fully
70             restructured to remove this assumption.
71              
72             =head2 WARNING
73              
74             The Net::Chat::Daemon name is most likely temporary (as in, I don't
75             like it very much, but haven't come up with anything better.) So be
76             prepared to change the name if you upgrade.
77              
78             =head1 API
79              
80             =over 4
81              
82             =cut
83              
84             package Net::Chat::Daemon;
85             our $VERSION = "0.3";
86              
87 1     1   31795 use strict;
  1         3  
  1         119  
88 1     1   2516 use Time::HiRes qw(time);
  1         2861  
  1         7  
89 1     1   237 use Carp qw(croak);
  1         9  
  1         98  
90              
91             # Subclasses. These probably ought to be discovered and loaded
92             # dynamically.
93 1     1   1141 use Net::Chat::Jabber;
  0            
  0            
94             our %scheme_registry = ( 'jabber' => 'Net::Chat::Jabber',
95             'xmpp' => 'Net::Chat::Jabber',
96             );
97              
98             # Internal routine to display a log message depending on the loglevel
99             # setting.
100             sub _log {
101             my $self = shift;
102             my $message;
103             my $level = 0;
104             if (@_ == 1) {
105             $message = shift;
106             } else {
107             ($level, $message) = @_;
108             }
109             my $allow_level = $self->{loglevel} || 0;
110             return if $level > $allow_level;
111             print $message, "\n";
112             }
113              
114             =item B($user, %options)
115              
116             To implement a server, you need to define a set of commands that it
117             will respond to. See C, below, for details on how commands
118             are registered. The part that's relevant to this method is that you
119             can pass in a C option, which is a hash ref mapping command
120             names to either subroutines or method names. When the server receives
121             a message, it will carve up the message into a command name and
122             whitespace-separated arguments. See C, below, for details.
123              
124             Methods that are invoked by being values in the C hash will
125             also be given the usual $self parameter at the beginning of the
126             parameter list, of course.
127              
128             The $user argument to the C() method is something like
129             jabber://userid@jabber.org/someresource or just
130             userid@jabber.org/someresource (who are we kidding?) Theoretically,
131             this allows a future subclass to work with yahoo://userid, but don't
132             hold your breath.
133              
134             =cut
135              
136             sub new {
137             my ($class, $user, %opts) = @_;
138             my $scheme = 'jabber'; # Default
139             ($scheme, $user) = ($1, $2) if $user =~ m!^(\w+)://(.*)!;
140              
141             my $cxn_opts = delete $opts{connection_options} || {};
142             $cxn_opts->{password} ||= delete $opts{password};
143             my $self = bless { %opts, user => $user }, $class;
144              
145             my $cxn_class = $scheme_registry{$scheme}
146             or croak "unknown scheme '$scheme'";
147              
148             $self->{cxn} = $cxn_class->new($self, $user, %$cxn_opts);
149              
150             if (defined $opts{master}) {
151             $self->push_callback('unavailable', sub { $self->checkMaster(@_) });
152             $self->subscribe($opts{master});
153             }
154              
155             $self->{cxn}->connect()
156             or die "unable to connect to server for $user";
157              
158             return $self;
159             }
160              
161             =item B($user, %options)
162              
163             Create a daemon with the given options, and loop forever waiting for
164             messages to come in. If the IM system dies, exit out with an error
165             unless the 'retry' option is given, in which case it will be
166             interpreted as the maximum number of seconds to retry, or zero to
167             retry forever (this is often a good idea.)
168              
169             If you want your server to exit gracefully, define your own command
170             that calls C.
171              
172             =cut
173              
174             sub run {
175             my ($class, $user, %opts) = @_;
176             my $RETRY_GAP = 1.5; # Seconds between retries
177              
178             my $server = $class->new($user, %opts);
179              
180             my $retry_sec = $opts{retry} or do {
181             1 while defined $server->process();
182             exit 1;
183             };
184              
185             # We know we want to retry now.
186             MAINLOOP: while (1) {
187             1 while defined $server->process();
188             next if $retry_sec == 0; # Retry forever
189              
190             if ($retry_sec == 0) {
191             # Retry forever
192             sleep $RETRY_GAP;
193             } elsif ($retry_sec < $RETRY_GAP) {
194             my $retry_deadline = time() + $retry_sec;
195             do {
196             sleep $RETRY_GAP;
197             next MAINLOOP if defined $server->process();
198             } while (time() < $retry_deadline);
199             last; # Couldn't process anything successfully before deadline
200             }
201             }
202              
203             exit 1;
204             }
205              
206             =item B($type, $callback, [$id])
207              
208             =item B($type, $callback, [$id])
209              
210             =item B($type, $id)
211              
212             Add or remove callback for the event $type. C is
213             only useful if an $id was passed into C or
214             C.
215              
216             Valid types:
217             message
218             available
219             unavailable
220             error
221              
222             =cut
223              
224             sub push_callback {
225             my ($self, $type, $callback, $id) = @_;
226             push @{ $self->{callbacks}{$type} }, $callback;
227             if (defined $id) {
228             $self->{callback_id}{$id} = $self->{callbacks}{$type}->[-1];
229             }
230             }
231              
232             sub unshift_callback {
233             my ($self, $type, $callback, $id) = @_;
234             unshift @{ $self->{callbacks}{$type} }, $callback;
235             if (defined $id) {
236             $self->{callback_id}{$id} = $self->{callbacks}{$type}->[-1];
237             }
238             }
239              
240             sub remove_callback {
241             my ($self, $type, $id) = @_;
242             my $cb = $self->{callback_id}{$id};
243             if (defined $cb) {
244             delete $self->{callback_id}{$id};
245             my $cb_list = $self->{callbacks}{$type};
246             @$cb_list = grep { $_ != $cb } @$cb_list;
247             }
248             }
249              
250             =item B($msg, %extra)
251              
252             This method will be invoked as a callback whenever a regular chat
253             message is received. The default implementation is to relay the
254             message to C, but this may be overridden in a subclass to
255             distinguish between the two.
256              
257             =cut
258              
259             sub onMessage {
260             my ($self, $msg, %extra) = @_;
261             $self->onRequest($msg, %extra);
262             }
263              
264             =item B($msg, %extra)
265              
266             This method will be invoked as a callback whenever a chat message is
267             received in reply to a previous request. The default implementation is
268             to relay the message to C above, but this may be overridden
269             in a subclass to distinguish between the two.
270              
271             =cut
272              
273             sub onReply {
274             my ($self, $message, $thread, %extra) = @_;
275             $self->onMessage($message, %extra);
276             }
277              
278             =item B($name, $command)
279              
280             Set the callback associated with a command. If a string is passed in,
281             it will be treated as a method on the current object (the object that
282             C was called on). The arguments to the method will be the
283             words in the command string. If a closure is passed in, it will be
284             invoked directly with the words in the command string. The $self
285             object will not be passed in by default in this case, but it is easy
286             enough to define your command like
287              
288             $x->setCommand('doit' => sub { $x->doit(@_) })
289              
290             Note that all commands are normally set up when constructing the
291             server, but this method can be useful for dynamically adding new
292             commands. I use this at time to temporarily define commands within
293             some sort of transaction.
294              
295             =cut
296              
297             sub setCommand {
298             my ($self, $name, $command) = @_;
299             $self->{commands}{$name} = $command;
300             }
301              
302             =item B($name)
303              
304             Get the handler for a given command. The normal way to do this is to
305             pass in a 'commands' hash while constructing the object, where each
306             command is mapped to the name of the corresponding method.
307              
308             Alternatively, you can simply define a method named handleSomething,
309             which will set the command 'something' (initial letter lower-cased) to
310             call the handleSomething method. (So 'handleSomeThing' would create
311             the command 'someThing'.)
312              
313             Also, if you ask for help on a command, it will call the method
314             'helpXxx' where 'xxx' is the name of the command. If no such method
315             exists, the default response will be "(command) args..." (accurate but
316             hardly helpful).
317              
318             =cut
319              
320             sub getHandler {
321             my ($self, $name) = @_;
322             my $sub;
323             if ($name eq 'help') {
324             $sub = sub { $self->showHelp(@_) };
325             } else {
326             $sub = $self->{commands}{$name};
327             }
328              
329             $sub ||= $self->can("handle\u$name");
330              
331             return $sub;
332             }
333              
334             =item B([$command])
335              
336             Return a help message listing out all available commands, or detailed
337             help on the one command passed in.
338              
339             =cut
340              
341             sub showHelp {
342             my ($self, $command) = @_;
343              
344             if (defined($command)) {
345             return $self->{help}{$command} if defined $self->{help}{$command};
346             my $sub = $self->can("help\u$command");
347             return $sub->($self) if $sub;
348             return "$command args..."; # Wise-ass help
349             }
350              
351             my %commands;
352             @commands{keys %{ $self->{commands} }} = ();
353              
354             no strict 'refs';
355             foreach (map { s/handle//; "\l$_" }
356             grep { *{${ref($self)."::"}{$_}}{CODE} }
357             grep { /^handle/ }
358             keys %{ref($self)."::"})
359             {
360             $commands{$_} = 1;
361             }
362              
363             return "Available commands: " . join(" ", sort keys %commands);
364             }
365              
366             =item B($msg, %extra)
367              
368             This method will be invoked as a callback whenever a request is
369             received. As you know if you've read the documentation for
370             C and C, by default all messages go through this
371             handler.
372              
373             The default implementation of onRequest parses the message into a
374             command and an array of arguments, looks up the appropriate handler
375             for that command, invokes the handler with the arguments, then sends
376             back a reply message with the return value of the handler as its text.
377              
378             If any files are attached to the message, they are extracted and
379             appended to the end of the argument list.
380              
381             An example: if you send the message "register me@jabber.org ALL" to
382             the server, it will look up its internal command map. If you defined a
383             C method, it will call that. Otherwise, if you
384             specified the command 'register' in the commands hash, it will call
385             whatever value if finds there. Two arguments will be passed to the
386             handler: the string "me@jabber.org", and the string "ALL".
387              
388             =cut
389              
390             sub onRequest {
391             my ($self, $message) = @_;
392             my $body = $message->GetBody();
393             my $from = $message->GetFrom();
394             $self->_log(1, "[$self->{user}] from($from): $body\n");
395              
396             # Parse the request body into a command and a list of arguments
397             my ($cmd, @args) = $body =~ /('(?:\\.|.)*'|"(?:\\.|.)*"|\S+)/g;
398             foreach (@args) {
399             $_ = substr($_, 1, -1) if (/^['"]/);
400             }
401              
402             # Add the attachments to the end of the @args array. This is most
403             # likely an abuse of the Jabber protocol.
404             my $attachments_node = $message->{TREE}->XPath("attachments");
405             my @attachments = $attachments_node ? $attachments_node->children() : ();
406             foreach my $node (@attachments) {
407             my %attachment;
408             foreach ($node->children()) {
409             $attachment{$_->get_tag()} = $_->get_cdata();
410             }
411             push @args, \%attachment;
412             }
413              
414             # Lookup the handler for this command and call it, then send back
415             # the result as a reply.
416             my $meth = $self->getHandler($cmd);
417             my $reply = $message->Reply();
418             local $self->{last_message} = $message;
419             if ($meth) {
420             if (UNIVERSAL::isa($meth, 'CODE')) {
421             $reply->SetBody($meth->(@args));
422             } else {
423             $reply->SetBody($self->$meth(@args));
424             }
425             $self->{cxn}->Send($reply);
426             return 1;
427             } else {
428             $self->_log(0, "[$self->{user}] ignoring message: $body");
429             return;
430             }
431             }
432              
433             =item B($sid, $presence)
434              
435             Internal: presence unavailable callback - exit if the master exited
436              
437             =cut
438              
439             sub checkMaster {
440             my ($self, $sid, $presence) = @_;
441             if ($self->{master} eq $presence->GetFrom("jid")->GetUserID()) {
442             $self->_log(0, "[$self->{user}] master terminated, exiting.");
443             exit 0;
444             }
445             return;
446             }
447              
448             =item B([$timeout])
449              
450             Wait $timeout seconds for more messages to come in. If $timeout is not
451             given or undefined, block until a message is received.
452              
453             Return value: 1 = data received, 0 = ok but no data received, undef = error
454              
455             =cut
456              
457             sub process {
458             my $self = shift;
459             return $self->{cxn}->wait(@_);
460             }
461              
462             ################## SYNCHRONIZATION METHODS #####################
463              
464             sub _makeId {
465             return time();
466             }
467              
468             =item B($nodes)
469              
470             This method is used for things like test harnesses, where you might
471             want to wait until a set of nodes are all alive and active before
472             starting the test case. You pass in a list of users, and this method
473             will wait until all of them have logged into the server.
474              
475             Implementation: wait until receiving presence notifications from the
476             given list of nodes. Works by temporarily adding new presence
477             callbacks, and periodically pinging nodes that haven't come up yet.
478              
479             Arguments: $nodes - reference to an array of user descriptors (eg jids)
480              
481             I suppose I ought to add a timeout argument, but right now, this will
482             block until all nodes have reported in.
483              
484             =cut
485              
486             sub waitUntilAllHere {
487             my ($self, $nodes) = @_;
488              
489             my ($id1, $id2, $id3) = (_makeId(), _makeId(), _makeId());
490             $self->unshift_callback(available => sub { $self->onSyncLogin(@_) }, $id1);
491             $self->unshift_callback(unavailable => sub { $self->onSyncLogout(@_) }, $id2);
492             $self->unshift_callback(error => sub { $self->onSyncError(@_) }, $id3);
493              
494             # Maximum time to pause before asking someone if they're awake yet.
495             my $PATIENCE = 0.5; # Seconds
496              
497             $self->{allhere} = (@$nodes == 0);
498              
499             # Ignore any nodes that we don't care about
500             delete $self->{care_about};
501             $self->{care_about}{$_} = 1 foreach (@$nodes);
502              
503             # Initialize the set of nodes that we're waiting for. This is
504             # different from the set of nodes we care about, in that a node
505             # could disappear and come back a few times while we're waiting for
506             # everyone to arrive.
507             delete $self->{waiting};
508             $self->{waiting}{$_} = 1 foreach (@$nodes);
509              
510             # Keep a timestamp for the last time we've heard from each of the
511             # nodes. This is used to decide when to send another ping.
512             my $now = time();
513             delete $self->{lastcheck};
514             $self->{lastcheck}{$_} = $now foreach (@$nodes);
515              
516             while (! $self->{allhere}) {
517             my ($oldest, $delay);
518             ($oldest) =
519             sort { $self->{lastcheck}{$a} <=> $self->{lastcheck}{$b} }
520             keys %{ $self->{waiting} };
521             my $age = time() - $self->{lastcheck}{$oldest};
522             $delay = $PATIENCE - $age;
523             $delay = 0 if $delay < 0;
524              
525             # Wait for $delay seconds for any responses
526             $self->process($delay);
527              
528             last if $self->{allhere};
529              
530             # Ping oldest
531             $self->subscribe("$oldest\@$self->{server}");
532             $self->{lastcheck}{$oldest} = time();
533             }
534              
535             # Everyone is here, so remove our callbacks
536             $self->remove_callback('available', $id1);
537             $self->remove_callback('unavailable', $id2);
538             $self->remove_callback('error', $id3);
539              
540             foreach my $node (@$nodes) {
541             $self->post($node, "hey guys", subject => "allhere");
542             }
543             }
544              
545             =item B($sid, $presence)
546              
547             Callback used when synchronizing with a bunch of nodes. Notified
548             when someone logs in who we care about.
549              
550             =cut
551              
552             sub onSyncLogin {
553             my ($self, $sid, $presence) = @_;
554             my $status = $presence->GetStatus();
555             my $show = $presence->GetShow();
556             my $from = $presence->GetFrom();
557             my $node = $presence->GetFrom("jid")->GetUserID();
558             $self->_log(1, "($$) presence from $node: $status ($show)");
559             if ($self->{care_about}{$node} && $self->{waiting}{$node}) {
560             delete $self->{waiting}{$node};
561             if (0 == keys %{ $self->{waiting} }) {
562             $self->{allhere} = 1;
563             }
564             return 1;
565             }
566             return;
567             }
568              
569             =item B($sid, $presence)
570              
571             If a node disappears while we are waiting for everyone to gather,
572             then re-set its waiting flag.
573              
574             =cut
575              
576             sub onSyncLogout {
577             my ($self, $sid, $presence) = @_;
578             my $status = $presence->GetStatus();
579             my $show = $presence->GetShow();
580             my $from = $presence->GetFrom();
581             my $node = $presence->GetFrom("jid")->GetUserID();
582             $self->_log(1, "bye bye from $node: $status ($show)");
583             if ($self->{care_about}{$node}) {
584             $self->{waiting}{$node} = 1;
585             return 1;
586             }
587             return;
588             }
589              
590             =item B($sid, $message)
591              
592             Watch for 404 errors coming back while waiting for all nodes to be
593             present.
594              
595             =cut
596              
597             sub onSyncError {
598             my ($self, $sid, $msg) = @_;
599             my $code = $msg->GetErrorCode();
600             return if $code != 404; # do not handle
601              
602             my $from = $msg->GetFrom();
603             $self->_log(0, "[$self->{user}] client $from not found");
604             my $node = $msg->GetFrom("jid")->GetUserID();
605             $self->{lastcheck}{$node} = time();
606             return 1;
607             }
608              
609             1;
610              
611             =back
612              
613             =head1 SEE ALSO
614              
615             Net::Chat::Jabber, Net::Jabber, Net::XMPP
616              
617             =head1 AUTHOR
618              
619             Steve Fink Esfink@cpan.orgE
620              
621             Send bug reports directly to me. Include the module name in the
622             subject of the email message.
623              
624             =head1 COPYRIGHT AND LICENSE
625              
626             Copyright 2004 by Steve Fink
627              
628             This library is free software; you can redistribute it and/or modify
629             it under the same terms as Perl itself.
630              
631             =cut