File Coverage

blib/lib/AnyEvent/SparkBot.pm
Criterion Covered Total %
statement 64 208 30.7
branch 0 60 0.0
condition 0 12 0.0
subroutine 22 27 81.4
pod 3 4 75.0
total 89 311 28.6


line stmt bran cond sub pod time code
1             package AnyEvent::SparkBot;
2              
3             our $VERSION='1.015';
4 2     2   1448 use Modern::Perl;
  2         7  
  2         15  
5 2     2   1566 use Moo;
  2         5317  
  2         10  
6 2     2   2399 use MooX::Types::MooseLike::Base qw(:all);
  2         13699  
  2         646  
7 2     2   16 use Scalar::Util qw( looks_like_number);
  2         4  
  2         108  
8 2     2   14 use Data::Dumper;
  2         4  
  2         106  
9 2     2   15 use namespace::clean;
  2         4  
  2         23  
10 2     2   2560 use AnyEvent::HTTP::MultiGet;
  2         216892  
  2         75  
11 2     2   1191 use AnyEvent::WebSocket::Client;
  2         391096  
  2         88  
12 2     2   18 use MIME::Base64;
  2         5  
  2         145  
13 2     2   699 use JSON;
  2         8075  
  2         14  
14 2     2   1520 use AnyEvent::HTTP::Spark;
  2         6  
  2         103  
