line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Bot::Backbone::Service::SlackChat; |
2
|
|
|
|
|
|
|
$Bot::Backbone::Service::SlackChat::VERSION = '0.161950'; |
3
|
1
|
|
|
1
|
|
498
|
use v5.14; |
|
1
|
|
|
|
|
3
|
|
4
|
1
|
|
|
1
|
|
394
|
use Bot::Backbone::Service; |
|
1
|
|
|
|
|
849492
|
|
|
1
|
|
|
|
|
4
|
|
5
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
with qw( |
7
|
|
|
|
|
|
|
Bot::Backbone::Service::Role::Service |
8
|
|
|
|
|
|
|
Bot::Backbone::Service::Role::Dispatch |
9
|
|
|
|
|
|
|
Bot::Backbone::Service::Role::BareMetalChat |
10
|
|
|
|
|
|
|
Bot::Backbone::Service::Role::GroupJoiner |
11
|
|
|
|
|
|
|
); |
12
|
|
|
|
|
|
|
|
13
|
1
|
|
|
1
|
|
6523
|
use Bot::Backbone::Message; |
|
1
|
|
|
|
|
239616
|
|
|
1
|
|
|
|
|
37
|
|
14
|
1
|
|
|
1
|
|
7
|
use Carp; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
59
|
|
15
|
1
|
|
|
1
|
|
661
|
use CHI; |
|
1
|
|
|
|
|
50362
|
|
|
1
|
|
|
|
|
28
|
|
16
|
1
|
|
|
1
|
|
488
|
use Encode; |
|
1
|
|
|
|
|
6696
|
|
|
1
|
|
|
|
|
64
|
|
17
|
1
|
|
|
1
|
|
407
|
use AnyEvent::SlackRTM; |
|
1
|
|
|
|
|
162437
|
|
|
1
|
|
|
|
|
28
|
|
18
|
1
|
|
|
1
|
|
456
|
use WebService::Slack::WebApi; |
|
1
|
|
|
|
|
20256
|
|
|
1
|
|
|
|
|
1600
|
|
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
has token => ( |
24
|
|
|
|
|
|
|
is => 'ro', |
25
|
|
|
|
|
|
|
isa => 'Str', |
26
|
|
|
|
|
|
|
required => 1, |
27
|
|
|
|
|
|
|
); |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
has on_channel_joined => ( |
31
|
|
|
|
|
|
|
is => 'ro', |
32
|
|
|
|
|
|
|
isa => 'CodeRef', |
33
|
|
|
|
|
|
|
predicate => 'has_channel_joined_callback', |
34
|
|
|
|
|
|
|
); |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
has _seen_channels => ( |
37
|
|
|
|
|
|
|
is => 'ro', |
38
|
|
|
|
|
|
|
isa => 'HashRef', |
39
|
|
|
|
|
|
|
required => 1, |
40
|
|
|
|
|
|
|
default => sub { {} }, |
41
|
|
|
|
|
|
|
); |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
has cache => ( |
46
|
|
|
|
|
|
|
is => 'ro', |
47
|
|
|
|
|
|
|
required => 1, |
48
|
|
|
|
|
|
|
lazy => 1, |
49
|
|
|
|
|
|
|
builder => '_build_cache', |
50
|
|
|
|
|
|
|
); |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
sub _build_cache { |
53
|
0
|
|
|
0
|
|
|
CHI->new( |
54
|
|
|
|
|
|
|
driver => 'Memory', |
55
|
|
|
|
|
|
|
datastore => {}, |
56
|
|
|
|
|
|
|
expires_in => 60, |
57
|
|
|
|
|
|
|
); |
58
|
|
|
|
|
|
|
} |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
has last_mark => ( |
63
|
|
|
|
|
|
|
is => 'rw', |
64
|
|
|
|
|
|
|
isa => 'Int', |
65
|
|
|
|
|
|
|
required => 1, |
66
|
|
|
|
|
|
|
default => 0, |
67
|
|
|
|
|
|
|
); |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
has api => ( |
71
|
|
|
|
|
|
|
is => 'ro', |
72
|
|
|
|
|
|
|
isa => 'WebService::Slack::WebApi', |
73
|
|
|
|
|
|
|
lazy => 1, |
74
|
|
|
|
|
|
|
builder => '_build_api', |
75
|
|
|
|
|
|
|
); |
76
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
sub _build_api { |
78
|
0
|
|
|
0
|
|
|
my $self = shift; |
79
|
0
|
|
|
|
|
|
WebService::Slack::WebApi->new(token => $self->token); |
80
|
|
|
|
|
|
|
} |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
has rtm => ( |
84
|
|
|
|
|
|
|
is => 'ro', |
85
|
|
|
|
|
|
|
isa => 'AnyEvent::SlackRTM', |
86
|
|
|
|
|
|
|
lazy => 1, |
87
|
|
|
|
|
|
|
builder => '_build_rtm', |
88
|
|
|
|
|
|
|
); |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
sub _build_rtm { |
91
|
0
|
|
|
0
|
|
|
my $self = shift; |
92
|
0
|
|
|
|
|
|
AnyEvent::SlackRTM->new($self->token); |
93
|
|
|
|
|
|
|
} |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
has error_callback => ( |
97
|
|
|
|
|
|
|
is => 'ro', |
98
|
|
|
|
|
|
|
isa => 'CodeRef', |
99
|
|
|
|
|
|
|
lazy => 1, |
100
|
|
|
|
|
|
|
builder => '_build_error_callback', |
101
|
|
|
|
|
|
|
); |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
sub _build_error_callback { |
104
|
|
|
|
|
|
|
return sub { |
105
|
0
|
|
|
0
|
|
|
my ($self, $rtm, $message) = @_; |
106
|
0
|
|
|
|
|
|
carp "Slack Error #$message->{error}{code}: $message->{error}{msg}\n"; |
107
|
|
|
|
|
|
|
} |
108
|
0
|
|
|
0
|
|
|
} |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
has whoami => ( |
112
|
|
|
|
|
|
|
is => 'rw', |
113
|
|
|
|
|
|
|
isa => 'HashRef', |
114
|
|
|
|
|
|
|
required => 1, |
115
|
|
|
|
|
|
|
lazy => 1, |
116
|
|
|
|
|
|
|
builder => '_build_whoami', |
117
|
|
|
|
|
|
|
); |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
sub _build_whoami { |
120
|
0
|
|
|
0
|
|
|
my $self = shift; |
121
|
0
|
|
|
|
|
|
my $res = $self->api->auth->test; |
122
|
|
|
|
|
|
|
|
123
|
0
|
0
|
|
|
|
|
if ($res->{ok}) { |
124
|
0
|
|
|
|
|
|
$res; |
125
|
|
|
|
|
|
|
} |
126
|
|
|
|
|
|
|
else { |
127
|
0
|
|
|
|
|
|
croak "unable to ask Slack who am I?"; |
128
|
|
|
|
|
|
|
} |
129
|
|
|
|
|
|
|
} |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
|
132
|
0
|
|
|
0
|
1
|
|
sub user { shift->whoami->{user} } |
133
|
0
|
|
|
0
|
1
|
|
sub user_id { shift->whoami->{user_id} } |
134
|
0
|
|
|
0
|
1
|
|
sub team_id { shift->whoami->{team_id} } |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
sub _when_channel_joined { |
138
|
0
|
|
|
0
|
|
|
my ($self, $channel) = @_; |
139
|
|
|
|
|
|
|
|
140
|
0
|
0
|
|
|
|
|
return unless $self->has_channel_joined_callback; |
141
|
|
|
|
|
|
|
|
142
|
0
|
|
|
|
|
|
my $id = $channel->{id}; |
143
|
|
|
|
|
|
|
|
144
|
0
|
0
|
|
|
|
|
next if $self->_seen_channels->{ $id }; |
145
|
|
|
|
|
|
|
|
146
|
0
|
|
|
|
|
|
$self->on_channel_joined->($self, $channel->{id}, $channel->{name}, ''); |
147
|
0
|
|
|
|
|
|
$self->_seen_channels->{ $id }++; |
148
|
|
|
|
|
|
|
|
149
|
0
|
|
|
|
|
|
$self->bot->construct_services; |
150
|
0
|
|
|
|
|
|
$self->bot->initialize_services; |
151
|
|
|
|
|
|
|
} |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
sub _check_channels_joined { |
154
|
0
|
|
|
0
|
|
|
my ($self) = @_; |
155
|
|
|
|
|
|
|
|
156
|
0
|
0
|
|
|
|
|
return unless $self->has_channel_joined_callback; |
157
|
|
|
|
|
|
|
|
158
|
0
|
|
|
|
|
|
my @mine; |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
my $channels = $self->_cached('api.channels.list', sub { |
161
|
0
|
|
|
0
|
|
|
$self->api->channels->list |
162
|
0
|
|
|
|
|
|
}); |
163
|
|
|
|
|
|
|
|
164
|
0
|
0
|
|
|
|
|
if ($channels->{ok}) { |
165
|
0
|
0
|
|
|
|
|
push @mine, grep { $_->{is_member} && !$_->{is_archived} } @{ $channels->{channels} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
} |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
my $groups = $self->_cached('api.groups.list', sub { |
169
|
0
|
|
|
0
|
|
|
$self->api->groups->list |
170
|
0
|
|
|
|
|
|
}); |
171
|
|
|
|
|
|
|
|
172
|
0
|
0
|
|
|
|
|
if ($groups->{ok}) { |
173
|
0
|
|
|
|
|
|
push @mine, grep { !$_->{is_archived} } @{ $groups->{groups} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
} |
175
|
|
|
|
|
|
|
|
176
|
0
|
|
|
|
|
|
for my $channel (@mine) { |
177
|
0
|
|
|
|
|
|
my $id = $channel->{id}; |
178
|
|
|
|
|
|
|
|
179
|
0
|
0
|
|
|
|
|
next if $self->_seen_channels->{ $id }; |
180
|
|
|
|
|
|
|
|
181
|
0
|
|
|
|
|
|
$self->on_channel_joined->($self, $channel->{id}, $channel->{name}, 1); |
182
|
0
|
|
|
|
|
|
$self->_seen_channels->{ $id }++; |
183
|
|
|
|
|
|
|
} |
184
|
|
|
|
|
|
|
|
185
|
0
|
|
|
|
|
|
$self->bot->construct_services; |
186
|
0
|
|
|
|
|
|
$self->bot->initialize_services; |
187
|
|
|
|
|
|
|
} |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
sub initialize { |
190
|
|
|
|
|
|
|
my $self = shift; |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
$self->_check_channels_joined; |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
$self->rtm->on( |
195
|
|
|
|
|
|
|
message => sub { $self->got_message(@_) }, |
196
|
|
|
|
|
|
|
error => sub { $self->error_callback->($self, @_) }, |
197
|
|
|
|
|
|
|
channel_joined => sub { $self->_when_channel_joined($_[1]{channel}) }, |
198
|
|
|
|
|
|
|
group_joined => sub { $self->_when_channel_joined($_[1]{channel}) }, |
199
|
|
|
|
|
|
|
); |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
$self->rtm->quiet(1); |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
$self->rtm->start; |
204
|
|
|
|
|
|
|
} |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
sub _cached { |
208
|
0
|
|
|
0
|
|
|
my ($self, $key, $code) = @_; |
209
|
|
|
|
|
|
|
|
210
|
0
|
|
|
|
|
|
my $cached = $self->cache->get($key); |
211
|
0
|
0
|
|
|
|
|
return $cached if $cached; |
212
|
|
|
|
|
|
|
|
213
|
0
|
|
|
|
|
|
my $value = $code->(); |
214
|
0
|
|
|
|
|
|
$self->cache->set($key, $value); |
215
|
0
|
|
|
|
|
|
return $value; |
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
sub load_user { |
219
|
0
|
|
|
0
|
1
|
|
my ($self, $by, $value) = @_; |
220
|
|
|
|
|
|
|
|
221
|
0
|
|
|
|
|
|
my $user; |
222
|
0
|
0
|
|
|
|
|
if ($by eq 'id') { |
|
|
0
|
|
|
|
|
|
223
|
|
|
|
|
|
|
my $res = $self->_cached("api.users.info:user=$value", sub { |
224
|
0
|
|
|
0
|
|
|
$self->api->users->info(user => $value); |
225
|
0
|
|
|
|
|
|
}); |
226
|
0
|
0
|
|
|
|
|
$user = $res->{user} if $res->{ok}; |
227
|
|
|
|
|
|
|
} |
228
|
|
|
|
|
|
|
elsif ($by eq 'name') { |
229
|
0
|
|
|
0
|
|
|
my $list = $self->_cached("api.users.list", sub { $self->api->users->list }); |
|
0
|
|
|
|
|
|
|
230
|
0
|
0
|
|
|
|
|
if ($list->{ok}) { |
231
|
0
|
|
|
|
|
|
($user) = grep { $_->{name} eq $value } @{ $list->{members} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
} |
234
|
|
|
|
|
|
|
else { |
235
|
0
|
|
|
|
|
|
croak "unknown lookup type $by"; |
236
|
|
|
|
|
|
|
} |
237
|
|
|
|
|
|
|
|
238
|
0
|
0
|
|
|
|
|
if (defined $user) { |
239
|
|
|
|
|
|
|
return Bot::Backbone::Identity->new( |
240
|
|
|
|
|
|
|
username => $user->{id}, |
241
|
|
|
|
|
|
|
nickname => $user->{name}, |
242
|
0
|
|
|
|
|
|
me => ($user->{id} eq $self->user_id), |
243
|
|
|
|
|
|
|
); |
244
|
|
|
|
|
|
|
} |
245
|
|
|
|
|
|
|
else { |
246
|
0
|
|
|
|
|
|
croak "unknown user $by $value"; |
247
|
|
|
|
|
|
|
} |
248
|
|
|
|
|
|
|
} |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
sub load_me { |
252
|
0
|
|
|
0
|
1
|
|
my $self = shift; |
253
|
0
|
|
|
|
|
|
return $self->load_user(id => $self->user_id); |
254
|
|
|
|
|
|
|
} |
255
|
|
|
|
|
|
|
|
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
sub load_user_channel { |
258
|
0
|
|
|
0
|
1
|
|
my ($self, $by, $value) = @_; |
259
|
|
|
|
|
|
|
|
260
|
0
|
0
|
0
|
|
|
|
croak "unknown lookup type $by" unless $by eq 'user' or $by eq 'id'; |
261
|
|
|
|
|
|
|
|
262
|
0
|
|
|
0
|
|
|
my $list = $self->_cached("api.im.list", sub { $self->api->im->list }); |
|
0
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
|
264
|
0
|
0
|
|
|
|
|
croak "unknown IM $by $value" unless $list->{ok}; |
265
|
|
|
|
|
|
|
|
266
|
0
|
|
|
|
|
|
my ($im) = grep { $_->{ $by } eq $value } @{ $list->{members} }; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
267
|
0
|
|
|
|
|
|
return $im->{id}; |
268
|
|
|
|
|
|
|
} |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
sub load_channel { |
273
|
0
|
|
|
0
|
0
|
|
my ($self, $by, $value) = @_; |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
|
276
|
0
|
0
|
|
|
|
|
croak "unknown lookup type $by" unless $by eq 'id'; |
277
|
|
|
|
|
|
|
|
278
|
0
|
|
|
|
|
|
my $group; |
279
|
0
|
|
|
|
|
|
my $type = substr $value, 0, 1; |
280
|
0
|
0
|
|
|
|
|
if ($type eq 'G') { |
|
|
0
|
|
|
|
|
|
281
|
|
|
|
|
|
|
my $res = $self->_cached("api.groups.info:group=$value", sub { |
282
|
0
|
|
|
0
|
|
|
$self->api->groups->info( channel => $value ) |
283
|
0
|
|
|
|
|
|
}); |
284
|
0
|
0
|
|
|
|
|
$group = $res->{group} if $res->{ok}; |
285
|
|
|
|
|
|
|
} |
286
|
|
|
|
|
|
|
elsif ($type eq 'C') { |
287
|
|
|
|
|
|
|
my $res = $self->_cached("api.channels.info:channel=$value", sub { |
288
|
0
|
|
|
0
|
|
|
$self->api->channels->info( channel => $value ) |
289
|
0
|
|
|
|
|
|
}); |
290
|
0
|
0
|
|
|
|
|
$group = $res->{channel} if $res->{ok}; |
291
|
|
|
|
|
|
|
} |
292
|
|
|
|
|
|
|
else { |
293
|
0
|
|
|
|
|
|
croak "unknown group type $type"; |
294
|
|
|
|
|
|
|
} |
295
|
|
|
|
|
|
|
|
296
|
0
|
0
|
|
|
|
|
if (defined $group) { |
297
|
0
|
|
|
|
|
|
return $group->{id}; |
298
|
|
|
|
|
|
|
} |
299
|
|
|
|
|
|
|
else { |
300
|
0
|
|
|
|
|
|
croak "cannot find group $by $value"; |
301
|
|
|
|
|
|
|
} |
302
|
|
|
|
|
|
|
} |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
sub join_group { |
306
|
0
|
|
|
0
|
1
|
|
my ($self, $options) = @_; |
307
|
|
|
|
|
|
|
|
308
|
0
|
|
|
|
|
|
my $type = substr $options->{group}, 0, 1; |
309
|
|
|
|
|
|
|
|
310
|
0
|
0
|
|
|
|
|
if ($type eq 'G') { |
|
|
0
|
|
|
|
|
|
311
|
0
|
|
|
|
|
|
$self->api->groups->open(channel => $options->{group}); |
312
|
|
|
|
|
|
|
} |
313
|
|
|
|
|
|
|
elsif ($type eq 'C') { |
314
|
0
|
|
|
|
|
|
$self->api->channels->join(name => $options->{group}); |
315
|
|
|
|
|
|
|
} |
316
|
|
|
|
|
|
|
else { |
317
|
0
|
|
|
|
|
|
croak "unknown group type $type"; |
318
|
|
|
|
|
|
|
} |
319
|
|
|
|
|
|
|
} |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
sub got_message { |
323
|
0
|
|
|
0
|
1
|
|
my ($self, $rtm, $slack_msg) = @_; |
324
|
|
|
|
|
|
|
|
325
|
|
|
|
|
|
|
|
326
|
0
|
|
|
|
|
|
$self->mark_read($slack_msg); |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
|
329
|
0
|
0
|
|
|
|
|
return if defined $slack_msg->{subtype}; |
330
|
|
|
|
|
|
|
|
331
|
|
|
|
|
|
|
|
332
|
0
|
0
|
|
|
|
|
return if defined $slack_msg->{edited}; |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
|
335
|
0
|
|
|
|
|
|
my $channel_type = substr $slack_msg->{channel}, 0, 1; |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
|
344
|
0
|
0
|
|
|
|
|
if ($channel_type eq 'D') { |
345
|
0
|
|
|
|
|
|
$self->got_direct_message($slack_msg); |
346
|
|
|
|
|
|
|
} |
347
|
|
|
|
|
|
|
else { |
348
|
0
|
|
|
|
|
|
$self->got_group_message($slack_msg); |
349
|
|
|
|
|
|
|
} |
350
|
|
|
|
|
|
|
} |
351
|
|
|
|
|
|
|
|
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
sub got_direct_message { |
354
|
0
|
|
|
0
|
1
|
|
my ($self, $slack_msg) = @_; |
355
|
|
|
|
|
|
|
|
356
|
|
|
|
|
|
|
|
357
|
0
|
0
|
|
|
|
|
return if $slack_msg->{user} eq $self->whoami->{user_id}; |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
my $message = Bot::Backbone::Message->new({ |
360
|
|
|
|
|
|
|
chat => $self, |
361
|
|
|
|
|
|
|
from => $self->load_user(id => $slack_msg->{user}), |
362
|
|
|
|
|
|
|
to => $self->load_user(id => $self->user_id), |
363
|
|
|
|
|
|
|
group => undef, |
364
|
|
|
|
|
|
|
text => $slack_msg->{text}, |
365
|
0
|
|
|
|
|
|
}); |
366
|
|
|
|
|
|
|
|
367
|
0
|
|
|
|
|
|
$self->resend_message($message); |
368
|
0
|
|
|
|
|
|
$self->dispatch_message($message); |
369
|
|
|
|
|
|
|
} |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
|
372
|
|
|
|
|
|
|
sub is_to_me { |
373
|
0
|
|
|
0
|
1
|
|
my ($self, $me_user, $text) = @_; |
374
|
|
|
|
|
|
|
|
375
|
0
|
|
|
|
|
|
my $me_nick = $me_user->nickname; |
376
|
0
|
|
|
|
|
|
return scalar($$text =~ s/^ @?$me_nick \s* [:,\-] \s* |
377
|
|
|
|
|
|
|
| \s* , \s* @?$me_nick [.!?]? $ |
378
|
|
|
|
|
|
|
| , \s* @?$me_nick \s* , |
379
|
|
|
|
|
|
|
//x); |
380
|
|
|
|
|
|
|
} |
381
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
sub mark_read { |
384
|
0
|
|
|
0
|
0
|
|
my ($self, $slack_msg) = @_; |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
|
387
|
0
|
0
|
|
|
|
|
return unless time - $self->last_mark > 15; |
388
|
|
|
|
|
|
|
|
389
|
0
|
|
|
|
|
|
my $channel = $slack_msg->{channel}; |
390
|
0
|
|
|
|
|
|
my $ts = $slack_msg->{ts}; |
391
|
|
|
|
|
|
|
|
392
|
0
|
|
|
|
|
|
my $type = substr $channel, 0, 1; |
393
|
0
|
0
|
|
|
|
|
if ($type eq 'C') { |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
394
|
0
|
|
|
|
|
|
$self->api->channels->mark( channel => $channel, ts => $ts ); |
395
|
|
|
|
|
|
|
} |
396
|
|
|
|
|
|
|
elsif ($type eq 'G') { |
397
|
0
|
|
|
|
|
|
$self->api->groups->mark( channel => $channel, ts => $ts ); |
398
|
|
|
|
|
|
|
} |
399
|
|
|
|
|
|
|
elsif ($type eq 'D') { |
400
|
0
|
|
|
|
|
|
$self->api->im->mark( channel => $channel, ts => $ts ); |
401
|
|
|
|
|
|
|
} |
402
|
|
|
|
|
|
|
|
403
|
0
|
|
|
|
|
|
$self->last_mark(time); |
404
|
|
|
|
|
|
|
} |
405
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
sub got_group_message { |
408
|
0
|
|
|
0
|
1
|
|
my ($self, $slack_msg) = @_; |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
|
411
|
0
|
0
|
|
|
|
|
return if $slack_msg->{user} eq $self->whoami->{user_id}; |
412
|
|
|
|
|
|
|
|
413
|
0
|
|
|
|
|
|
my $me_user = $self->load_me; |
414
|
|
|
|
|
|
|
|
415
|
0
|
|
|
|
|
|
my $text = $slack_msg->{text}; |
416
|
0
|
|
|
|
|
|
my $to_identity; |
417
|
0
|
0
|
|
|
|
|
if ($self->is_to_me($me_user, \$text)) { |
418
|
0
|
|
|
|
|
|
$to_identity = $me_user; |
419
|
|
|
|
|
|
|
} |
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
my $message = Bot::Backbone::Message->new({ |
422
|
|
|
|
|
|
|
chat => $self, |
423
|
|
|
|
|
|
|
from => $self->load_user(id => $slack_msg->{user}), |
424
|
|
|
|
|
|
|
to => $to_identity, |
425
|
0
|
|
|
|
|
|
group => $self->load_channel( id => $slack_msg->{channel} ), |
426
|
|
|
|
|
|
|
text => $text, |
427
|
|
|
|
|
|
|
}); |
428
|
|
|
|
|
|
|
|
429
|
0
|
|
|
|
|
|
$self->resend_message($message); |
430
|
0
|
|
|
|
|
|
$self->dispatch_message($message); |
431
|
|
|
|
|
|
|
} |
432
|
|
|
|
|
|
|
|
433
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
sub send_message { |
435
|
|
|
|
|
|
|
my ($self, $params) = @_; |
436
|
|
|
|
|
|
|
|
437
|
|
|
|
|
|
|
my $to = $params->{to}; |
438
|
|
|
|
|
|
|
my $group = $params->{group}; |
439
|
|
|
|
|
|
|
my $text = $params->{text}; |
440
|
|
|
|
|
|
|
my $attachments = $params->{attachments}; |
441
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
my $channel; |
443
|
|
|
|
|
|
|
if (defined $group) { |
444
|
|
|
|
|
|
|
$channel = $self->load_channel( id => $group ); |
445
|
|
|
|
|
|
|
} |
446
|
|
|
|
|
|
|
else { |
447
|
|
|
|
|
|
|
$channel = $self->load_user_channel( user => $to ); |
448
|
|
|
|
|
|
|
} |
449
|
|
|
|
|
|
|
|
450
|
|
|
|
|
|
|
my %message_opts = ( |
451
|
|
|
|
|
|
|
channel => $channel, |
452
|
|
|
|
|
|
|
as_user => 1, |
453
|
|
|
|
|
|
|
); |
454
|
|
|
|
|
|
|
if (defined $text) { |
455
|
|
|
|
|
|
|
$message_opts{text} = encode('utf8', $text); |
456
|
|
|
|
|
|
|
} |
457
|
|
|
|
|
|
|
if (defined $attachments) { |
458
|
|
|
|
|
|
|
$message_opts{attachments} = $attachments; |
459
|
|
|
|
|
|
|
} |
460
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
$self->api->chat->post_message(%message_opts); |
462
|
|
|
|
|
|
|
} |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
|
465
|
|
|
|
|
|
|
__PACKAGE__->meta->make_immutable; |
466
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
__END__ |
468
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
=pod |
470
|
|
|
|
|
|
|
|
471
|
|
|
|
|
|
|
=encoding UTF-8 |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
=head1 NAME |
474
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
Bot::Backbone::Service::SlackChat - Connect and chat with a Slack server |
476
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
=head1 VERSION |
478
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
version 0.161950 |
480
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
=head1 SYNOPSIS |
482
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
package MyBot; |
484
|
|
|
|
|
|
|
use Bot::Backbone; |
485
|
|
|
|
|
|
|
|
486
|
|
|
|
|
|
|
service slack_chat => ( |
487
|
|
|
|
|
|
|
service => 'SlackChat', |
488
|
|
|
|
|
|
|
token => '...', # see slack.com for your tokens |
489
|
|
|
|
|
|
|
); |
490
|
|
|
|
|
|
|
|
491
|
|
|
|
|
|
|
service dice => ( |
492
|
|
|
|
|
|
|
service => 'OFun::Dice', |
493
|
|
|
|
|
|
|
); |
494
|
|
|
|
|
|
|
|
495
|
|
|
|
|
|
|
service "general_chat" => ( |
496
|
|
|
|
|
|
|
service => 'GroupChat', |
497
|
|
|
|
|
|
|
chat => 'SlackChat', |
498
|
|
|
|
|
|
|
group => 'C', |
499
|
|
|
|
|
|
|
dispatcher => 'general_dispatch', |
500
|
|
|
|
|
|
|
); |
501
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
dispatcher 'general_dispatch' => as { |
503
|
|
|
|
|
|
|
redispatch_to "dice"; |
504
|
|
|
|
|
|
|
}; |
505
|
|
|
|
|
|
|
|
506
|
|
|
|
|
|
|
__PACKAGE__->new->run; |
507
|
|
|
|
|
|
|
|
508
|
|
|
|
|
|
|
=head1 DESCRIPTION |
509
|
|
|
|
|
|
|
|
510
|
|
|
|
|
|
|
This allows a L<Bot::Backbone> chat bot to be connect to a Slack server using their Real-Time Messaging API. |
511
|
|
|
|
|
|
|
|
512
|
|
|
|
|
|
|
This is based on L<AnyEvent::SlackRTM> and L<WebService::Slack::WebApi>. It also uses a L<CHI> cache to help avoid contacting the Slack server too often, which could result in your bot becoming rate limited. |
513
|
|
|
|
|
|
|
|
514
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
515
|
|
|
|
|
|
|
|
516
|
|
|
|
|
|
|
=head2 token |
517
|
|
|
|
|
|
|
|
518
|
|
|
|
|
|
|
The C<token> is the access token from Slack to use. This may be either of the following type of tokens: |
519
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
=over |
521
|
|
|
|
|
|
|
|
522
|
|
|
|
|
|
|
=item * |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
L<User Token|https://api.slack.com/tokens>. This is a token to perform actions on behalf of a user account. |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
=item * |
527
|
|
|
|
|
|
|
|
528
|
|
|
|
|
|
|
L<Bot Token|https://slack.com/services/new/bot>. If you configure a bot integration, you may use the access token on the bot configuration page to use this library to act on behalf of the bot account. Bot accounts may not have the same features as a user account, so please be sure to read the Slack documentation to understand any differences or limitations. |
529
|
|
|
|
|
|
|
|
530
|
|
|
|
|
|
|
=back |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
Which you use will depend on whether you want the bot to control a user account or a bot integration account. You are responsible for adhering to the Slack terms of use in whatever you do. |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
=head2 on_channel_joined |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
This may be set to a subroutine to call whenever the bot is invited to join a channel. This allows the bot to be configured to handle channels as it is invited to them. |
537
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
For example: |
539
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
service slack_chat => ( |
541
|
|
|
|
|
|
|
service => 'SlackChat', |
542
|
|
|
|
|
|
|
token => '...', # see slack.com for your tokens |
543
|
|
|
|
|
|
|
on_channel_joined => sub { |
544
|
|
|
|
|
|
|
my ($slack, $channel, $name, $during_init) = @_; |
545
|
|
|
|
|
|
|
service "group_$name" => ( |
546
|
|
|
|
|
|
|
service => 'GroupChat', |
547
|
|
|
|
|
|
|
chat => 'slack_chat', |
548
|
|
|
|
|
|
|
group => $channel, |
549
|
|
|
|
|
|
|
dispatcher => 'general', |
550
|
|
|
|
|
|
|
); |
551
|
|
|
|
|
|
|
}, |
552
|
|
|
|
|
|
|
); |
553
|
|
|
|
|
|
|
|
554
|
|
|
|
|
|
|
The called subroutine will be passed this object (from which you can make API calls via C<< $slack->api >>), the Slack ID of the newly joined channel, the human name of the newly joined channel, and the "during init" flag. The boolean flag sent as the third argument is set to true if the callback is being called while the SlackChat service is being initialized. If the flag is false, this indicates that it is happening in reaction to the bot receiving a "channel_joined" message while running. |
555
|
|
|
|
|
|
|
|
556
|
|
|
|
|
|
|
=head2 cache |
557
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
This is a L<CHI> cache to use to temporarily store response from the Slack APIs. By default, this is a memory-only cache that caches data for only 60 seconds. The purpose is mainly to prevent repeated requests to the API, which might result in rate limiting. |
559
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
=head2 api |
561
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
This is the L<WebService::Slack::WebApi> object used to contact Slack for information about channels, users, etc. |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
=head2 rtm |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
This is the L<AnyEvent::SlackRTM> object used to communicate with Slack and trigger events from the Real-Time Messaging API. |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
=head2 error_callback |
569
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
This is a callback sub that may be used to report error events from the RTM API. Set it to a sub that will be called as follows: |
571
|
|
|
|
|
|
|
|
572
|
|
|
|
|
|
|
sub { |
573
|
|
|
|
|
|
|
my ($self, $rtm, $message) = @_; |
574
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
... |
576
|
|
|
|
|
|
|
} |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
Here, C<$self> is the L<Bot::Backbone::Service::SlackChat> object, C<$rtm> is the L<AnyEvent::SlackRTM> object, and C<$message> is a hash containing the error message, as described on the L<Real Time Messaging API|https://api.slack.com/rtm> documentation. |
579
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
=head2 whoami |
581
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
This returns a hash containing information about who the bot is. |
583
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
=head1 METHODS |
585
|
|
|
|
|
|
|
|
586
|
|
|
|
|
|
|
=head2 user |
587
|
|
|
|
|
|
|
|
588
|
|
|
|
|
|
|
Returns the name of the bot. |
589
|
|
|
|
|
|
|
|
590
|
|
|
|
|
|
|
=head2 user_id |
591
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
Returns the user ID for the bot. |
593
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
=head2 team_id |
595
|
|
|
|
|
|
|
|
596
|
|
|
|
|
|
|
Returns the team ID for the team account. |
597
|
|
|
|
|
|
|
|
598
|
|
|
|
|
|
|
=head2 initialize |
599
|
|
|
|
|
|
|
|
600
|
|
|
|
|
|
|
This connects to Slack and prepares the bot for communication. |
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
=head2 load_user |
603
|
|
|
|
|
|
|
|
604
|
|
|
|
|
|
|
method load_user($by, $value) returns Bot::Backbone::Identity |
605
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
Fetches information about a user from Slack and returns the user as a L<Bot::Backbone::Identity>. The C<$by> setting determines how the user is looked up, which may either be by "id" or by "name". The value, then, is the value to check. |
607
|
|
|
|
|
|
|
|
608
|
|
|
|
|
|
|
=head2 load_me |
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
method load_me() returns Bot::Backbone::Identity |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
Returns the identity object for the bot itself. |
613
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
=head2 load_user_channel |
615
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
method load_user_channel($by, $value) returns Str |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
Returns the ID of a user's IM channel. Here C<$by> may be "user" to lookup by user ID. |
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
=head2 join_group |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
method join_group({ group => $group }) |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
Given the ID of a channel or group, this causes the bot to open or join it. Note that Slack bot integration accounts might not be able to join team channels, but may still be invited. |
625
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
B<CAVEAT:> Slack does not permit bots to join groups, so this method call will be a no-op for bot users. This will only work if this code is operating a regular user account. |
627
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
=head2 got_message |
629
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
Handles messages from Slack. Decides whether they are group messages or direct and forwards them on as appropriate. Messages with a "subtype" will be ignored as will messages that are "edited". |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
This method also marks messages as read. |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
=head2 got_direct_message |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
Handles direct messages received from an IM channel. |
637
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
=head2 is_to_me |
639
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
This determines whether or not the message is to the bot. |
641
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
=head2 got_group_message |
643
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
This handles message received from private group or team channels. |
645
|
|
|
|
|
|
|
|
646
|
|
|
|
|
|
|
=head2 send_message |
647
|
|
|
|
|
|
|
|
648
|
|
|
|
|
|
|
method send_message({ |
649
|
|
|
|
|
|
|
to => $user_id, |
650
|
|
|
|
|
|
|
group => $group_id, |
651
|
|
|
|
|
|
|
text => $message, |
652
|
|
|
|
|
|
|
attachments => $attachments, |
653
|
|
|
|
|
|
|
}) |
654
|
|
|
|
|
|
|
|
655
|
|
|
|
|
|
|
This sends a message to a Slack channel. To the named user's IM channel or to a private group or team channel named by C<$group_id>. Attachments can be included to produce formatted messages. |
656
|
|
|
|
|
|
|
|
657
|
|
|
|
|
|
|
L<Slack Message Attachment API|https://api.slack.com/docs/attachments> |
658
|
|
|
|
|
|
|
|
659
|
|
|
|
|
|
|
=for Pod::Coverage load_channel |
660
|
|
|
|
|
|
|
mark_read |
661
|
|
|
|
|
|
|
|
662
|
|
|
|
|
|
|
=head1 AUTHOR |
663
|
|
|
|
|
|
|
|
664
|
|
|
|
|
|
|
Andrew Sterling Hanenkamp <hanenkamp@cpan.org> |
665
|
|
|
|
|
|
|
|
666
|
|
|
|
|
|
|
=head1 COPYRIGHT AND LICENSE |
667
|
|
|
|
|
|
|
|
668
|
|
|
|
|
|
|
This software is copyright (c) 2016 by Qubling Software LLC. |
669
|
|
|
|
|
|
|
|
670
|
|
|
|
|
|
|
This is free software; you can redistribute it and/or modify it under |
671
|
|
|
|
|
|
|
the same terms as the Perl 5 programming language system itself. |
672
|
|
|
|
|
|
|
|
673
|
|
|
|
|
|
|
=cut |
674
|
|
|
|
|
|
|
|