File Coverage

blib/lib/Net/Async/Slack/RTM.pm
Criterion Covered Total %
statement 339 382 88.7
branch 0 10 0.0
condition 0 11 0.0
subroutine 113 127 88.9
pod 4 12 33.3
total 456 542 84.1


line stmt bran cond sub pod time code
1             package Net::Async::Slack::RTM;
2              
3 2     2   423103 use strict;
  2         3  
  2         66  
4 2     2   7 use warnings;
  2         5  
  2         133  
5              
6             our $VERSION = '0.015'; # VERSION
7              
8 2     2   8 use parent qw(IO::Async::Notifier);
  2         3  
  2         15  
9              
10             =head1 NAME
11              
12             Net::Async::Slack::RTM - realtime messaging support for L
13              
14             =head1 DESCRIPTION
15              
16             This is a basic wrapper for Slack's RTM features.
17              
18             The realtime messaging API is mostly useful as an event stream. Although it is
19             possible to send messages through this API as well - see L - the
20             main HTTP API offers more features.
21              
22             For a full list of events, see L.
23              
24             =cut
25              
26 2     2   18956 no indirect;
  2         966  
  2         11  
27 2     2   497 use mro;
  2         509  
  2         11  
28              
29 2     2   68 use Future;
  2         3  
  2         66  
30 2     2   397 use Dir::Self;
  2         420  
  2         13  
31 2     2   558 use URI;
  2         4501  
  2         62  
32 2     2   304 use URI::QueryParam;
  2         94  
  2         45  
33 2     2   413 use URI::Template;
  2         5649  
  2         89  
34 2     2   386 use JSON::MaybeXS;
  2         7000  
  2         153  
35 2     2   342 use Time::Moment;
  2         1229  
  2         69  
36 2     2   565 use Syntax::Keyword::Try;
  2         2185  
  2         14  
37              
38 2     2   607 use IO::Async::Timer::Countdown;
  2         2047  
  2         88  
39 2     2   607 use Net::Async::WebSocket::Client;
  2         158966  
  2         123  
40              
41 2     2   1175 use Net::Async::Slack::Event::AccountsChanged;
  2         8  
  2         11  
42 2     2   1092 use Net::Async::Slack::Event::AppHomeOpened;
  2         7  
  2         10  
43 2     2   1911 use Net::Async::Slack::Event::AppMention;
  2         6  
  2         12  
44 2     2   1013 use Net::Async::Slack::Event::AppRateLimited;
  2         7  
  2         11  
45 2     2   1021 use Net::Async::Slack::Event::AppUninstalled;
  2         5  
  2         20  
46 2     2   988 use Net::Async::Slack::Event::BotAdded;
  2         21  
  2         13  
47 2     2   1030 use Net::Async::Slack::Event::BotChanged;
  2         5  
  2         13  
48 2     2   11 use Net::Async::Slack::Event::Bot;
  2         32  
  2         65  
49 2     2   1182 use Net::Async::Slack::Event::ChannelArchive;
  2         6  
  2         11  
50 2     2   1014 use Net::Async::Slack::Event::ChannelCreated;
  2         7  
  2         12  
51 2     2   1039 use Net::Async::Slack::Event::ChannelDeleted;
  2         5  
  2         11  
52 2     2   1203 use Net::Async::Slack::Event::ChannelHistoryChanged;
  2         7  
  2         10  
53 2     2   998 use Net::Async::Slack::Event::ChannelJoined;
  2         6  
  2         11  
54 2     2   992 use Net::Async::Slack::Event::ChannelLeft;
  2         6  
  2         10  
55 2     2   1009 use Net::Async::Slack::Event::ChannelMarked;
  2         6  
  2         35  
56 2     2   12 use Net::Async::Slack::Event::Channel;
  2         3  
  2         60  
57 2     2   1063 use Net::Async::Slack::Event::ChannelRename;
  2         5  
  2         10  
58 2     2   993 use Net::Async::Slack::Event::ChannelUnarchive;
  2         5  
  2         55  
59 2     2   996 use Net::Async::Slack::Event::CommandsChanged;
  2         5  
  2         11  
60 2     2   986 use Net::Async::Slack::Event::DndUpdated;
  2         5  
  2         10  
61 2     2   994 use Net::Async::Slack::Event::DndUpdatedUser;
  2         7  
  2         23  
62 2     2   995 use Net::Async::Slack::Event::EmailDomainChanged;
  2         5  
  2         11  
63 2     2   1024 use Net::Async::Slack::Event::EmojiChanged;
  2         5  
  2         13  
64 2     2   967 use Net::Async::Slack::Event::FileChange;
  2         6  
  2         10  
