File Coverage

blib/lib/Net/Jabber/Bot.pm
Criterion Covered Total %
statement 210 360 58.3
branch 47 116 40.5
condition 12 36 33.3
subroutine 26 36 72.2
pod 19 20 95.0
total 314 568 55.2


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