File Coverage

blib/lib/Net/Mattermost/Bot.pm
Criterion Covered Total %
statement 32 110 29.0
branch 0 32 0.0
condition 0 14 0.0
subroutine 11 28 39.2
pod 5 7 71.4
total 48 191 25.1


line stmt bran cond sub pod time code
1             package Net::Mattermost::Bot;
2              
3 1     1   55557 use 5.6.1;
  1         4  
4              
5 1     1   4 use Carp qw(carp croak);
  1         2  
  1         38  
6 1     1   389 use Furl;
  1         22822  
  1         27  
7 1     1   369 use HTTP::Status ':is';
  1         3312  
  1         174  
8 1     1   345 use JSON::MaybeXS qw(encode_json decode_json);
  1         4543  
  1         66  
9 1     1   9 use List::Util 'pairs';
  1         3  
  1         91  
10 1     1   495 use Mojo::IOLoop;
  1         208737  
  1         6  
11 1     1   433 use Mojo::UserAgent;
  1         108920  
  1         7  
12 1     1   465 use Moo;
  1         7860  
  1         5  
13 1     1   1710 use MooX::HandlesVia;
  1         3740  
  1         5  
14 1     1   564 use Types::Standard qw(ArrayRef Bool HashRef Int Object Str);
  1         58722  
  1         10  