65 2     2   972 use Net::Async::Slack::Event::FileCommentAdded;
  2         5  
  2         11  
66 2     2   1028 use Net::Async::Slack::Event::FileCommentDeleted;
  2         5  
  2         11  
67 2     2   950 use Net::Async::Slack::Event::FileCommentEdited;
  2         5  
  2         11  
68 2     2   986 use Net::Async::Slack::Event::FileCreated;
  2         5  
  2         12  
69 2     2   964 use Net::Async::Slack::Event::FileDeleted;
  2         7  
  2         12  
70 2     2   1290 use Net::Async::Slack::Event::FilePublic;
  2         8  
  2         12  
71 2     2   1091 use Net::Async::Slack::Event::FileShared;
  2         7  
  2         15  
72 2     2   1006 use Net::Async::Slack::Event::FileUnshared;
  2         8  
  2         12  
73 2     2   1192 use Net::Async::Slack::Event::Goodbye;
  2         5  
  2         11  
74 2     2   1004 use Net::Async::Slack::Event::GridMigrationFinished;
  2         6  
  2         12  
75 2     2   1011 use Net::Async::Slack::Event::GridMigrationStarted;
  2         5  
  2         12  
76 2     2   998 use Net::Async::Slack::Event::GroupArchive;
  2         6  
  2         11  
77 2     2   950 use Net::Async::Slack::Event::GroupClose;
  2         5  
  2         12  
78 2     2   1179 use Net::Async::Slack::Event::GroupDeleted;
  2         130  
  2         18  
79 2     2   1028 use Net::Async::Slack::Event::GroupHistoryChanged;
  2         6  
  2         11  
80 2     2   969 use Net::Async::Slack::Event::GroupJoined;
  2         6  
  2         11  
81 2     2   955 use Net::Async::Slack::Event::GroupLeft;
  2         6  
  2         13  
82 2     2   1082 use Net::Async::Slack::Event::GroupMarked;
  2         6  
  2         10  
83 2     2   927 use Net::Async::Slack::Event::GroupOpen;
  2         6  
  2         10  
84 2     2   1006 use Net::Async::Slack::Event::GroupRename;
  2         5  
  2         12  
85 2     2   983 use Net::Async::Slack::Event::GroupUnarchive;
  2         5  
  2         11  
86 2     2   1034 use Net::Async::Slack::Event::Hello;
  2         6  
  2         11  
87 2     2   1034 use Net::Async::Slack::Event::ImClose;
  2         7  
  2         14  
88 2     2   981 use Net::Async::Slack::Event::ImCreated;
  2         5  
  2         13  
89 2     2   1002 use Net::Async::Slack::Event::ImHistoryChanged;
  2         6  
  2         11  
90 2     2   1008 use Net::Async::Slack::Event::ImMarked;
  2         9  
  2         13  
91 2     2   1036 use Net::Async::Slack::Event::ImOpen;
  2         6  
  2         14  
92 2     2   1015 use Net::Async::Slack::Event::LinkShared;
  2         6  
  2         12  
93 2     2   1035 use Net::Async::Slack::Event::ManualPresenceChange;
  2         5  
  2         13  
94 2     2   1039 use Net::Async::Slack::Event::MemberJoinedChannel;
  2         6  
  2         10  
95 2     2   969 use Net::Async::Slack::Event::MemberLeftChannel;
  2         6  
  2         11  
96 2     2   1022 use Net::Async::Slack::Event::MessageAppHome;
  2         7  
  2         11  
97 2     2   1021 use Net::Async::Slack::Event::MessageChannels;
  2         8  
  2         11  
98 2     2   1037 use Net::Async::Slack::Event::MessageGroups;
  2         7  
  2         24  
99 2     2   1045 use Net::Async::Slack::Event::MessageIm;
  2         7  
  2         13  
100 2     2   1007 use Net::Async::Slack::Event::MessageMpim;
  2         7  
  2         12  
101 2     2   1038 use Net::Async::Slack::Event::Message;
  2         6  
  2         17  
102 2     2   943 use Net::Async::Slack::Event::PinAdded;
  2         7  
  2         12  
103 2     2   1016 use Net::Async::Slack::Event::PinRemoved;
  2         6  
  2         13  
104 2     2   960 use Net::Async::Slack::Event::PrefChange;
  2         6  
  2         13  
105 2     2   971 use Net::Async::Slack::Event::PresenceChange;
  2         7  
  2         11  
106 2     2   968 use Net::Async::Slack::Event::PresenceQuery;
  2         5  
  2         230  
107 2     2   967 use Net::Async::Slack::Event::PresenceSub;
  2         6  
  2         11  
