File Coverage

blib/lib/AnyEvent/SlackRTM.pm
Criterion Covered Total %
statement 30 117 25.6
branch 2 28 7.1
condition 2 14 14.2
subroutine 10 33 30.3
pod 11 11 100.0
total 55 203 27.0


line stmt bran cond sub pod time code
1             package AnyEvent::SlackRTM;
2             $AnyEvent::SlackRTM::VERSION = '1.3';
3 5     5   348194 use v5.14;
  5         65  
4              
5             # ABSTRACT: AnyEvent module for interacting with the Slack RTM API
6              
7 5     5   1594 use AnyEvent;
  5         8746  
  5         172  
8 5     5   2066 use AnyEvent::WebSocket::Client 0.12;
  5         1015700  
  5         160  
9 5     5   36 use Carp;
  5         11  
  5         240  
10 5     5   1940 use Furl;
  5         72954  
  5         127  
11 5     5   2544 use JSON;
  5         39171  
  5         24  
12 5     5   471 use Try::Tiny;
  5         9  
  5         796  
13              
14             our $START_URL = 'https://slack.com/api/rtm.connect';
15              
16              
17             sub new {
18 4     4 1 1293 my ($class, $token, $client_opts) = @_;
19              
20 4   100     13 $client_opts //= {};
21 4 100       139 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   157 $client = AnyEvent::WebSocket::Client->new(%$client_opts);
26             } catch {
27 0     0   0 croak "Can't create client object: $_";
28 3         17 };
29              
30 3         2131 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   31 use vars qw( $VERSION );
  5         9  
  5         4516  
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             # We've now asked to re-open the connection,
67             # so don't close again on timeout.
68 0           delete $self->{closed};
69              
70             $self->{client}->connect( $start->{url} )->cb( sub {
71 0     0     my $client = shift;
72              
73 0           delete $self->{finished};
74 0           $self->{started}++;
75 0           $self->{id} = 1;
76              
77 0           my $conn = $self->{conn} = $client->recv;
78 0           $conn->on( each_message => sub { $self->_handle_incoming(@_) } );
  0            
79 0           $conn->on( finish => sub { $self->_handle_finish(@_) } );
  0            
80              
81 0           my $started = localtime;
82 0           $self->{_last_keep_alive} = time;
83             $self->{keep_alive} = AnyEvent->timer(
84             after => 15,
85             interval => 15,
86             cb => sub {
87 0           my $id = $self->{id};
88 0           my $now = time;
89 0           my $since = $now - $self->{_last_keep_alive};
90 0 0         if ( $since > 30 ) {
    0          
91             # will trigger a finish, which will reconnect
92             # if $self->{closed} is not set.
93 0           $conn->close;
94             }
95             elsif ( $since > 10 ) {
96 0           $self->ping( { keep_alive => $now } );
97             }
98             },
99 0           );
100 0           } );
101             }
102              
103              
104 0   0 0 1   sub metadata { shift->{metadata} // {} }
105             sub quiet {
106 0     0 1   my $self = shift;
107              
108 0 0         if (@_) {
109 0           $self->{quiet} = shift;
110             }
111              
112 0   0       return $self->{quiet} // '';
113             }
114              
115              
116             sub on {
117 0     0 1   my ($self, %registrations) = @_;
118              
119 0           for my $type (keys %registrations) {
120 0           my $cb = $registrations{ $type };
121 0           $self->{registry}{$type} = $cb;
122             }
123             }
124              
125              
126             sub off {
127 0     0 1   my ($self, @types) = @_;
128 0           delete $self->{registry}{$_} for @types;
129             }
130              
131             sub _do {
132 0     0     my ($self, $type, @args) = @_;
133              
134 0 0         if (defined $self->{registry}{$type}) {
135 0           $self->{registry}{$type}->($self, @args);
136             }
137             }
138              
139              
140             sub send {
141 0     0 1   my ($self, $msg) = @_;
142              
143             croak "Cannot send because the Slack connection is not started"
144 0 0         unless $self->{started};
145             croak "Cannot send because Slack has not yet said hello"
146 0 0         unless $self->{said_hello};
147             croak "Cannot send because the connection is finished"
148 0 0         if $self->{finished};
149              
150 0           $msg->{id} = $self->{id}++;
151              
152 0           $self->{conn}->send(encode_json($msg));
153             }
154              
155              
156             sub ping {
157 0     0 1   my ($self, $msg) = @_;
158              
159             $self->send({
160 0   0       %{ $msg // {} },
  0            
161             type => 'ping'
162             });
163             }
164              
165             sub _handle_incoming {
166 0     0     my ($self, $conn, $raw) = @_;
167              
168             my $msg = try {
169 0     0     decode_json($raw->body);
170             }
171             catch {
172 0     0     my $message = $raw->body;
173 0           croak "unable to decode incoming message: $message";
174 0           };
175              
176 0           $self->{_last_keep_alive} = time;
177              
178             # Handle errors when they occur
179 0 0         if ($msg->{error}) {
    0          
    0          
180 0           $self->_handle_error($conn, $msg);
181             }
182              
183             # Handle the initial hello
184             elsif ($msg->{type} eq 'hello') {
185 0           $self->_handle_hello($conn, $msg);
186             }
187              
188             # Periodic response to our pings
189             elsif ($msg->{type} eq 'pong') {
190 0           $self->_handle_pong($conn, $msg);
191             }
192              
193             # And anything else...
194             else {
195 0           $self->_handle_other($conn, $msg);
196             }
197             }
198              
199              
200 0   0 0 1   sub said_hello { shift->{said_hello} // '' }
201 0   0 0 1   sub finished { shift->{finished} // '' }
202              
203             sub _handle_hello {
204 0     0     my ($self, $conn, $msg) = @_;
205              
206 0           $self->{said_hello}++;
207              
208 0           $self->_do(hello => $msg);
209             }
210              
211             sub _handle_error {
212 0     0     my ($self, $conn, $msg) = @_;
213              
214             carp "Error #$msg->{error}{code}: $msg->{error}{msg}"
215 0 0         unless $self->{quiet};
216              
217 0           $self->_do(error => $msg);
218             }
219              
220             sub _handle_pong {
221 0     0     my ($self, $conn, $msg) = @_;
222              
223 0           $self->_do($msg->{type}, $msg);
224             }
225              
226             sub _handle_other {
227 0     0     my ($self, $conn, $msg) = @_;
228              
229 0           $self->_do($msg->{type}, $msg);
230             }
231              
232             sub _handle_finish {
233 0     0     my ($self, $conn) = @_;
234              
235             # Cancel the keep_alive watchdog
236 0           undef $self->{keep_alive};
237              
238 0           $self->{finished}++;
239              
240 0           $self->_do('finish');
241              
242 0 0         $self->start unless $self->{closed};
243             }
244              
245              
246             sub close {
247 0     0 1   my ($self) = @_;
248 0           $self->{closed}++;
249 0           $self->{conn}->close;
250             }
251              
252             __END__