15              
16             our $VERSION = '0.04';
17              
18             ################################################################################
19              
20             has base_url => (is => 'ro', isa => Str, required => 1);
21             has team_name => (is => 'ro', isa => Str, required => 1);
22             has username => (is => 'ro', isa => Str, required => 1);
23             has password => (is => 'ro', isa => Str, required => 1);
24              
25             has debug => (is => 'ro', isa => Bool, default => 0);
26             has ping_interval => (is => 'ro', isa => Int, default => 15);
27             has ssl_opts => (is => 'ro', isa => HashRef, default => sub { {} });
28             has token => (is => 'rw', isa => Str, default => '');
29             has user_id => (is => 'rw', isa => Str, default => '');
30              
31             has api_url => (is => 'ro', isa => Str, lazy => 1, builder => '_build_api_url');
32             has endpoints => (is => 'ro', isa => HashRef, lazy => 1, builder => '_build_endpoints');
33             has furl => (is => 'ro', isa => Object, lazy => 1, builder => '_build_furl');
34             has headers => (is => 'rw', isa => ArrayRef, lazy => 1, builder => '_build_headers',
35             handles_via => 'Array',
36             handles => { add_header => 'push' });
37             has ws_url => (is => 'ro', isa => Str, lazy => 1, builder => '_build_ws_url');
38              
39             ################################################################################
40              
41             sub connect {
42 0     0 0   my $self = shift;
43              
44 0           my $login_endpoint = sprintf('%s/users/login', $self->api_url);
45              
46 0           my $init = $self->furl->post($login_endpoint, $self->headers, encode_json({
47             name => $self->team_name,
48             login_id => $self->username,
49             password => $self->password,
50             }));
51              
52 0           my $out = decode_json($init->{content});
53              
54 0 0 0       if ($out->{status_code} && !is_success($init->{status_code})) {
55 0           croak $out->{message};
56             }
57              
58 0 0         if ($init->header('Token')) {
59 0           $self->token($init->header('Token'));
60 0           $self->user_id($out->{id});
61 0           $self->add_header(
62             Cookie => sprintf('MMAUTHTOKEN=%s', $self->token),
63             Authorization => sprintf('Bearer %s', $self->token),
64             'Keep-Alive' => 1,
65             );
66             } else {
67 0           croak 'Unauthorized';
68             }
69              
70 0           $self->event_connected();
71 0           $self->_start();
72              
73 0           return 1;
74             }
75              
76             sub handle_message {
77 0     0 0   my $self = shift;
78 0           my $content = shift;
79              
80             # Filter out empty responses and messages from ourself
81 0 0 0       return unless $content && $content->{event};
82              
83 0 0         if ($content->{data}->{post}) {
84 0           my $post_data = decode_json($content->{data}->{post});
85              
86 0 0         return if $post_data->{user_id} eq $self->user_id;
87             }
88              
89 0           my $output;
90              
91 0 0         if ($content->{event} eq 'hello') {
    0          
    0          
    0          
92 0 0         if ($self->debug) {
93 0           carp sprintf('Sending auth token (token: %s) at %d', $self->token, time());
94             }
95              
96             $output = {
97 0           seq => 1,
98             action => 'authentication_challenge',
99             data => { token => $self->token },
100             };
101             } elsif ($content->{event} eq 'typing') {
102 0           $output = $self->event_typing($content);
103             } elsif ($content->{event} eq 'channel_viewed') {
104 0           $output = $self->event_channel_viewed($content);
105             } elsif ($content->{event} eq 'posted') {
106 0           $output = $self->event_posted($content);
107             } else {
108 0           $output = $self->event_generic($content);
109             }
110              
111 0           return $output;
112             }
113              
114             # Override these
115       0 1   sub event_connected {}
116       0 1   sub event_typing {}
117       0 1   sub event_channel_viewed {}
118       0 1   sub event_posted {}
119       0 1   sub event_generic {}
120              
121             ################################################################################
122              
123             sub _start {
124 0     0     my $self = shift;
125              
126 0           my $ua = Mojo::UserAgent->new();
127 0           my ($id, $ping_loop_id);
128              
129             $ua->on('start' => sub {
130 0     0     my ($ua, $tx) = @_;
131              
132 0 0         carp 'Started' if $self->debug;
133              
134 0           $tx->req->headers->header($_->[0] => $_->[1]) foreach pairs @{$self->headers};
  0            
135 0           });
136              
137             $id = $ua->websocket($self->ws_url => sub {
138 0     0     my ($ua, $tx) = @_;
139              
140 0           my $last = 0;
141              
142 0 0         croak 'Websocket handshake failed' unless $tx->is_websocket;
143              
144             $ping_loop_id = Mojo::IOLoop->recurring($self->ping_interval => sub {
145 0           my $loop = shift;
146              
147 0 0         carp 'Ping '.time() if $self->debug;
148              
149 0           $tx->send(encode_json({ seq => ++$last, action => 'ping' }));
150 0           });
151              
152             $tx->on(finish => sub {
153 0           my ($tx, $code, $reason) = @_;
154 0   0       carp sprintf('Finished (%d: %s)', $code, $reason // 'Unknown');
155 0           return Mojo::IOLoop->remove($ping_loop_id);
156 0           });
157              
158             $tx->on(message => sub {
159 0           my ($tx, $message) = @_;
160              
161 0           my $content = decode_json($message);
162 0           my $ret = $self->handle_message($content);
163              
164 0           $last = $content->{seq};
165              
166 0 0 0       $tx->send(encode_json($ret->{ws_send})) if $ret && ref $ret eq 'HASH' && $ret->{ws_send};
      0        
167 0           });
168 0           });
169              
170 0 0         Mojo::IOLoop->start() unless Mojo::IOLoop->is_running();
171             }
172              
173             sub _format_mm_url {
174 0     0     my $self = shift;
175 0           my $end = shift;
176              
177 0           return sprintf('%s/%s', $self->base_url, $end);
178             }
179              
180             sub _post_to_channel {
181 0     0     my $self = shift;
182 0           my $args = shift;
183              
184 0 0         unless ($args->{channel_id}) {
185 0           carp 'No channel_id provided - could not send to channel';
186 0           return;
187             }
188              
189 0           return $self->furl->post($self->endpoints->{channel_msg}, $self->headers, encode_json($args));
190             }
191              
192             ################################################################################
193              
194             sub _build_endpoints {
195 0     0     my $self = shift;
196              
197 0           my $base = $self->api_url;
198              
199             return {
200 0           channel_msg => sprintf('%s/posts', $base),
201             };
202             }
203              
204             sub _build_furl {
205 0     0     my $self = shift;
206              
207 0           return Furl->new(ssl_opts => $self->ssl_opts);
208             }
209              
210             sub _build_headers {
211             # Initial headers, added to at connection
212             return [
213 0     0     'Content-Type' => 'application/json',
214             'X-Requested-With' => 'XMLHttpRequest',
215             ];
216             }
217              
218             sub _build_api_url {
219 0     0     my $self = shift;
220              
221 0           return $self->_format_mm_url('api/v4');
222             }
223              
224             sub _build_ws_url {
225 0     0     my $self = shift;
226              
227 0           my $url = $self->_format_mm_url('api/v4/websocket');
228              
229 0           $url =~ s/^http(?:s)?/wss/s;
230              
231 0           return $url;
232             }
233              
234             ################################################################################
235              
236             1;
237             __END__