108 2     2   952 use Net::Async::Slack::Event::ReactionAdded;
  2         7  
  2         11  
109 2     2   1025 use Net::Async::Slack::Event::ReactionRemoved;
  2         8  
  2         14  
110 2     2   959 use Net::Async::Slack::Event::ReconnectURL;
  2         6  
  2         12  
111 2     2   1016 use Net::Async::Slack::Event::ResourcesAdded;
  2         6  
  2         15  
112 2     2   1126 use Net::Async::Slack::Event::ResourcesRemoved;
  2         6  
  2         10  
113 2     2   1016 use Net::Async::Slack::Event::ScopeDenied;
  2         6  
  2         11  
114 2     2   1118 use Net::Async::Slack::Event::ScopeGranted;
  2         6  
  2         14  
115 2     2   979 use Net::Async::Slack::Event::StarAdded;
  2         5  
  2         14  
116 2     2   1010 use Net::Async::Slack::Event::StarRemoved;
  2         6  
  2         11  
117 2     2   1007 use Net::Async::Slack::Event::SubteamCreated;
  2         6  
  2         12  
118 2     2   980 use Net::Async::Slack::Event::SubteamMembersChanged;
  2         7  
  2         13  
119 2     2   1012 use Net::Async::Slack::Event::SubteamSelfAdded;
  2         7  
  2         90  
120 2     2   988 use Net::Async::Slack::Event::SubteamSelfRemoved;
  2         7  
  2         12  
121 2     2   979 use Net::Async::Slack::Event::SubteamUpdated;
  2         5  
  2         11  
122 2     2   1003 use Net::Async::Slack::Event::TeamDomainChange;
  2         5  
  2         12  
123 2     2   1032 use Net::Async::Slack::Event::TeamJoin;
  2         5  
  2         10  
124 2     2   1139 use Net::Async::Slack::Event::TeamMigrationStarted;
  2         6  
  2         11  
125 2     2   969 use Net::Async::Slack::Event::TeamPlanChange;
  2         5  
  2         11  
126 2     2   1017 use Net::Async::Slack::Event::TeamPrefChange;
  2         7  
  2         15  
127 2     2   954 use Net::Async::Slack::Event::TeamProfileChange;
  2         6  
  2         13  
128 2     2   989 use Net::Async::Slack::Event::TeamProfileDelete;
  2         7  
  2         13  
129 2     2   946 use Net::Async::Slack::Event::TeamProfileReorder;
  2         6  
  2         12  
130 2     2   946 use Net::Async::Slack::Event::TeamRename;
  2         5  
  2         12  
131 2     2   981 use Net::Async::Slack::Event::TokensRevoked;
  2         7  
  2         19  
132 2     2   1033 use Net::Async::Slack::Event::URLVerification;
  2         6  
  2         11  
133 2     2   1003 use Net::Async::Slack::Event::UserChange;
  2         5  
  2         11  
134 2     2   973 use Net::Async::Slack::Event::UserResourceDenied;
  2         6  
  2         10  
135 2     2   950 use Net::Async::Slack::Event::UserResourceGranted;
  2         5  
  2         10  
136 2     2   806 use Net::Async::Slack::Event::UserResourceRemoved;
  2         5  
  2         11  
137 2     2   977 use Net::Async::Slack::Event::UserTyping;
  2         6  
  2         11  
138              
139 2     2   12 use Log::Any qw($log);
  2         4  
  2         24  
