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__ |