File Coverage

blib/lib/Net/Jabber/Bot.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Net::Jabber::Bot;
2              
3 1     1   31300 use Moose;
  1         765637  
  1         9  
4             use MooseX::Types
5 1     1   8336 -declare => [qw( JabberClientObject PosInt PosNum HundredInt )];
  1         48277  
  1         13  
6              
7             # import builtin types
8 1     1   8344 use MooseX::Types::Moose qw/Int HashRef Str Maybe ArrayRef Bool CodeRef Object Num/;
  1         17263  
  1         15  
9              
10 1     1   8655 use version;
  1         2432  
  1         6  
11 1     1   547 use Net::Jabber;
  0            
  0            
12             use Time::HiRes;
13             use Sys::Hostname;
14             use Log::Log4perl qw(:easy);
15             #use Data::Dumper; #For testing only.
16              
17              
18             coerce Bool, from Str,
19             via {($_ =~ m/(^on$)|(^true$)/i) + 0}; # True if it's on or true. Otherwise false.
20              
21             subtype JabberClientObject, as Object, where { $_->isa('Net::Jabber::Client') };
22              
23             subtype PosInt, as Int, where { $_ > 0 };
24             subtype PosNum, as Num, where { $_ > 0 };
25             subtype HundredInt, as Num, where { $_ > 100 };
26              
27             has jabber_client => (isa => Maybe[JabberClientObject],
28             is => 'rw',
29             default => sub {Net::Jabber::Client->new});
30             #my %connection_hash : ATTR; # Keep track of connection options fed to client.
31              
32             has 'client_session_id' => (isa => Str, is => 'rw');
33             has 'connect_time' => (isa => PosInt, is => 'rw', default => 9_999_999_999);
34             has 'forum_join_grace' => (isa => PosNum, is => 'rw', default => 10);
35             has 'server_host' => (isa => Str, is => 'rw', lazy => 1, default => sub{shift->server });
36             has 'server' => (isa => Str, is => 'rw');
37             has 'port' => (isa => PosInt, is => 'rw', default => 5222);
38             has 'tls' => (isa => Bool, is => 'rw', default => '0');
39             has 'connection_type' => (isa => Str, is => 'rw', default => 'tcpip');
40             has 'conference_server' => (isa => Str, is => 'rw');
41             has 'username' => (isa => Str, is => 'rw');
42             has 'password' => (isa => Str, is => 'rw');
43             has 'alias' => (isa => Str, is => 'rw', default => sub{'net_jabber_bot'});
44             # Resource defaults to alias_hostname_pid
45             has 'resource' => (isa => Str, lazy => 1, is => 'rw', default => sub{shift->alias . "_" . hostname . "_" . $$});
46             #has 'resource' => (isa => Str, lazy => 1, is => 'rw', default => sub{shift->alias});
47             has 'message_function' => (isa => Maybe[CodeRef], is => 'rw', default => sub{undef});
48             has 'background_function' => (isa => Maybe[CodeRef], is => 'rw', default => sub{undef});
49             has 'loop_sleep_time' => (isa => PosNum, is => 'rw', default => 5);
50             has 'process_timeout' => (isa => PosNum, is => 'rw', default => 5);
51             has 'from_full' => (isa => Str, is => 'rw', default => sub{my $self = shift;
52             $self->username .
53             '@' .
54             $self->server .
55             '/' .
56             $self->alias});
57            
58              
59             has 'safety_mode' => (isa => Bool, is => 'rw', default => 1, coerce => 1);
60             has 'ignore_server_messages' => (isa => Bool, is => 'rw', default => 1, coerce => 1);
61             has 'ignore_self_messages' => (isa => Bool, is => 'rw', default => 1, coerce => 1);
62             has 'forums_and_responses' => (isa => HashRef[ArrayRef[Str]], is => 'rw'); # List of forums we're in and the strings we monitor for.
63             has 'forum_join_time' => (isa => HashRef[Int], is => 'rw', default => sub{{}}); # List of when we joined each forum
64             has 'out_messages_per_second' => (isa => PosNum, is => 'rw', default => sub{5});
65             has 'message_delay' => (isa => PosNum, is => 'rw', default => sub {1/5});
66              
67             has 'max_message_size' => (isa => HundredInt, is => 'rw', default => 1000000);
68             has 'max_messages_per_hour' => (isa => PosInt, is => 'rw', default => 1000000);
69              
70             # Initialize this hour's message count.
71             has 'messages_sent_today' => (isa => 'HashRef', is => 'ro', default => sub{{(localtime)[7] => {(localtime)[2] => 0}}});
72              
73              
74             #my %message_function : ATTR; # What is called if we are fed a new message once we are logged in.
75             #my %bot_background_function : ATTR; # What is called if we are fed a new message once we are logged in.
76             #my %forum_join_time : ATTR; # Tells us if we've parsed historical messages yet.
77             #my %client_start_time :ATTR; # Track when we came online. Also used to determine if we're online.
78             #my %process_timeout : ATTR; # Time to take in process loop if no messages found
79             #my %loop_sleep_time : ATTR; # Time to sleep each time we go through a Start() loop.
80             #my %ignore_messages : ATTR; # Messages to ignore if we recieve them.
81             #my %forums_and_responses: ATTR; # List of forums we have joined and who we respond to in each forum
82             #my %message_delay: ATTR; # Allows us to limit Messages per second
83             #my %max_message_size: ATTR; # Maximum allowed message size before we chunk them.
84             #my %forum_join_grace: ATTR; # Time before we start responding to forum messages.
85             #my %messages_sent_today: ATTR; # Tracks messages sent in 2 dimentional hash by day/hour
86             #my %max_messages_per_hour: ATTR; # Limits the number of messages per hour.
87             #my %safety_mode: ATTR; # Tracks if we are in safety mode.
88              
89             =head1 NAME
90              
91             Net::Jabber::Bot - Automated Bot creation with safeties
92              
93             =head1 VERSION
94              
95             Version 2.1.5
96              
97             =cut
98              
99             our $VERSION = '2.1.5';
100              
101             =head1 SYNOPSIS
102              
103             Program design:
104             This is a Moose based Class.
105              
106             The idea behind the module is that someone creating a bot should not really have to know a whole lot about how the Jabber protocol works in order to use it. It also allows us to abstract away all the things that can get a bot maker into trouble. Essentially the object helps protect the coders from their own mistakes.
107              
108             All someone should have to know and define in the program away from the object is:
109              
110             =over
111              
112             =item 1. Config - Where to connect, how often to do things, timers, etc
113              
114             =item 2. A subroutine to be called by the bot object when a new message comes in.
115              
116             =item 3. A subroutine to be called by the bot object every so often that lets the user do background activities (check logs, monitor web pages, etc.),
117              
118             =back
119              
120             The object at present has the following enforced safeties as long as you do not override safety mode:
121              
122             =over
123              
124             =item 1. Limits messages per second, configurable at start up, (Max is 5 per second) by requiring a sleep timer in the message sending subroutine each time one is sent.
125              
126             =item 2. Endless loops of responding to self prevented by now allowing the bot message processing subroutine to know about messages from self
127              
128             =item 3. Forum join grace period to prevent bot from reacting to historical messages
129              
130             =item 4. Configurable aliases the bot will respond to per forum
131              
132             =item 5. Limits maximum message size, preventing messages that are too large from being sent (largest configurable message size limit is 1000).
133              
134             =item 6. Automatic chunking of messages to split up large messages in message sending subroutine
135              
136             =item 7. Limit on messages per hour. (max configurable limit of 125) Messages are visible via log4perl, but not ever be sent once the message limit is reached for that hour.
137              
138             =back
139              
140             =head1 FUNCTIONS
141              
142             =over 4
143              
144             =item B<new>
145              
146             Minimal:
147             my $bot = Net::Jabber::Bot->new(
148             server => 'host.domain.com' # Name of server when sending messages internally.
149             , conference_server => 'conference.host.domain.com'
150             , port => 522
151             , username => 'username'
152             , password => 'pasword'
153             , safety_mode => 1
154             , message_function => \&new_bot_message
155             , background_function => \&background_checks
156             , forums_and_responses => \%forum_list
157             );
158            
159             All options:
160             my $bot = Net::Jabber::Bot->new(
161             server => 'host.domain.com' # Name of server when sending messages internally.
162             , conference_server => 'conference.host.domain.com'
163             , server_host => 'talk.domain.com' # used to specify what jabber server to connect to on connect?
164             , tls => 0 # set to 1 for google
165             , connection_type => 'tcpip'
166             , port => 522
167             , username => 'username'
168             , password => 'pasword'
169             , alias => 'cpan_bot'
170             , message_function => \&new_bot_message
171             , background_function => \&background_checks
172             , loop_sleep_time => 15
173             , process_timeout => 5
174             , forums_and_responses => \%forum_list
175             , ignore_server_messages => 1
176             , ignore_self_messages => 1
177             , out_messages_per_second => 4
178             , max_message_size => 1000
179             , max_messages_per_hour => 100
180             );
181              
182              
183              
184              
185             Setup the object and connect to the server. Hash values are passed to new as a hash.
186              
187             The following initialization variables can be passed. Only marked variables are required (TODO)
188              
189             =over 5
190              
191             =item B<safety_mode>
192              
193             safety_mode = (1,0)
194              
195             Determines if the bot safety features are turned on and enforced. This mode is on by default. Many of the safety features are here to assure you do not crash your favorite jabber server with floods, etc. DO NOT turn it off unless you're sure you know what you're doing (not just Sledge Hammer ceratin)
196              
197             =item B<server>
198              
199             Jabber server name
200              
201             =item B<server_host>
202              
203             Defaults to the same value set for 'server' above.
204             This is where the bot initially connects. For google for instance, you should set this to 'gmail.com'
205              
206             =item B<conference_server>
207              
208             conferencee server (usually conference.$server_name)
209              
210             =item B<port>
211              
212             Defaults to 5222
213              
214             =item B<tls>
215             Boolean value. defaults to 0. for google, it is know that this value must be 1 to work.
216              
217             =item B<connection_type>
218             defaults to 'tcpip' also takes 'http'
219              
220             =item B<username>
221              
222             The user you authenticate with to access the server. Not full name, just the stuff to the left of the @...
223              
224             =item B<password>
225              
226             password to get into the server
227              
228             =item B<alias>
229              
230             This will be your nickname in rooms, as well as the login resource (which can't have duplicates). I couldn't come up with any reason these should not be the same so hardcoded them to be the same.
231              
232             =item B<forums_and_responses>
233              
234             A hash ref which lists the forum names to join as the keys and the values are an array reference to a list of strings they are supposed to be responsive to.
235             The array is order sensitive and an empty string means it is going to respond to all messages in this forum. Make sure you list this last.
236              
237             The found 'response string' is assumed to be at the beginning of the message. The message_funtion function will be called with the modified string.
238              
239             alias = jbot:, attention:
240              
241             example1:
242              
243             message: 'jbot: help'
244              
245             passed to callback: 'help'
246              
247             =item B<message_function>
248              
249             The subroutine the bot will call when a new message is recieved by the bot. Only called if the bot's logic decides it's something you need to know about.
250              
251             =item B<background_function>
252              
253             The subroutine the bot will call when every so often (loop_sleep_time) to allow you to do background activities outside jabber stuff (check logs, web pages, etc.)
254              
255             =item B<loop_sleep_time>
256              
257             Frequency background function is called.
258              
259             =item B<process_timeout>
260              
261             Time Process() will wait if no new activity is received from the server
262              
263             =item B<ignore_server_messages>
264              
265             Boolean value as to whether we should ignore messages sent to us from the jabber server (addresses can be a little cryptic and hard to process)
266              
267             =item B<ignore_self_messages>
268              
269             Boolean value as to whether we should ignore messages sent by us.
270              
271             BE CAREFUL if you turn this on!!! Turning this on risks potentially endless loops. If you're going to do this, please be sure safety is turned on at least initially.
272              
273             =item B<out_messages_per_second>
274              
275             Limits the number of messages per second. Number must be <gt> 0
276              
277             default: 5
278              
279             safety: 5
280              
281             =item B<max_message_size>
282              
283             Specify maximimum size a message can be before it's split and sent in pieces.
284              
285             default: 1,000,000
286              
287             safetey: 1,000
288              
289             =item B<max_messages_per_hour>
290              
291             Limits the number of messages per hour before we refuse to send them
292              
293             default: 125
294              
295             safetey: 166
296              
297             =back
298              
299             =cut
300              
301             # Handle initialization of objects of this class...
302             sub BUILD {
303             my ($self, $params) = @_;
304              
305             # Deal with legacy bug
306             if($params->{background_activity} || $params->{message_callback}) {
307             my $warn_message = "\n\n"
308             . "*" x 70 . "\n"
309             . "WARNING!!! You're using old parameters for your bot initialization\n"
310             . "'message_callback' should be changed to 'message_function'\n"
311             . "'background_activity' should be changed to 'background_function'\n"
312             . "I'm correcting this, but you should fix your code\n"
313             . "*" x 70 . "\n"
314             . "\n\n";
315             warn($warn_message);
316             WARN($warn_message);
317              
318             $self->background_function($params->{background_activity})
319             if(!$self->background_function && $params->{background_activity});
320             $self->message_function($params->{message_callback})
321             if(!$self->message_function && $params->{message_callback});
322             sleep 30;
323             }
324            
325             # Message delay is inverse of out_messages_per_second
326             $self->message_delay(1/$self->out_messages_per_second);
327              
328             # Enforce all our safety restrictions here.
329             if($self->safety_mode) {
330             # more than 5 messages per second risks server flooding.
331             $self->message_delay(1/5) if($self->message_delay < 1/5);
332              
333             # Messages should be small to not overwhelm rooms/people/server
334             $self->max_message_size(1000) if($self->max_message_size > 1000);
335              
336             # More than 4,000 messages a day is a little excessive.
337             $self->max_messages_per_hour(125) if($self->max_messages_per_hour > 166);
338              
339             # Should not be responding to self messages to prevent loops.
340             $self->ignore_self_messages(1);
341             }
342              
343             #Initialize the connection.
344             $self->_init_jabber;
345             }
346              
347             # Return a code reference that will pass self in addition to arguements passed to callback code ref.
348             sub _callback_maker {
349             my $self = shift;
350             my $Function = shift;
351              
352             # return sub {return $code_ref->($self, @_);};
353             return sub {return $Function->($self, @_);};
354             }
355              
356             # Creates client object and manages connection. Called on new but also called by re-connect
357             sub _init_jabber {
358             my $self = shift;
359              
360             # Determine if the object already exists and if not, create it.
361             DEBUG("new client object.");
362             if(!$self->jabber_client) {
363             $self->jabber_client(Net::Jabber::Client->new);
364             }
365             my $connection = $self->jabber_client;
366              
367             DEBUG("Set the call backs.");
368             $connection->PresenceDB(); # Init presence DB.
369             $connection->RosterDB(); # Init Roster DB.
370             $connection->SetCallBacks( 'message' => $self->_callback_maker(\&_process_jabber_message)
371             ,'presence' => $self->_callback_maker(\&_jabber_presence_message)
372             ,'iq' => $self->_callback_maker(\&_jabber_in_iq_message)
373             );
374              
375             DEBUG("Connect. hostname => " . $self->server . ", port => " . $self->port);
376             my %client_connect_hash = (
377             hostname => $self->server,
378             port => $self->port,
379             tls => $self->tls,
380             connectiontype => $self->connection_type,
381             componentname => $self->server_host,
382             );
383              
384             my $status = $connection->Connect(%client_connect_hash);
385              
386             if(!defined $status) {
387             ERROR("ERROR: Jabber server is down or connection was not allowed: $!");
388             die("Jabber server is down or connection was not allowed: $!");
389             }
390              
391             DEBUG("Logging in... as user " . $self->username . " / " . $self->resource);
392             DEBUG("PW: " . $self->password);
393              
394             # Moved into connect hash via 'componentname'
395             # my $sid = $connection->{SESSION}->{id};
396             # $connection->{STREAM}->{SIDS}->{$sid}->{hostname} = $self->server_host;
397              
398              
399             my @auth_result = $connection->AuthSend(username => $self->username,
400             password => $self->password,
401             resource => $self->resource,
402             );
403              
404             if(!defined $auth_result[0] || $auth_result[0] ne "ok") {
405             ERROR("Authorization failed: for " . $self->username . " / " . $self->resource);
406             foreach my $result (@auth_result) {
407             ERROR("$result");
408             }
409             die("Failed to re-connect: " . join("\n", @auth_result));
410             }
411              
412             $connection->RosterRequest();
413              
414             $self->client_session_id($connection->{SESSION}->{id});
415              
416             DEBUG("Sending presence to tell world that we are logged in");
417             $connection->PresenceSend();
418             $self->Process(5);
419              
420             DEBUG("Getting Roster to tell server to send presence info");
421             $connection->RosterGet();
422             $self->Process(5);
423              
424             foreach my $forum (keys %{$self->forums_and_responses}) {
425             $self->JoinForum($forum);
426             }
427              
428             INFO("Connected to server '" . $self->server . "' successfully");
429             $self->connect_time(time); # Track when we came online.
430             return 1;
431             }
432              
433             =item B<JoinForum>
434              
435             Joins a jabber forum and sleeps safety time. Also prevents the object
436             from responding to messages for a grace period in efforts to get it to
437             not respond to historical messages. This has failed sometimes.
438              
439             NOTE: No error detection for join failure is present at the moment. (TODO)
440              
441             =cut
442              
443             sub JoinForum {
444             my $self = shift;
445             my $forum_name = shift;
446              
447             DEBUG("Joining $forum_name on " . $self->conference_server . " as " . $self->alias);
448              
449             $self->jabber_client->MUCJoin(room => $forum_name,
450             server => $self->conference_server,
451             nick => $self->alias,
452             );
453              
454             $self->forum_join_time->{$forum_name} = time;
455             DEBUG("Sleeping " . $self->message_delay . " seconds");
456             Time::HiRes::sleep $self->message_delay;
457             }
458              
459             =item B<Process>
460              
461             Mostly calls it's client connection's "Process" call.
462             Also assures a timeout is enforced if not fed to the subroutine
463             You really should not have to call this very often.
464             You should mostly be calling Start() and just let the Bot kernel handle all this.
465              
466             =cut
467              
468             sub Process { # Call connection process.
469             my $self = shift;
470             my $timeout_seconds = shift;
471              
472             #If not passed explicitly
473             $timeout_seconds = $self->process_timeout if(!defined $timeout_seconds);
474              
475             my $process_return = $self->jabber_client->Process($timeout_seconds);
476             return $process_return;
477             }
478              
479             =item B<Start>
480              
481             Primary subroutine save new called by the program. Does an endless loop of:
482             1. Process
483             2. If Process failed, Reconnect to server over larger and larger timeout
484             3. run background process fed from new, telling it who I am and how many loops we have been through.
485             4. Enforce a sleep to prevent server floods.
486              
487             =cut
488              
489             sub Start {
490             my $self = shift;
491              
492             my $time_between_background_routines = $self->loop_sleep_time;
493             my $process_timeout = $self->process_timeout;
494             my $background_subroutine = $self->background_function;
495             my $message_delay = $self->message_delay;
496              
497             my $last_background = time - $time_between_background_routines - 1; # Call background process every so often...
498             my $counter = 0; # Keep track of how many times we've looped. Not sure if we'll use this long term.
499              
500             while(1) { # Loop for ever!
501             # Process and re-connect if you have to.
502             my $reconnect_timeout = 1;
503             eval {$self->Process($process_timeout)};
504              
505             if($@) { #Assume the connection is down...
506             my $message = "Disconnected from " . $self->server . ":" . $self->port
507             . " as " . $self->username;
508             ERROR("$message Reconnecting...");
509             sleep 5; # TODO: Make re-connect time flexible somehow
510             $self->ReconnectToServer();
511             }
512              
513             # Call background function
514             if(defined $background_subroutine && $last_background + $time_between_background_routines < time) {
515             &$background_subroutine($self, ++$counter);
516             $last_background = time;
517             }
518             Time::HiRes::sleep $message_delay;
519             }
520             }
521              
522             =item B<ReconnectToServer>
523              
524             You should not ever need to use this. the Start() kernel usually figures this out and calls it.
525              
526             Internal process
527             1. Disconnects
528             3. Re-initializes
529              
530             =cut
531              
532             sub ReconnectToServer {
533             my $self = shift;
534              
535             my $background_subroutine = $self->background_function;
536              
537             $self->Disconnect();
538              
539             my $sleep_time = 5;
540             while (!$self->IsConnected()) { # jabber_client variable defines if we're connected.
541             INFO("Sleeping $sleep_time before attempting re-connect");
542             sleep $sleep_time;
543             $sleep_time *= 2 if($sleep_time < 300);
544             $self->InitJabber();
545             INFO("Running background routine.");
546             &$background_subroutine($self, 0); # call background proc so we can check for errors while down.
547             }
548             }
549              
550             =item B<Disconnect>
551              
552             Disconnects from server if client object is defined. Assures the client object is deleted.
553              
554             =cut
555              
556              
557             sub Disconnect {
558             my $self = shift;
559              
560             $self->connect_time('9' x 10); # Way in the future
561              
562             INFO("Disconnecting from server");
563             return if(!defined $self->jabber_client); # do not proceed, no object.
564              
565             $self->jabber_client->Disconnect();
566             my $old_client = $self->jabber_client;
567             $self->jabber_client(undef);
568              
569             DEBUG("Disconnected.");
570             return 1;
571             }
572              
573             =item B<IsConnected>
574              
575             Reports connect state (true/false) based on the status of client_start_time.
576              
577             =cut
578              
579             sub IsConnected {
580             my $self = shift;
581              
582             DEBUG("REF = " . ref($self->jabber_client));
583             return $self->connect_time;
584             }
585              
586             # TODO: ***NEED VERY GOOD DOCUMENTATION HERE*****
587             =item B<_process_jabber_message> - DO NOT CALL
588              
589             Handles incoming messages.
590              
591             =cut
592              
593             sub _process_jabber_message {
594             my $self = shift;
595             DEBUG("_process_jabber_message called");
596              
597             my $session_id = shift;
598             my $message = shift;
599              
600             my $type = $message->GetType();
601             my $fromJID = $message->GetFrom("jid");
602             my $from_full = $message->GetFrom();
603              
604             my $from = $fromJID->GetUserID();
605             my $resource = $fromJID->GetResource();
606             my $subject = $message->GetSubject();
607             my $body = $message->GetBody();
608              
609             my $reply_to = $from_full;
610             $reply_to =~ s/\/.*$// if($type eq 'groupchat');
611              
612             # TODO:
613             # Don't know exactly why but when a message comes from gtalk-web-interface, it works well, but if the message comes from Gtalk client, bot dies
614             # my $message_date_text; eval { $message_date_text = $message->GetTimeStamp(); } ; # Eval is a really bad idea. we need to understand why this is failing.
615              
616             # my $message_date_text = $message->GetTimeStamp(); # Since we're not using the data, we'll turn this off since it crashes gtalk clients aparently?
617             # my $message_date = UnixDate($message_date_text, "%s") - 1*60*60; # Convert to EST from CST;
618              
619             # Ignore any messages within 10 seconds of start or join of that forum
620             my $grace_period = $self->forum_join_grace;
621             my $time_now = time;
622             if($self->connect_time > $time_now - $grace_period
623             || (defined $self->forum_join_time->{$from} && $self->forum_join_time->{$from} > $time_now - $grace_period)) {
624             my $cond1 = $self->connect_time . " > $time_now - $grace_period";
625             my $cond2 = $self->forum_join_time->{$from} || 'undef'
626             . " > $time_now - $grace_period";
627             DEBUG("Ignoring messages cause I'm in startup for forum $from\n$cond1\n$cond2");
628             return; # Ignore messages the first few seconds.
629             }
630              
631             # Ignore Group messages with no resource on them. (Server Messages?)
632             if($self->ignore_server_messages) {
633             if($from_full !~ m/^([^\@]+)\@([^\/]+)\/(.+)$/) {
634             DEBUG("Server message? ($from_full) - $message");
635             return if($from_full !~ m/^([^\@]+)\@([^\/]+)\//);
636             ERROR("Couldn't recognize from_full ($from_full). Ignoring message: $body");
637             return;
638             }
639             }
640              
641             # Are these my own messages?
642             if($self->ignore_self_messages ) { # TODO: || $self->safety_mode (this breaks tests in 06?)
643            
644             if(defined $resource && $resource eq $self->resource) { # Ignore my own messages.
645             DEBUG("Ignoring message from self...\n");
646             return;
647             }
648             }
649              
650             # Determine if this message was addressed to me. (groupchat only)
651             my $bot_address_from;
652             my @aliases_to_respond_to = $self->get_responses($from);
653              
654             if($#aliases_to_respond_to >= 0 and $type eq 'groupchat') {
655             my $request;
656             foreach my $address_type (@aliases_to_respond_to) {
657             my $qm_address_type = quotemeta($address_type);
658             next if($body !~ m/^\s*$qm_address_type\s*(\S.*)$/ms);
659             $request = $1;
660             $bot_address_from = $address_type;
661             last; # do not need to loop any more.
662             }
663             DEBUG("Message not relevant to bot");
664             return if(!defined $request);
665             $body = $request;
666             }
667              
668             # Call the message callback if it's defined.
669             if( defined $self->message_function) {
670             $self->message_function->(bot_object => $self,
671             from_full => $from_full,
672             body => $body,
673             type => $type,
674             reply_to => $reply_to,
675             bot_address_from => $bot_address_from,
676             message => $message
677             );
678             return;
679             } else {
680             WARN("No handler for messages!");
681             INFO("New Message: $type from $from ($resource). sub=$subject -- $body");
682             }
683             }
684              
685             =item B<get_responses>
686              
687             $bot->get_ident($forum_name);
688              
689             Returns the array of messages we are monitoring for in supplied forum or replies with undef.
690              
691             =cut
692              
693             sub get_responses {
694             my $self = shift;
695              
696             my $forum = shift;
697              
698             if(!defined $forum) {
699             WARN("No forum supplied for get_responses()");
700             return;
701             }
702              
703             my @aliases_to_respond_to;
704             if(defined $self->forums_and_responses->{$forum}) {
705             @aliases_to_respond_to = @{$self->forums_and_responses->{$forum}};
706             }
707              
708             return @aliases_to_respond_to;
709             }
710              
711              
712             =item B<_jabber_in_iq_message> - DO NOT CALL
713              
714             Called when the client receives new messages during Process of this type.
715              
716             =cut
717              
718             sub _jabber_in_iq_message {
719             my $self = shift;
720              
721             my $session_id = shift;
722             my $iq = shift;
723              
724             DEBUG("IQ Message:" . $iq->GetXML());
725             my $from = $iq->GetFrom();
726             # my $type = $iq->GetType();DEBUG("Type=$type");
727             my $query = $iq->GetQuery();#DEBUG("query=" . Dumper($query));
728             my $xmlns = $query->GetXMLNS();DEBUG("xmlns=$xmlns");
729             my $iqReply;
730              
731             # Respond to version requests with information about myself.
732             if($xmlns eq "jabber:iq:version") {
733             # convert 5.010000 to 5.10.0
734             my $perl_version = $];
735             $perl_version =~ s/(\d{3})(?=\d)/$1./g;
736             $perl_version =~ s/\.0+(\d)/.$1/;
737            
738             $self->jabber_client
739             ->VersionSend(to=> $from,
740             name=>__PACKAGE__,
741             ver=> $VERSION,
742             os=> "Perl v$perl_version");
743             } else { # Unknown request. Just ignore it.
744             return;
745             }
746              
747             if($iqReply) {
748             DEBUG("Reply: ", $iqReply->GetXML());
749             $self->jabber_client->Send($iqReply);
750             }
751              
752             # INFO("IQ from $from ($type). XMLNS: $xmlns");
753             }
754              
755             =item B<_jabber_presence_message> - DO NOT CALL
756              
757             Called when the client receives new presence messages during Process.
758             Mostly we are just pushing the data down into the client DB for later processing.
759              
760             =cut
761              
762             sub _jabber_presence_message {
763             my $self = shift;
764              
765             my $session_id = shift;
766             my $presence = shift;
767              
768             my $type = $presence->GetType();
769             if($type eq 'subscribe') { # Always allow people to subscribe to us. Why wouldn't we?
770             my $from = $presence->GetFrom();
771             $self->jabber_client->Subscription(type=>"subscribe",
772             to=>$from);
773             $self->jabber_client->Subscription(type=>"subscribed",to=>$from);
774             INFO("Processed subscription request from $from");
775             return;
776             } elsif($type eq 'unsubscribe') { # Always allow people to subscribe to us. Why wouldn't we?
777             my $from = $presence->GetFrom();
778             $self->jabber_client->Subscription(type=>"unsubscribed",
779             to=>$from);
780             INFO("Processed unsubscribe request from $from");
781             return;
782             }
783              
784             $self->jabber_client->PresenceDBParse($presence); # Since we are always an object just throw it into the db.
785              
786             my $from = $presence->GetFrom();
787             $from = "." if(!defined $from);
788              
789             my $status = $presence->GetStatus();
790             $status = "." if(!defined $status);
791              
792             DEBUG("Presence From $from t=$type s=$status");
793             DEBUG("Presence XML: " . $presence->GetXML());
794             }
795              
796             =item B<respond_to_self_messages>
797              
798             $bot->respond_to_self_messages($value = 1);
799              
800              
801             Tells the bot to start reacting to it\'s own messages if non-zero is passed. Default is 1.
802              
803             =cut
804              
805              
806             sub respond_to_self_messages {
807             my $self = shift;
808              
809             my $setting = shift;
810             $setting = 1 if(!defined $setting);
811              
812             $self->ignore_self_messages(!$setting);
813             return !!$setting;
814             }
815              
816             =item B<get_messages_this_hour>
817              
818             $bot->get_messages_this_hour();
819              
820             replys with number of messages sent so far this hour.
821              
822             =cut
823              
824              
825             sub get_messages_this_hour {
826             my $self = shift;
827              
828             my $yday = (localtime)[7];
829             my $hour = (localtime)[2];
830             my $messages_this_hour = $self->messages_sent_today->{$yday}->{$hour};
831             return $messages_this_hour || 0; # Assure it's not undef to avoid math warnings
832             }
833              
834             =item B<get_safety_mode>
835              
836             Validates that we are in safety mode. Returns a bool as long as we are an object, otherwise returns undef
837              
838             =cut
839              
840             sub get_safety_mode {
841             my $self = shift;
842              
843             # Must be in safety mode and all thresholds met.
844             my $mode = $self->safety_mode
845             && $self->message_delay >= 1/5
846             && $self->max_message_size <= 1000
847             && $self->max_messages_per_hour <= 166
848             && $self->ignore_self_messages
849             ;
850            
851             return $mode || 0;
852             }
853              
854             =item B<SendGroupMessage>
855              
856             $bot->SendGroupMessage($name, $message);
857              
858             Tells the bot to send a message to the recipient room name
859              
860             =cut
861              
862             sub SendGroupMessage {
863             my $self = shift;
864             my $recipient = shift;
865             my $message = shift;
866              
867             $recipient .= '@' . $self->conference_server if($recipient !~ m{\@});
868              
869             return $self->SendJabberMessage($recipient, $message, 'groupchat');
870             }
871              
872             =item B<SendPersonalMessage>
873              
874             $bot->SendPersonalMessage($recipient, $message);
875              
876             How to send an individual message to someone.
877              
878             $recipient must read as user@server/Resource or it will not send.
879              
880             =cut
881              
882             sub SendPersonalMessage {
883             my $self = shift;
884             my $recipient = shift;
885             my $message = shift;
886              
887             return $self->SendJabberMessage($recipient, $message, 'chat');
888             }
889              
890             =item B<SendJabberMessage>
891              
892             $bot->SendJabberMessage($recipient, $message, $message_type, $subject);
893              
894             The master subroutine to send a message. Called either by the user, SendPersonalMessage, or SendGroupMessage. Sometimes there
895             is call to call it directly when you do not feel like figuring you messaged you.
896             Assures message size does not exceed a limit and chops it into pieces if need be.
897              
898             NOTE: non-printable characters (unicode included) will be stripped before sending to the server via:
899             s/[^[:print:]]+/./xmsg
900              
901             =cut
902              
903             sub SendJabberMessage {
904             my $self = shift;
905              
906             my $recipient = shift;
907             my $message = shift;
908             my $message_type = shift;
909             my $subject = shift;
910              
911             my $max_size = $self->max_message_size;
912              
913             # Split the message into no more than max_message_size so that we do not piss off jabber.
914             # Split on new line. Space if you have to or just chop at max size.
915             my @message_chunks = ( $message =~ /.{1,$max_size}$|.{1,$max_size}\n|.{1,$max_size}\s|.{1,$max_size}/gs );
916              
917              
918             DEBUG("Max message = $max_size. Splitting...") if($#message_chunks > 0);
919             my $return_value;
920             foreach my $message_chunk (@message_chunks) {
921             my $msg_return = $self->_send_individual_message($recipient, $message_chunk, $message_type, $subject);
922             if(defined $msg_return) {
923             $return_value .= $msg_return;
924             }
925             }
926             return $return_value;
927             }
928              
929             # $self->_send_individual_message($recipient, $message_chunk, $message_type, $subject);
930             # Private subroutine only called directly by SetForumSubject and SendJabberMessage.
931             # There are a bunch of fancy things this does, but the important things are:
932             # 1. sleep a minimum of .2 seconds every message
933             # 2. Make sure we have not sent too many messages this hour and block sends if they are attempted over a certain limit (max limit is 125)
934             # 3. Strip out special characters that will get us booted from the server.
935              
936             sub _send_individual_message {
937             my $self = shift;
938              
939             my $recipient = shift;
940             my $message_chunk = shift;
941             my $message_type = shift;
942             my $subject = shift;
943              
944             if(!defined $message_type) {
945             ERROR("Undefined \$message_type");
946             return "No message type!\n";
947             }
948              
949             if(!defined $recipient) {
950             ERROR('$recipient not defined!');
951             return "No recipient!\n";
952             }
953              
954             my $yday = (localtime)[7];
955             my $hour = (localtime)[2];
956             my $messages_this_hour = $self->messages_sent_today->{$yday}->{$hour} += 1;
957              
958             if($messages_this_hour > $self->max_messages_per_hour) {
959             $subject = "" if(!defined $subject); # Keep warning messages quiet.
960             $message_chunk = "" if(!defined $message_chunk); # Keep warning messages quiet.
961              
962             ERROR("Can't Send message because we've already tried to send $messages_this_hour of $self->max_messages_per_hour messages this hour.\n"
963             . "To: $recipient\n"
964             . "Subject: $subject\n"
965             . "Type: $message_type\n"
966             . "Message sent:\n"
967             . "$message_chunk"
968             );
969              
970             # Send 1 panic message out to jabber if this is our last message before quieting down.
971             return "Too many messages ($messages_this_hour)\n";
972             }
973              
974             if(!$self->IsConnected) {
975             $subject = "" if(!defined $subject); # Keep warning messages quiet.
976             $message_chunk = "" if(!defined $message_chunk); # Keep warning messages quiet.
977              
978             ERROR("Can't Jabber server is down. Tried to send: \n"
979             . "To: $recipient\n"
980             . "Subject: $subject\n"
981             . "Type: $message_type\n"
982             . "Message sent:\n"
983             . "$message_chunk"
984             );
985              
986             # Send 1 panic message out to jabber if this is our last message before quieting down.
987             return "Server is down.\n";
988             }
989              
990             # Strip out anything that's not a printable character
991             # Now with unicode support?
992             $message_chunk =~ s/[^[:print:]]+/./xmsg;
993              
994             my $message_length = length($message_chunk);
995             DEBUG("Sending message $yday-$hour-$messages_this_hour $message_length bytes to $recipient");
996             $self->jabber_client->MessageSend(to => $recipient
997             , body => $message_chunk
998             , type => $message_type
999             # , from => $connection_hash{$obj_ID}{'from_full'}
1000             , subject => $subject
1001             );
1002              
1003             DEBUG("Sleeping " . $self->message_delay . " after sending message.");
1004             Time::HiRes::sleep $self->message_delay; #Throttle messages.
1005              
1006             if($messages_this_hour == $self->max_messages_per_hour) {
1007             $self->jabber_client->MessageSend(to => $recipient
1008             , body => "Cannot send more messages this hour. "
1009             . "$messages_this_hour of " . $self->max_messages_per_hour . " already sent."
1010             , type => $message_type
1011             );
1012             }
1013             return; # Means we succeeded!
1014             }
1015              
1016             =item B<SetForumSubject>
1017              
1018             $bot->SetForumSubject($recipient, $subject);
1019              
1020             Sets the subject of a forum
1021              
1022             =cut
1023              
1024             sub SetForumSubject {
1025             my $self = shift;
1026              
1027             my $recipient = shift;
1028             my $subject = shift;
1029              
1030             if(length $subject > $self->max_message_size) {
1031             my $subject_len = length($subject);
1032             ERROR("Someone tried to send a subject message $subject_len bytes long!");
1033             my $subject = substr($subject, 0, $self->max_message_size);
1034             DEBUG("Truncated subject: $subject");
1035             return "Subject is too long!";
1036             }
1037             $self->_send_individual_message($recipient, "Setting subject to $subject", 'groupchat', $subject);
1038              
1039             return;
1040             }
1041              
1042             =item B<ChangeStatus>
1043              
1044             $bot->ChangeStatus($presence_mode, $status_string);
1045              
1046             Sets the Bot's presence status.
1047             $presence mode could be something like: (Chat, Available, Away, Ext. Away, Do Not Disturb).
1048             $status_string is an optional comment to go with your presence mode. It is not required.
1049              
1050             =cut
1051              
1052             sub ChangeStatus {
1053             my $self = shift;
1054             my $presence_mode = shift;
1055             my $status_string = shift; # (optional)
1056              
1057             $self->jabber_client->PresenceSend(show=>$presence_mode, status=>$status_string);
1058              
1059             return 1;
1060             }
1061              
1062             =item B<GetRoster>
1063              
1064             $bot->GetRoster();
1065              
1066             Returns a list of the people logged into the server.
1067             I suspect we really want to know who is in a paticular forum right?
1068             In which case we need another sub for this.
1069             =cut
1070              
1071             sub GetRoster {
1072             my $self = shift;
1073              
1074             my @rosterlist;
1075             foreach my $jid ($self->jabber_client->RosterDBJIDs()) {
1076             my $username =$jid->GetJID();
1077             push(@rosterlist, $username) ;
1078             }
1079             return @rosterlist;
1080             }
1081              
1082             =item B<GetStatus>
1083              
1084             Need documentation from Yago on this sub.
1085              
1086             =cut
1087              
1088             sub GetStatus {
1089              
1090             my $self = shift;
1091             my ($jid) = shift;
1092              
1093             my $Pres = $self->jabber_client->PresenceDBQuery($jid);
1094              
1095             if (!(defined($Pres))) {
1096              
1097             return "unavailable" ;
1098             }
1099              
1100             my $show = $Pres->GetShow();
1101             if ($show) {
1102              
1103             return $show;
1104             }
1105              
1106             return "available";
1107              
1108             }
1109              
1110             =item B<AddUser>
1111              
1112             Need documentation from Yago on this sub.
1113              
1114             =cut
1115              
1116             sub AddUser {
1117             my $self = shift;
1118             my $user = shift;
1119              
1120             $self->jabber_client->Subscription(type=>"subscribe", to=>$user);
1121             $self->jabber_client->Subscription(type=>"subscribed",to=>$user);
1122             }
1123              
1124             =item B<RmUser>
1125              
1126             Need documentation from Yago on this sub.
1127              
1128             =cut
1129              
1130             sub RmUser {
1131             my $self = shift;
1132             my $user = shift;
1133              
1134             $self->jabber_client->Subscription(type=>"unsubscribe", to=>$user);
1135             $self->jabber_client->Subscription(type=>"unsubscribed",to=>$user);
1136             }
1137             =back
1138              
1139             =head1 AUTHOR
1140              
1141             Todd Rinaldo C<< <perl-net-jabber-bot@googlegroups.com) > >>
1142              
1143             =head1 BUGS
1144              
1145             Please report any bugs or feature requests to
1146             C<perl-net-jabber-bot@googlegroups.com>, or through the web interface at
1147             L<http://code.google.com/p/perl-net-jabber-bot/issues/entry>.
1148             I will be notified, and then you'll automatically be notified of progress on
1149             your bug as I make changes.
1150              
1151             =head1 SUPPORT
1152              
1153             You can find documentation for this module with the perldoc command.
1154              
1155             perldoc Net::Jabber::Bot
1156              
1157             You can also look for information at:
1158              
1159             =over 4
1160              
1161             =item * AnnoCPAN: Annotated CPAN documentation
1162              
1163             L<http://annocpan.org/dist/Net-Jabber-Bot>
1164              
1165             =item * CPAN Ratings
1166              
1167             L<http://cpanratings.perl.org/d/Net-Jabber-Bot>
1168              
1169             =item * Search CPAN
1170              
1171             L<http://search.cpan.org/dist/Net-Jabber-Bot>
1172              
1173             =item * Project homepage
1174              
1175             L<http://code.google.com/p/perl-net-jabber-bot/>
1176              
1177             =item * Google Issue Tracker (reporting bugs)
1178              
1179             L<http://code.google.com/p/perl-net-jabber-bot/issues/entry>
1180              
1181             =back
1182              
1183             =head1 ACKNOWLEDGEMENTS
1184              
1185             =head1 COPYRIGHT & LICENSE
1186              
1187             Copyright 2007 Todd E Rinaldo, all rights reserved.
1188              
1189             This program is free software; you can redistribute it and/or modify it
1190             under the same terms as Perl itself.
1191              
1192             =cut
1193              
1194             __PACKAGE__->meta->make_immutable;
1195             no Moose;
1196             no MooseX::Types;
1197             1; # End of Net::Jabber::Bot