File Coverage

blib/lib/WebService/Mattermost/V4/Client.pm
Criterion Covered Total %
statement 55 137 40.1
branch 4 30 13.3
condition 2 17 11.7
subroutine 16 27 59.2
pod 0 3 0.0
total 77 214 35.9


line stmt bran cond sub pod time code
1             package WebService::Mattermost::V4::Client;
2              
3             # ABSTRACT: Perl websocket client for Mattermost.
4              
5 2     2   183662 use Encode 'encode';
  2         14801  
  2         137  
6 2     2   602 use Mojo::IOLoop;
  2         330825  
  2         29  
7 2     2   107 use Mojo::JSON qw(decode_json encode_json);
  2         5  
  2         99  
8 2     2   541 use Moo;
  2         6635  
  2         10  
9 2     2   2503 use MooX::HandlesVia;
  2         7009  
  2         14  
10 2     2   846 use Types::Standard qw(ArrayRef Bool InstanceOf Int Maybe Str);
  2         74854  
  2         18  
11              
12             extends 'WebService::Mattermost';
13             with    qw(
14             WebService::Mattermost::Role::Logger
15             WebService::Mattermost::Role::UserAgent
16             Role::EventEmitter
17             );
18              
19             ################################################################################
20              
21             has _events => (is => 'ro', isa => ArrayRef, lazy => 1, builder => 1);
22             has _ua => (is => 'rw', isa => InstanceOf['Mojo::UserAgent'], lazy => 1, builder => 1);
23             has ioloop => (is => 'rw', isa => InstanceOf['Mojo::IOLoop'], lazy => 1, builder => 1);
24             has websocket_url => (is => 'ro', isa => Str, lazy => 1, builder => 1);
25              
26             has ws => (is => 'rw', isa => Maybe[InstanceOf['Mojo::Base']]);
27              
28             has debug => (is => 'ro', isa => Bool, default => 0);
29             has ignore_self => (is => 'ro', isa => Bool, default => 1);
30             has ping_interval => (is => 'ro', isa => Int, default => 15);
31             has reconnection_wait_time => (is => 'ro', isa => Int, default => 2);
32             has reauthentication_interval => (is => 'ro', isa => Int, default => 3600);
33              
34             has last_seq => (is => 'rw', isa => Int, default => 1,
35                 handles_via => 'Number',
36                 handles => {
37                     inc_last_seq => 'add',
38                 });
39              
40             has loops => (is => 'rw', isa => ArrayRef[InstanceOf['Mojo::IOLoop']], default => sub { [] },
41                 handles_via => 'Array',
42                 handles => {
43                     add_loop => 'push',
44                     clear_loops => 'clear',
45                 });
46              
47             ################################################################################
48              
49             sub BUILD {
50 12     12 0 26     my $self = shift;
51              
52 12         274     $self->authenticate(1);
53 12         407     $self->next::method(@_);
54              
55             # Set up expected subroutines for a child class to catch. The events can
56             # also be caught raw in a script.
57 12         22     foreach my $emission (@{$self->_events}) {
  12         205  
58             # Values from events must be set up in child class
59 60 50       490         if ($self->can($emission)) {
60 0     0   0             $self->on($emission, sub { shift; $self->$emission(@_) });
  0         0  
  0         0  
61                     }
62                 }
63              
64 12         158     return 1;
65             }
66              
67             sub start {
68 2     2 0 1638     my $self = shift;
69              
70 2         44     $self->_connect();
71 2 50       65     $self->ioloop->start unless $self->ioloop->is_running();
72              
73 2         329     return;
74             }
75              
76             sub message_has_content {
77 3     3 0 2034     my $self = shift;
78 3         9     my $args = shift;
79              
80 3   66     48     return $args->{post_data} && $args->{post_data}->{message};
81             }
82              
83             ################################################################################
84              
85             sub _connect {
86 2     2   6     my $self = shift;
87              
88 2     1   53     $self->_ua->on(start => sub { $self->_on_start(@_) });
  1         889  
89              
90                 $self->_ua->websocket($self->websocket_url => sub {
91 0     0   0         my ($ua, $tx) = @_;
92              
93 0         0         $self->ws($tx);
94              
95 0 0       0         unless ($tx->is_websocket) {
96 0         0             $self->logger->fatal('WebSocket handshake failed');
97                     }
98              
99 0         0         $self->emit(gw_ws_started => {});
100              
101 0         0         $self->logger->debug('Adding ping loop');
102              
103 0         0         $self->add_loop($self->ioloop->recurring(15 => sub { $self->_ping($tx) }));
  0         0  
104 0         0         $self->add_loop($self->ioloop->recurring($self->reauthentication_interval => sub { $self->_reauthenticate() }));
  0         0  
105              
106 0         0         $tx->on(error => sub { $self->_on_error(@_) });
  0         0  
107 0         0         $tx->on(finish => sub { $self->_on_finish(@_) });
  0         0  
108 0         0         $tx->on(message => sub { $self->_on_message(@_) });
  0         0  
109 2         180     });
110              
111 2         1052     return 1;
112             }
113              
114             sub _ping {
115 0     0   0     my $self = shift;
116 0         0     my $tx = shift;
117              
118 0 0       0     if ($self->debug) {
119 0         0         $self->logger->debugf('[Seq: %d] Sending ping', $self->last_seq);
120                 }
121              
122 0         0     return $tx->send(encode_json({
123                     seq => $self->last_seq,
124                     action => 'ping',
125                 }));
126             }
127              
128             sub _on_start {
129 1     1   3     my $self = shift;
130 1         4     my $ua = shift;
131 1         3     my $tx = shift;
132              
133 1 50       6     if ($self->debug) {
134 0         0         $self->logger->debugf('UserAgent connected to %s', $tx->req->url->to_string);
135 0         0         $self->logger->debugf('Auth token: %s', $self->auth_token);
136                 }
137              
138             # The methods here are from the UserAgent role
139 1         5     $tx->req->headers->header('Cookie' => $self->mmauthtoken($self->auth_token));
140 1         36     $tx->req->headers->header('Authorization' => $self->bearer($self->auth_token));
141 1         25     $tx->req->headers->header('Keep-Alive' => 1);
142              
143 1         34     return 1;
144             }
145              
146             sub _on_finish {
147 0     0   0     my $self = shift;
148 0         0     my $tx = shift;
149 0         0     my $code = shift;
150 0   0     0     my $reason = shift || 'Unknown';
151              
152 0         0     $self->logger->infof('WebSocket connection closed: [%d] %s', $code, $reason);
153 0         0     $self->logger->infof('Reconnecting in %d seconds...', $self->reconnection_wait_time);
154              
155 0         0     $self->ws->finish;
156 0         0     $self->emit(gw_ws_finished => { code => $code, reason => $reason });
157              
158             # Delay the reconnection a little
159                 Mojo::IOLoop->timer($self->reconnection_wait_time => sub {
160 0     0   0         return $self->_reconnect();
161 0         0     });
162             }
163              
164             sub _on_message {
165 0     0   0     my $self = shift;
166 0         0     my $tx = shift;
167 0         0     my $input = shift;
168              
169 0 0       0     return unless $input;
170              
171 0         0     my $message = decode_json(encode('utf8', $input));
172              
173 0 0       0     if ($message->{seq}) {
174 0 0       0         $self->logger->debugf('[Seq: %d]', $message->{seq}) if $self->debug;
175 0         0         $self->last_seq($message->{seq});
176                 }
177              
178 0 0 0     0     return $self->_on_non_event($message) unless $message && $message->{event};
179              
180 0         0     my $message_args = { message => $message };
181              
182 0 0       0     if ($message->{data}->{post}) {
183 0         0         my $post_data = decode_json(encode('utf8', $message->{data}->{post}));
184              
185             # Early return if the message is from the bot's own user ID (to halt
186             # recursion)
187 0 0 0     0         return if $self->ignore_self && $post_data->{user_id} eq $self->user_id;
188              
189 0         0         $message_args->{post_data} = $post_data;
190                 }
191              
192 0         0     $self->emit(gw_message => $message_args);
193              
194 0 0       0     if ($message->{event} eq 'hello') {
195 0 0       0         if ($self->debug) {
196 0         0             $self->logger->debug('Received "hello" event, sending authentication challenge');
197                     }
198              
199 0         0         $tx->send(encode_json({
200                         seq => 1,
201                         action => 'authentication_challenge',
202                         data => { token => $self->auth_token },
203                     }));
204                 }
205              
206 0         0     return 1;
207             }
208              
209             sub _on_non_event {
210 0     0   0     my $self = shift;
211 0         0     my $message = shift;
212              
213 0 0 0     0     if ($self->debug && $message->{data} && $message->{data}->{text}) {
      0        
214 0         0         $self->logger->debugf('[Seq: %d] Received %s', $self->last_seq, $message->{data}->{text});
215                 }
216              
217 0         0     return $self->emit(gw_message_no_event => $message);
218             }
219              
220             sub _on_error {
221 0     0   0     my $self = shift;
222 0         0     my $ws = shift;
223 0         0     my $message = shift;
224              
225 0         0     $self->emit(gw_ws_error => { message => $message });
226              
227 0         0     return $ws->finish($message);
228             }
229              
230             sub _reauthenticate {
231 0     0   0     my $self = shift;
232              
233             # Mattermost authentication tokens expire after a given (and unknown) amount
234             # of time. By default, the client will reconnect every hour in order to
235             # refresh the token.
236 0         0     $self->authenticate(1);
237 0         0     $self->_try_authentication();
238              
239 0         0     return 1;
240             }
241              
242             sub _reconnect {
243 0     0   0     my $self = shift;
244              
245             # Reset things which have been altered during the course of the last
246             # connection
247 0         0     $self->last_seq(1);
248 0         0     $self->_try_authentication();
249 0         0     $self->_clean_up_loops();
250 0         0     $self->ws(undef);
251 0         0     $self->_ua($self->_build__ua);
252              
253 0         0     return $self->_connect();
254             }
255              
256             sub _clean_up_loops {
257 0     0   0     my $self = shift;
258              
259 0         0     foreach my $loop (@{$self->loops}) {
  0         0  
260 0         0         $self->ioloop->remove($loop);
261                 }
262              
263 0         0     return $self->clear_loops();
264             }
265              
266             ################################################################################
267              
268             sub _build__events {
269 12     12   316     return [ qw(
270             gw_ws_error
271             gw_ws_finished
272             gw_ws_started
273             gw_message
274             gw_message_no_event
275             ) ];
276             }
277              
278 2     2   31 sub _build__ua { Mojo::UserAgent->new }
279              
280 2     2   35 sub _build_ioloop { Mojo::IOLoop->singleton }
281              
282             sub _build_websocket_url {
283 3     3   830     my $self = shift;
284              
285             # Convert the API URL to the WebSocket URL
286 3         26     my $ws_url = $self->base_url;
287              
288 3 50       15     if ($ws_url !~ /\/$/) {
289 3         14         $ws_url .= '/';
290                 }
291              
292 3         10     $ws_url .= 'websocket';
293 3         15     $ws_url =~ s/^http(?:s)?/wss/s;
294              
295 3         57     return $ws_url;
296             }
297              
298             ################################################################################
299              
300             1;
301              
302             __END__
303            
304             =pod
305            
306             =encoding UTF-8
307            
308             =head1 NAME
309            
310             WebService::Mattermost::V4::Client - Perl websocket client for Mattermost.
311            
312             =head1 VERSION
313            
314             version 0.26
315            
316             =head1 DESCRIPTION
317            
318             This class connects to Mattermost via the WebSocket gateway and can either be
319             extended in a child class, or used in a script.
320            
321             =head2 USAGE
322            
323             =head3 FROM A SCRIPT
324            
325             use WebService::Mattermost::V4::Client;
326            
327             my $bot = WebService::Mattermost::V4::Client->new({
328             username => 'usernamehere',
329             password => 'password',
330             base_url => 'https://mattermost.server.com/api/v4/',
331            
332             # Optional arguments
333             debug => 1, # Show extra connection information
334             ignore_self => 0, # May cause recursion!
335             });
336            
337             $bot->on(message => sub {
338             my ($bot, $args) = @_;
339            
340             # $args contains the decoded message content
341             });
342            
343             $bot->start(); # Add me last
344            
345             =head3 EXTENSION
346            
347             See L<WebService::Mattermost::V4::Example::Bot>.
348            
349             =head2 EVENTS
350            
351             Events are either available to be caught with C<on> in scripts, or have methods
352             which can be overridden in child classes.
353            
354             =over 4
355            
356             =item C<gw_ws_started>
357            
358             The bot connected to the Mattermost gateway. Can be overridden as
359             C<gw_ws_started()>.
360            
361             =item C<gw_ws_finished>
362            
363             The bot disconnected from the Mattermost gateway. Can be overridden as
364             C<gw_ws_finished()>.
365            
366             =item C<gw_message>
367            
368             The bot received a message. Can be overridden as C<gw_message()>.
369            
370             =item C<gw_ws_error>
371            
372             The bot received an error. Can be overridden as C<gw_error()>.
373            
374             =item C<gw_message_no_event>
375            
376             The bot received a message without an event (which is usually a "ping" item).
377             Can be overridden as C<gw_message_no_event()>.
378            
379             =back
380            
381             =head1 AUTHOR
382            
383             Mike Jones <mike@netsplit.org.uk>
384            
385             =head1 COPYRIGHT AND LICENSE
386            
387             This software is Copyright (c) 2020 by Mike Jones.
388            
389             This is free software, licensed under:
390            
391             The MIT (X11) License
392            
393             =cut
394