15              
16             BEGIN {
17 2     2   17 no namespace::clean;
  2         4  
  2         14  
18 2     2   1067 with 'HTTP::MultiGet::Role', 'AnyEvent::SparkBot::SharedRole';
19             }
20              
21             =head1 NAME
22              
23             AnyEvent::SparkBot - Cisco Spark WebSocket Client for the AnyEvent Loop
24              
25             =head1 SYNOPSIS
26              
27             use Modern::Perl;
28             use Data::Dumper;
29             use AnyEvent::SparkBot;
30             use AnyEvent::Loop;
31             $|=1;
32              
33             our $obj=new AnyEvent::SparkBot(token=>$ENV{SPARK_TOKEN},on_message=>\&cb);
34              
35             $obj->que_getWsUrl(sub {
36             my ($agent,$id,$result)=@_;
37              
38             # start here if we got a valid connection
39             return $obj->start_connection if $result;
40             $obj->handle_reconnect;
41             });
42             $obj->agent->run_next;
43             AnyEvent::Loop::run;
44              
45             sub cb {
46             my ($sb,$result,$eventType,$verb,$json)=@_;
47             return unless $eventType eq 'conversation.activity' and $verb eq 'post';
48              
49             # Data::Result Object is False when combination of EvenType and Verb are unsupprted
50             if($result) {
51             my $data=$result->get_data;
52             my $response={
53             roomId=>$data->{roomId},
54             personId=>$data->{personId},
55             text=>"ya.. ya ya.. I'm on it!"
56             };
57             # Proxy our lookup in a Retry-After ( prevents a lot of errors )
58             $obj->run_lookup('que_createMessage',(sub {},$response);
59             } else {
60             print "Error: $result\n";
61             }
62             }
63              
64             =head1 DESCRIPTION
65              
66             Connects to cisco spark via a websocket. By itself this class only provides connectivty to Spark, the on_message callback is used to handle events that come in. By default No hanlder is provided.
67              
68             =head1 Moo Role(s)
69              
70             This module uses the following Moo role(s)
71              
72             HTTP::MultiGet::Role
73             AnyEvent::SparkBot::SharedRole
74              
75             =cut
76              
77             has retryTimeout=>(
78             is=>'ro',
79             isa=>Int,
80             default=>10,
81             lazy=>1,
82             );
83              
84             has retryCount=>(
85             is=>'ro',
86             isa=>Int,
87             default=>1,
88             lazy=>1,
89             );
90              
91             has retries=>(
92             is=>'ro',
93             isa=>HashRef,
94             lazy=>1,
95             default=>sub { {} },
96             required=>0,
97             );
98              
99             has reconnect_sleep=>(
100             is=>'ro',
101             isa=>Int,
102             default=>10,
103             required=>1,
104             );
105              
106             has reconnect=>(
107             is=>'ro',
108             isa=>Bool,
109             default=>1,
110             required=>1,
111             );
112              
113             has pingEvery=>(
114             is=>'ro',
115             isa=>Int,
116             default=>60,
117             );
118              
119             has pingWait=>(
120             is=>'ro',
121             isa=>Int,
122             default=>10,
123             );
124              
125             has ping=>(
126             is=>'rw',
127             );
128              
129             has lastPing=>(
130             is=>'rw',
131             isa=>Str,
132             lazy=>1,
133             );
134              
135             has connInfo=>(
136             is=>'rw',
137             lazy=>1,
138             default=>sub { {} },
139             );
140              
141             has deviceDesc=>(
142             is=>'ro',
143             isa=>Str,
144             default=>'{"deviceName":"perlwebscoket-client","deviceType":"DESKTOP","localizedModel":"nodeJS","model":"nodeJS","name":"perl-spark-client","systemName":"perl-spark-client","systemVersion":"'.$VERSION.'"}',
145             );
146              
147             has defaultUrl=>(
148             is=>'ro',
149             isa=>Str,
150             default=>'https://wdm-a.wbx2.com/wdm/api/v1/devices',
151             #default=>'https://webexapis.com/wdm/api/v1/devices',
152             );
153              
154             has lastConn=>(
155             isa=>Str,
156             is=>'ro',
157             required=>1,
158             default=>'/tmp/sparkBotLastConnect.json',
159             );
160              
161             has connection=>(
162             is=>'rw',
163             isa=>Object,
164             required=>0,
165             );
166              
167             has on_message=>(
168             is=>'ro',
169             isa=>CodeRef,
170             required=>1,
171             );
172              
173             has spark=>(
174             is=>'rw',
175             isa=>Object,
176             required=>0,
177             lazy=>1,
178             );
179              
180             has currentUser=>(
181             is=>'rw',
182             isa=>HashRef,
183             required=>0,
184             lazy=>1,
185             default=>sub {return {}}
186             );
187              
188             =head1 OO Arguments and accessors
189              
190             Required Argument(s)
191              
192             token: The token used to authenticate the bot
193             on_message: code ref used to handle incomming messages
194              
195             Optional Arguments
196              
197             reconnect: default is true
198             logger: null(default) or an instance of log4perl::logger
199             lastConn: location to the last connection file
200             # it may be a very good idea to set this value
201             # default: /tmp/sparkBotLastConnect.json
202             defaultUrl: https://wdm-a.wbx2.com/wdm/api/v1/devices
203             # this is where we authenticate and pull the websocket url from
204             deviceDesc: JSON hash, representing the client description
205             agent: an instance of AnyEvent::HTTP::MultiGet
206             retryTimeout: default 10, sets how long to wait afer getting a 429 error
207             retryCount: default 1, sets how many retries when we get a 429 error
208              
209             Timout and retry values:
210              
211             pingEvery: 60 # used to check how often we run a ping
212             # pings only happen if no inbound request has come in for
213             # the interval
214             pingWait: 10
215             # how long to wait for a ping response
216             reconnect_sleep: 10
217             # how long to wait before we try to reconnect
218              
219             Objects set at runtime:
220              
221             lastConn: sets the location of the last connection file
222             ping: sets an object that will wake up and do something
223             lastPing: contains the last ping string value
224             connection: contains the current websocket connection if any
225             spark: Instance of AnyEvent::HTTP::Spark
226             currentUser: Hash ref representing the current bot user
227              
228             =cut
229              
230             # This method runs after the new constructor
231             sub BUILD {
232 2     2 0 1056 my ($self)=@_;
233              
234 2         41 my $sb=new AnyEvent::HTTP::Spark(agent=>$self->agent,token=>$self->token);
235 2         42 $self->spark($sb);
236             }
237              
238             # this method runs before the new constructor, and can be used to change the arguments passed to the module
239             around BUILDARGS => sub {
240             my ($org,$class,@args)=@_;
241            
242             return $class->$org(@args);
243             };
244              
245             =head1 OO Methods
246              
247             =over 4
248              
249             =item * my $result=$self->new_true({qw( some data )});
250              
251             Returns a new true Data::Result object.
252              
253             =item * my $result=$self->new_false("why this failed")
254              
255             Returns a new false Data::Result object
256              
257             =item * my $self->start_connection()
258              
259             Starts the bot up.
260              
261             =cut
262              
263             sub start_connection : BENCHMARK_DEBUG {
264 0         0 my ($self)=@_;
265              
266 0         0 my $url=$self->connInfo->{webSocketUrl};
267              
268             $self->run_lookup('que_getMe',sub {
269 0         0 my ($sb,$id,$result)=@_;
270 0 0       0 return $self->log_error("Could not get spark Bot user info?") unless $result;
271              
272 0         0 $self->currentUser($result->get_data);
273 0         0 });
274 0         0 $self->agent->run_next;
275 0         0 my $client=AnyEvent::WebSocket::Client->new;
276              
277             $client->connect($url)->cb(sub {
278 0         0 my $conn=eval { shift->recv };
  0         0  
279              
280 0 0       0 if($@) {
281 0         0 $self->log_error("Failed to cnnect to our web socket, error was: $@");
282 0         0 return $self->handle_reconnect;
283             }
284              
285 0         0 $self->connection($conn);
286 0         0 $conn->on(finish=>sub { $self->handle_reconnect() });
  0         0  
287 0         0 $self->setPing();
288              
289              
290 0         0 $conn->send(to_json({
291             id=>$self->uuidv4,
292             type=>'authorization',
293             data=>{
294             token=>'Bearer '.$self->token,
295             }
296             }));
297              
298 0         0 $conn->on(each_message=>sub { $self->handle_message(@_) });
  0         0  
299 0         0 });
300              
301 2     2   8069 }
  2         5  
  2         16  
302              
303              
304             =item * $self->handle_message($connection,$message)
305              
306             Handles incoming messages
307              
308             =cut
309              
310             sub handle_message : BENCHMARK_INFO {
311 0         0 my ($self,$conn,$message)=@_;
312 0         0 my $json=eval { from_json($message->body) };
  0         0  
313 0         0 $self->ping(undef);
314 0 0       0 if($@) {
315 0         0 $self->log_error("Failed to parse message, error was: $@");
316 0         0 $self->handle_reconnect;
317 0         0 return;
318             }
319              
320 0 0 0     0 if(exists $json->{type} && $json->{type} eq 'pong') {
321 0 0       0 if($json->{id} ne $self->lastPing) {
322 0         0 $self->log_error('Got a bad ping back?');
323 0         0 return $self->handle_reconnect;
324             } else {
325 0         0 $self->log_debug("got a ping response");
326 0         0 return $self->setPing();
327             }
328             } else {
329 0 0 0     0 if(exists $json->{data} and exists $json->{data}->{eventType} and exists $json->{data}->{activity} ) {
      0        
330 0         0 my $activity=$json->{data}->{activity};
331 0         0 my $eventType=$json->{data}->{eventType};
332 0 0       0 $eventType='unknown' unless defined $eventType;
333 0 0       0 if(exists $activity->{verb}) {
334 0         0 my $verb=$activity->{verb};
335 0 0       0 $verb='unknown' unless defined($verb);
336 0 0       0 if($eventType eq 'conversation.activity') {
337 0 0 0     0 if($verb=~ /post|share/) {
    0          
    0          
    0          
338 0 0       0 if(exists $activity->{actor}) {
339 0         0 my $actor=$activity->{actor};
340              
341 0 0       0 if($self->currentUser->{displayName} eq $actor->{displayName}) {
342 0         0 $self->log_debug("ignoring message because we sent it");
343 0         0 $self->setPing();
344 0         0 return;
345             }
346             $self->run_lookup('que_getMessage',sub {
347 0         0 my ($agent,$id,$result,$req,$resp)=@_;
348 0         0 $self->on_message->($self,$result,$eventType,$verb,$json,$req,$resp,$message);
349 0         0 },$activity->{id});
350             }
351             } elsif($verb eq 'add' and $activity->{object}->{objectType} eq 'person') {
352             my $args={
353             roomId=>$activity->{target}->{id},
354             personEmail=>$activity->{object}->{emailAddress},
355 0         0 };
356             $self->run_lookup('que_listMemberships',sub {
357 0         0 my ($agent,$id,$result,$req,$resp)=@_;
358 0         0 $self->on_message->($self,$result,$eventType,$verb,$json,$req,$resp,$message);
359 0         0 },$args);
360             } elsif($verb eq 'create') {
361             my $args={
362             personId=>$self->currentUser->{id},
363 0         0 };
364             $self->run_lookup('que_listMemberships',sub {
365 0         0 my ($agent,$id,$result,$req,$resp)=@_;
366 0         0 $self->on_message->($self,$result,$eventType,$verb,$json,$req,$resp,$message);
367 0         0 },$args);
368             } elsif($verb=~ /lock|unlock|update/) {
369             $self->run_lookup('que_getRoom',sub {
370 0         0 my ($agent,$id,$result,$req,$resp)=@_;
371 0         0 $self->on_message->($self,$result,$eventType,$verb,$json,$req,$resp,$message);
372 0         0 },$activity->{object}->{id});
373             } else {
374 0         0 $self->on_message->($self,$self->new_false("Unsupported EventType: [$eventType] and Verb: [$verb]"),$eventType,$verb,$json);
375             }
376             } else {
377 0         0 $self->on_message->($self,$self->new_false("Unsupported EventType: [$eventType] and Verb: [$verb]"),$eventType,$verb,$json);
378             }
379             } else {
380 0 0       0 my $eventType=defined($json->{data}->{eventType}) ? $json->{data}->{eventType} : 'unknown';
381 0 0       0 my $verb=defined($json->{data}->{activity}->{verb}) ? $json->{data}->{activity}->{verb} : 'unknown';
382 0         0 $self->on_message->($self,$self->new_false("Unsupported EventType: [$eventType] and Verb: [$verb]"),$eventType,'unknown',$json);
383             }
384             } else {
385 0 0       0 my $eventType=defined($json->{data}->{eventType}) ? $json->{data}->{eventType} : 'unknown';
386 0 0       0 my $verb=defined($json->{data}->{activity}->{verb}) ? $json->{data}->{activity}->{verb} : 'unknown';
387 0         0 $self->on_message->($self,$self->new_false("Unsupported EventType: [$eventType] and Verb: [$verb]"),$eventType,$verb,$json);
388             }
389             }
390 0         0 $self->setPing();
391 2     2   2965 }
  2         5  
  2         9  
392              
393             =item * $self->run_lookup($method,$cb,@args);
394              
395             Shortcut for:
396              
397             $self->spark->$method($cb,@args);
398             $self->agent->run_next;
399              
400             =cut
401              
402             sub run_lookup {
403 0     0 1   my ($self,$method,$cb,@args)=@_;
404            
405 0           $self->spark->$method($cb,@args);
406 0           $self->agent->run_next;
407             }
408              
409              
410             =item * $self->handle_reconnect()
411              
412             Handles reconnecting to spark
413              
414             =cut
415              
416             sub handle_reconnect : BENCHMARK_INFO {
417 0         0 my ($self)=@_;
418 0         0 $self->ping(undef);
419 0 0       0 $self->connection->close if $self->connection;
420              
421             my $ping=AnyEvent->timer(after=>$self->reconnect_sleep,cb=>sub {
422 0         0 $self->que_getWsUrl(sub { $self->start_connection });
  0         0  
423 0         0 $self->agent->run_next;
424 0         0 });
425 0         0 $self->ping($ping);
426 2     2   1025 }
  2         6  
  2         8  
427              
428             =item * $self->setPing()
429              
430             Sets the next ping object
431              
432             =cut
433              
434             sub setPing {
435 0     0 1   my ($self)=@_;
436              
437 0           $self->ping(undef);
438             my $ping=AnyEvent->timer(after=>$self->pingEvery,cb=>sub {
439              
440 0 0   0     unless($self->connection) {
441 0           $self->ping(undef);
442 0           $self->log_error('current conenction is not valid?');
443 0           return;
444             }
445 0           my $id=$self->uuidv4;
446 0           $self->lastPing($id);
447 0           $self->connection->send(to_json({ type=>'ping', id=> $id, }));
448 0           $self->setPingWait;
449 0           });
450 0           $self->ping($ping);
451             }
452              
453             =item * $self->setPingWait()
454              
455             This method is called by ping, sets a timeout to wait for the response.
456              
457             =cut
458              
459             sub setPingWait {
460 0     0 1   my ($self)=@_;
461 0           $self->ping(undef);
462             my $wait=AnyEvent->timer(after=>$self->pingWait,cb=>sub {
463 0     0     $self->ping(undef);
464 0           $self->handle_reconnect;
465 0           });
466 0           $self->ping($wait);
467             }
468              
469             =item * my $result=$self->getLastConn()
470              
471             Fetches the last connection info
472              
473             Returns a Data::Result Object, when true it contains the hash, when false it contains why it failed.
474              
475             =cut
476              
477             sub getLastConn : BENCHMARK_DEBUG {
478 0         0 my ($self)=@_;
479              
480 0         0 my $lc=$self->lastConn;
481 0 0       0 if(-r $lc) {
482 0         0 my $fh=IO::File->new($lc,'r');
483 0 0       0 return $self->new_false("Could not open file: $lc, error was: $!") unless $fh;
484              
485 0         0 my $str=join '',$fh->getlines;
486 0         0 $fh->close;
487              
488 0         0 my $json=eval { from_json($str) };
  0         0  
489 0 0       0 if($@) {
490 0         0 return $self->new_false("Could not parse $lc, error was: $@");
491             }
492              
493 0         0 return $self->new_true($json);
494             }
495              
496 0         0 return $self->new_false("Could not read $lc");
497 2     2   1516 }
  2         5  
  2         9  
498              
499             =item * my $result=$self->saveLastConn($ref)
500              
501             Saves the last conenction, returns a Data::Result Object
502              
503             $ref is assumed to be the data strucutre intended to be serialzied into json
504              
505             =cut
506              
507             sub saveLastConn : BENCHMARK_DEBUG {
508 0         0 my ($self,$ref)=@_;
509 0         0 my $json=to_json($ref,{pretty=>1});
510              
511 0         0 my $fh=IO::File->new($self->lastConn,'w');
512 0 0       0 return $self->new_false("Failed to create: [".$self->lastConn."] error was: [$!]") unless $fh;
513              
514 0         0 $fh->print($json);
515              
516 0         0 return $self->new_true($json);
517 2     2   847 }
  2         6  
  2         9  
518              
519             =item * my $job_id=$self->que_deleteLastUrl($cb)
520              
521             Returns a Data::Result Object, when true it contains the url that was deleted, when false it contains why it failed.
522              
523             =cut
524              
525             sub que_deleteLastUrl : BENCHMARK_INFO {
526 0         0 my ($self,$cb)=@_;
527 0         0 my $result=$self->getLastConn();
528              
529 0 0       0 return $self->queue_result($cb,$result) unless $result;
530              
531 0         0 my $json=$result->get_data;
532 0 0       0 return $self->queue_result($cb,$self->new_false('URL not found in json data strucutre')) unless exists $json->{url};
533 0         0 my $url=$json->{url};
534              
535 0         0 my $req=new HTTP::Request(DELETE=>$url,$self->default_headers);
536 0         0 return $self->queue_request($req,$cb);
537 2     2   854 }
  2         5  
  2         9  
538              
539             =item * my $job_id=$self->que_getWsUrl($cb)
540              
541             Gets the WebSocket URL
542              
543             Returns a Data::Result Object: When true it contains the url. When false it contains why it failed.
544              
545             =cut
546              
547             sub que_getWsUrl : BENCHMARK_INFO {
548 0         0 my ($self,$cb)=@_;
549            
550 0         0 $self->que_deleteLastUrl(\&log_delete_call);
551              
552             my $run_cb=sub {
553 0         0 my ($self,$id,$result)=@_;
554              
555 0 0       0 if($result) {
556 0         0 my $json=$result->get_data;
557 0         0 $self->connInfo($json);
558 0         0 $self->saveLastConn($json);
559             }
560            
561 0         0 $cb->(@_);
562 0         0 };
563 0         0 my $req=new HTTP::Request(POST=>$self->defaultUrl,$self->default_headers,$self->deviceDesc);
564 0         0 return $self->queue_request($req,$run_cb);
565 2     2   910 }
  2         5  
  2         8  
566              
567             =item * $self->log_delete_call($id,$result)
568              
569             Call back to handle logging clean up of previous session
570              
571             =cut
572              
573             sub log_delete_call : BENCHMARK_INFO {
574 0         0 my ($self,$id,$result)=@_;
575 0 0       0 if($result) {
576 0         0 $self->log_always("Removed old device object without error");
577             } else {
578 0         0 $self->log_always("Failed to remove old device, error was: $result");
579             }
580 2     2   723 }
  2         36  
  2         10  
581              
582             =back
583              
584             =head1 AUTHOR
585              
586             Michael Shipper <AKALINUX@CPAN.ORG>
587              
588             =cut
589              
590             1;