140              
141             my $json = JSON::MaybeXS->new;
142              
143             =head1 METHODS
144              
145             =head2 events
146              
147             This is the stream of events, as a L.
148              
149             Example usage:
150              
151             $rtm->events
152             ->filter(type => 'message')
153             ->sprintf_methods('> %s', $_->text)
154             ->say
155             ->await;
156              
157             =cut
158              
159             sub events {
160 0     0 1   my ($self) = @_;
161 0   0       $self->{events} //= do {
162 0           $self->ryu->source
163             }
164             }
165              
166             =head2 send_message
167              
168             Sends a message to a user or channel.
169              
170             This is limited (by the Slack API) to the L,
171             so it's only useful for simple messages.
172              
173             Takes the following named parameters:
174              
175             =over 4
176              
177             =item * id - custom message ID (optional)
178              
179             =item * channel - either a L instance, or a channel ID
180              
181             =back
182              
183             =cut
184              
185             sub send_message {
186 0     0 1   my ($self, %args) = @_;
187 0           my $id = $self->next_id($args{id});
188 0           my $f = $self->loop->new_future;
189             $self->ws->send_frame(
190             buffer => $json->encode({
191             type => 'message',
192             id => $id,
193             channel => (ref $args{channel} ? $args{channel}->id : $args{channel}),
194             text => $args{text},
195 0 0         }),
196             masked => 1
197             );
198 0           $self->{pending_message}{$id} = $f;
199             }
200              
201             =head1 METHODS - Internal
202              
203             You may not need to call these directly. If I'm wrong and you find yourself having
204             to do that, please complain via the usual channels.
205              
206             =head2 connect
207              
208             Establishes the connection. Called by the top-level L instance.
209              
210             =cut
211              
212             sub connect {
213 0     0 1   my ($self, %args) = @_;
214 0 0         my $uri = $self->wss_uri or die 'no websocket URI available';
215             $self->add_child(
216 0           $self->{ws} = Net::Async::WebSocket::Client->new(
217             on_frame => $self->curry::weak::on_frame,
218             )
219             );
220 0           $log->tracef('URL for websockets will be %s', "$uri");
221             $self->{ws}->connect(
222 0           url => "$uri",
223             )
224             }
225              
226             sub on_frame {
227 0     0 0   my ($self, $ws, $bytes) = @_;
228 0           my $text = Encode::decode_utf8($bytes);
229              
230             # Empty frame is used for PING, send a response back
231 0 0         if(!length($text)) {
232 0           $ws->send_frame('');
233             } else {
234 0           $log->tracef("<< %s", $text);
235             try {
236             my $data = $json->decode($text);
237             if(my $id = $data->{reply_to}) {
238             if(my $f = delete $self->{pending_message}{$id}) {
239             if($data->{ok}) {
240             $f->done(
241             id => $id,
242             ts => $data->{ts},
243             text => $data->{text},
244             );
245             } else {
246             $f->fail(
247             'Failed to send message: ' . $data->{error}{msg},
248             'slack_rtm',
249             code => @{$data->{error}}{qw(code msg)}
250             );
251             }
252             } else {
253             # This can happen with the initial stream of events, so maybe
254             # a warning is not necessary.
255             $log->warnf('Had reply %s to message, but it was not listed as pending, content is %s', $id, $data);
256             }
257             } elsif(my $ev = Net::Async::Slack::EventType->from_json($data)) {
258             $log->tracef("Have event [%s], emitting", $ev->type);
259             $self->events->emit($ev);
260             } else {
261             $self->events->emit($data);
262             }
263 0           } catch {
264             $log->errorf("Exception in websocket raw frame handling: %s (original text %s)", $@, $text);
265             }
266             }
267             }
268              
269 0     0 0   sub slack { shift->{slack} }
270              
271 0     0 0   sub wss_uri { shift->{wss_uri} }
272              
273 0     0 0   sub ws { shift->{ws} }
274              
275 0     0 0   sub ryu { shift->{ryu} }
276              
277             sub next_id {
278 0     0 0   my ($self, $id) = @_;
279 0   0       $self->{last_id} = $id // ++$self->{last_id};
280             }
281              
282             sub configure {
283 0     0 1   my ($self, %args) = @_;
284 0           for my $k (qw(slack wss_uri)) {
285 0 0         $self->{$k} = delete $args{$k} if exists $args{$k};
286             }
287 0           $self->next::method(%args);
288             }
289              
290             sub ping_timer {
291 0     0 0   my ($self) = @_;
292 0   0       $self->{ping_timer} ||= do {
293             $self->add_child(
294             my $timer = IO::Async::Timer::Countdown->new(
295             delay => 10,
296 0     0     on_expire => $self->$curry::weak(sub { shift->trigger_ping }),
  0            
297             )
298             );
299 0           $timer
300             }
301             }
302              
303             sub trigger_ping {
304 0     0 0   my ($self, %args) = @_;
305 0           my $id = $self->next_id($args{id});
306 0           $self->ws->send_frame(
307             buffer => $json->encode({
308             type => 'ping',
309             id => $id,
310             }),
311             masked => 1
312             );
313 0           $self->ping_timer->reset;
314 0 0         $self->ping_timer->start if $self->ping_timer->is_expired;
315             }
316              
317             sub _add_to_loop {
318 0     0     my ($self, $loop) = @_;
319             $self->add_child(
320 0           $self->{ryu} = Ryu::Async->new
321             );
322 0           $self->ping_timer->start;
323 0   0       $self->{last_id} //= 0;
324             }
325              
326             1;
327              
328             =head1 AUTHOR
329              
330             Tom Molesworth
331              
332             =head1 LICENSE
333              
334             Copyright Tom Molesworth 2016-2024. Licensed under the same terms as Perl itself.
335