File Coverage

blib/lib/AnyEvent/SlackRTM.pm
Criterion Covered Total %
statement 30 109 27.5
branch 2 22 9.0
condition 2 14 14.2
subroutine 10 33 30.3
pod 11 11 100.0
total 55 189 29.1


line stmt bran cond sub pod time code
1             package AnyEvent::SlackRTM;
2             $AnyEvent::SlackRTM::VERSION = '1.2';
3 5     5   404947 use v5.14;
  5         61  
4              
5             # ABSTRACT: AnyEvent module for interacting with the Slack RTM API
6              
7 5     5   2123 use AnyEvent;
  5         11826  
  5         164  
8 5     5   2657 use AnyEvent::WebSocket::Client 0.12;
  5         1264693  
  5         233  
9 5     5   50 use Carp;
  5         10  
  5         295  
10 5     5   2581 use Furl;
  5         90053  
  5         153  
11 5     5   2993 use JSON;
  5         42879  
  5         28  
12 5     5   613 use Try::Tiny;
  5         13  
  5         918  
13              
14             our $START_URL = 'https://slack.com/api/rtm.start';
15              
16              
17             sub new {
18 4     4 1 1360 my ($class, $token, $client_opts) = @_;
19              
20 4   100     14 $client_opts //= {};
21 4 100       160 croak "Client options must be passed as a HashRef" unless ref $client_opts eq 'HASH';
22              
23 3         4 my $client;
24             try {
25 3     3   206 $client = AnyEvent::WebSocket::Client->new(%$client_opts);
26             } catch {
27 0     0   0 croak "Can't create client object: $_";
28 3         20 };
29              
30 3         2326 return bless {
31             token => $token,
32             client => $client,
33             registry => {},
34             }, $class;
35             }
36              
37              
38             sub start {
39 0     0 1   my $self = shift;
40              
41 5     5   32 use vars qw( $VERSION );
  5         10  
  5         4318  
42 0   0       $VERSION //= '*-devel';
43              
44             my $furl = Furl->new(
45             agent => "AnyEvent::SlackRTM/$VERSION",
46             timeout => $self->{client}->timeout,
47 0           );
48              
49 0           my $res = $furl->get($START_URL . '?token=' . $self->{token});
50             my $start = try {
51 0     0     decode_json($res->content);
52             }
53             catch {
54 0     0     my $status = $res->status;
55 0           my $message = $res->content;
56 0           croak "unable to start, Slack call failed: $status $message";
57 0           };
58              
59 0           my $ok = $start->{ok};
60 0 0         croak "unable to start, Slack returned an error: $start->{error}"
61             unless $ok;
62              
63             # Store this stuff in case we want it
64 0           $self->{metadata} = $start;
65              
66 0           my $wss = $start->{url};
67 0           my $client = $self->{client};
68              
69             $client->connect($wss)->cb(sub {
70 0     0     my $client = shift;
71              
72             my $conn = try {
73 0           $client->recv;
74             }
75             catch {
76 0           die $_;
77 0           };
78              
79 0           $self->{started}++;
80 0           $self->{id} = 1;
81              
82 0           $self->{conn} = $conn;
83              
84             $self->{pinger} = AnyEvent->timer(
85             after => 60,
86             interval => 60,
87 0           cb => sub { $self->ping },
88 0           );
89              
90 0           $conn->on(each_message => sub { $self->_handle_incoming(@_) });
  0            
91 0           $conn->on(finish => sub { $self->_handle_finish(@_) });
  0            
92 0           });
93             }
94              
95              
96 0   0 0 1   sub metadata { shift->{metadata} // {} }
97             sub quiet {
98 0     0 1   my $self = shift;
99              
100 0 0         if (@_) {
101 0           $self->{quiet} = shift;
102             }
103              
104 0   0       return $self->{quiet} // '';
105             }
106              
107              
108             sub on {
109 0     0 1   my ($self, %registrations) = @_;
110              
111 0           for my $type (keys %registrations) {
112 0           my $cb = $registrations{ $type };
113 0           $self->{registry}{$type} = $cb;
114             }
115             }
116              
117              
118             sub off {
119 0     0 1   my ($self, @types) = @_;
120 0           delete $self->{registry}{$_} for @types;
121             }
122              
123             sub _do {
124 0     0     my ($self, $type, @args) = @_;
125              
126 0 0         if (defined $self->{registry}{$type}) {
127 0           $self->{registry}{$type}->($self, @args);
128             }
129             }
130              
131              
132             sub send {
133 0     0 1   my ($self, $msg) = @_;
134              
135             croak "Cannot send because the Slack connection is not started"
136 0 0         unless $self->{started};
137             croak "Cannot send because Slack has not yet said hello"
138 0 0         unless $self->{said_hello};
139             croak "Cannot send because the connection is finished"
140 0 0         if $self->{finished};
141              
142 0           $msg->{id} = $self->{id}++;
143              
144 0           $self->{conn}->send(encode_json($msg));
145             }
146              
147              
148             sub ping {
149 0     0 1   my ($self, $msg) = @_;
150              
151             $self->send({
152 0   0       %{ $msg // {} },
  0            
153             type => 'ping'
154             });
155             }
156              
157             sub _handle_incoming {
158 0     0     my ($self, $conn, $raw) = @_;
159              
160             my $msg = try {
161 0     0     decode_json($raw->body);
162             }
163             catch {
164 0     0     my $message = $raw->body;
165 0           croak "unable to decode incoming message: $message";
166 0           };
167              
168             # Handle errors when they occur
169 0 0         if ($msg->{error}) {
    0          
    0          
170 0           $self->_handle_error($conn, $msg);
171             }
172              
173             # Handle the initial hello
174             elsif ($msg->{type} eq 'hello') {
175 0           $self->_handle_hello($conn, $msg);
176             }
177              
178             # Periodic response to our pings
179             elsif ($msg->{type} eq 'pong') {
180 0           $self->_handle_pong($conn, $msg);
181             }
182              
183             # And anything else...
184             else {
185 0           $self->_handle_other($conn, $msg);
186             }
187             }
188              
189              
190 0   0 0 1   sub said_hello { shift->{said_hello} // '' }
191 0   0 0 1   sub finished { shift->{finished} // '' }
192              
193             sub _handle_hello {
194 0     0     my ($self, $conn, $msg) = @_;
195              
196 0           $self->{said_hello}++;
197              
198 0           $self->_do(hello => $msg);
199             }
200              
201             sub _handle_error {
202 0     0     my ($self, $conn, $msg) = @_;
203              
204             carp "Error #$msg->{error}{code}: $msg->{error}{msg}"
205 0 0         unless $self->{quiet};
206              
207 0           $self->_do(error => $msg);
208             }
209              
210             sub _handle_pong {
211 0     0     my ($self, $conn, $msg) = @_;
212              
213 0           $self->_do($msg->{type}, $msg);
214             }
215              
216             sub _handle_other {
217 0     0     my ($self, $conn, $msg) = @_;
218              
219 0           $self->_do($msg->{type}, $msg);
220             }
221              
222             sub _handle_finish {
223 0     0     my ($self, $conn) = @_;
224              
225             # Cancel the pinger
226 0           undef $self->{pinger};
227              
228 0           $self->{finished}++;
229              
230 0           $self->_do('finish');
231             }
232              
233              
234 0     0 1   sub close { shift->{conn}->close }
235              
236             __END__