File Coverage

blib/lib/Net/Async/Slack/Socket.pm
Criterion Covered Total %
statement 366 484 75.6
branch 0 28 0.0
condition 0 28 0.0
subroutine 122 153 79.7
pod 5 23 21.7
total 493 716 68.8


line stmt bran cond sub pod time code
1             package Net::Async::Slack::Socket;
2              
3 2     2   429840 use strict;
  2         6  
  2         116  
4 2     2   14 use warnings;
  2         4  
  2         212  
5              
6             our $VERSION = '0.015'; # VERSION
7             our $AUTHORITY = 'cpan:TEAM'; # AUTHORITY
8              
9 2     2   11 use parent qw(IO::Async::Notifier);
  2         3  
  2         37  
10              
11             =head1 NAME
12              
13             Net::Async::Slack::Socket - socket-mode notifications for L
14              
15             =head1 DESCRIPTION
16              
17             This is a basic wrapper for Slack's socket-mode features.
18              
19             See L for some background on using this feature.
20              
21             This provides an event stream using websockets.
22              
23             For a full list of events, see L.
24              
25             =cut
26              
27 2     2   27066 use Syntax::Keyword::Try;
  2         3102  
  2         22  
28 2     2   811 no indirect qw(fatal);
  2         2067  
  2         16  
29 2     2   720 use mro;
  2         914  
  2         16  
30              
31 2     2   77 use Future;
  2         4  
  2         97  
32 2     2   754 use Future::AsyncAwait;
  2         3747  
  2         14  
33 2     2   689 use Dir::Self;
  2         898  
  2         18  
34 2     2   815 use URI;
  2         11017  
  2         133  
35 2     2   1378 use URI::QueryParam;
  2         188  
  2         76  
36 2     2   1113 use URI::Template;
  2         13242  
  2         220  
37 2     2   705 use JSON::MaybeUTF8 qw(:v2);
  2         18861  
  2         403  
38 2     2   653 use Time::Moment;
  2         2640  
  2         98  
39              
40 2     2   661 use IO::Async::Timer::Countdown;
  2         3482  
  2         102  
41 2     2   636 use Net::Async::WebSocket::Client;
  2         199134  
  2         132  
42              
43             # We have a long list of events, these are all autogenerated - the
44             # idea being that you should be able to `->isa()` or check `->type`
45             # to filter on the features you are interested in.
46 2     2   616 use Net::Async::Slack::Event::AccountsChanged;
  2         8  
  2         24  
47 2     2   609 use Net::Async::Slack::Event::AppHomeOpened;
  2         5  
  2         15  
48 2     2   547 use Net::Async::Slack::Event::AppMention;
  2         12  
  2         13  
49 2     2   653 use Net::Async::Slack::Event::AppRateLimited;
  2         4  
  2         15  
50 2     2   592 use Net::Async::Slack::Event::AppUninstalled;
  2         5  
  2         12  
51 2     2   1026 use Net::Async::Slack::Event::BlockActions;
  2         5  
  2         11  
52 2     2   644 use Net::Async::Slack::Event::BotAdded;
  2         5  
  2         13  
53 2     2   544 use Net::Async::Slack::Event::BotChanged;
  2         5  
  2         14  
54 2     2   10 use Net::Async::Slack::Event::Bot;
  2         4  
  2         53  
55 2     2   565 use Net::Async::Slack::Event::ChannelArchive;
  2         4  
  2         15  
56 2     2   635 use Net::Async::Slack::Event::ChannelCreated;
  2         4  
  2         13  
57 2     2   656 use Net::Async::Slack::Event::ChannelDeleted;
  2         6  
  2         12  
58 2     2   621 use Net::Async::Slack::Event::ChannelHistoryChanged;
  2         5  
  2         12  
59 2     2   600 use Net::Async::Slack::Event::ChannelJoined;
  2         4  
  2         15  
60 2     2   601 use Net::Async::Slack::Event::ChannelLeft;
  2         5  
  2         10  
61 2     2   593 use Net::Async::Slack::Event::ChannelMarked;
  2         4  
  2         15  
62 2     2   11 use Net::Async::Slack::Event::Channel;
  2         4  
  2         82  
63 2     2   587 use Net::Async::Slack::Event::ChannelRename;
  2         4  
  2         13  
64 2     2   614 use Net::Async::Slack::Event::ChannelUnarchive;
  2         5  
  2         11  
65 2     2   568 use Net::Async::Slack::Event::CommandsChanged;
  2         5  
  2         13  
66 2     2   619 use Net::Async::Slack::Event::DndUpdated;
  2         4  
  2         12  
67 2     2   586 use Net::Async::Slack::Event::DndUpdatedUser;
  2         5  
  2         12  
68 2     2   582 use Net::Async::Slack::Event::EmailDomainChanged;
  2         5  
  2         11  
69 2     2   772 use Net::Async::Slack::Event::EmojiChanged;
  2         5  
  2         14  
70 2     2   1022 use Net::Async::Slack::Event::EventCallback;
  2         24  
  2         14  
71 2     2   612 use Net::Async::Slack::Event::FileChange;
  2         6  
  2         13  
72 2     2   587 use Net::Async::Slack::Event::FileCommentAdded;
  2         5  
  2         14  
73 2     2   574 use Net::Async::Slack::Event::FileCommentDeleted;
  2         6  
  2         31  
74 2     2   654 use Net::Async::Slack::Event::FileCommentEdited;
  2         4  
  2         12  
75 2     2   554 use Net::Async::Slack::Event::FileCreated;
  2         4  
  2         14  
76 2     2   580 use Net::Async::Slack::Event::FileDeleted;
  2         4  
  2         14  
77 2     2   679 use Net::Async::Slack::Event::FilePublic;
  2         4  
  2         14  
78 2     2   568 use Net::Async::Slack::Event::FileShared;
  2         5  
  2         14  
79 2     2   568 use Net::Async::Slack::Event::FileUnshared;
  2         5  
  2         12  
80 2     2   559 use Net::Async::Slack::Event::Goodbye;
  2         5  
  2         14  
81 2     2   554 use Net::Async::Slack::Event::GridMigrationFinished;
  2         5  
  2         19  
82 2     2   573 use Net::Async::Slack::Event::GridMigrationStarted;
  2         7  
  2         14  
83 2     2   578 use Net::Async::Slack::Event::GroupArchive;
  2         4  
  2         112  
84 2     2   571 use Net::Async::Slack::Event::GroupClose;
  2         24  
  2         12  
85 2     2   681 use Net::Async::Slack::Event::GroupDeleted;
  2         6  
  2         14  
86 2     2   557 use Net::Async::Slack::Event::GroupHistoryChanged;
  2         5  
  2         12  
87 2     2   526 use Net::Async::Slack::Event::GroupJoined;
  2         5  
  2         11  
88 2     2   567 use Net::Async::Slack::Event::GroupLeft;
  2         4  
  2         13  
89 2     2   564 use Net::Async::Slack::Event::GroupMarked;
  2         4  
  2         12  
90 2     2   538 use Net::Async::Slack::Event::GroupOpen;
  2         4  
  2         26  
91 2     2   543 use Net::Async::Slack::Event::GroupRename;
  2         5  
  2         13  
92 2     2   678 use Net::Async::Slack::Event::GroupUnarchive;
  2         5  
  2         12  
93 2     2   560 use Net::Async::Slack::Event::Hello;
  2         6  
  2         14  
94 2     2   575 use Net::Async::Slack::Event::ImClose;
  2         4  
  2         12  
95 2     2   554 use Net::Async::Slack::Event::ImCreated;
  2         5  
  2         15  
96 2     2   557 use Net::Async::Slack::Event::ImHistoryChanged;
  2         6  
  2         12  
97 2     2   540 use Net::Async::Slack::Event::ImMarked;
  2         4  
  2         17  
98 2     2   559 use Net::Async::Slack::Event::ImOpen;
  2         6  
  2         11  
99 2     2   528 use Net::Async::Slack::Event::LinkShared;
  2         4  
  2         13  
100 2     2   731 use Net::Async::Slack::Event::ManualPresenceChange;
  2         4  
  2         12  
101 2     2   556 use Net::Async::Slack::Event::MemberJoinedChannel;
  2         5  
  2         11  
102 2     2   577 use Net::Async::Slack::Event::MemberLeftChannel;
  2         5  
  2         14  
103 2     2   1017 use Net::Async::Slack::Event::MessageAction;
  2         6  
  2         11  
104 2     2   584 use Net::Async::Slack::Event::MessageAppHome;
  2         6  
  2         13  
105 2     2   569 use Net::Async::Slack::Event::MessageChannels;
  2         7  
  2         12  
106 2     2   564 use Net::Async::Slack::Event::MessageGroups;
  2         6  
  2         13  
107 2     2   548 use Net::Async::Slack::Event::MessageIm;
  2         5  
  2         16  
108 2     2   599 use Net::Async::Slack::Event::MessageMpim;
  2         4  
  2         14  
109 2     2   570 use Net::Async::Slack::Event::Message;
  2         5  
  2         12  
110 2     2   564 use Net::Async::Slack::Event::PinAdded;
  2         5  
  2         12  
111 2     2   565 use Net::Async::Slack::Event::PinRemoved;
  2         6  
  2         12  
112 2     2   571 use Net::Async::Slack::Event::PrefChange;
  2         5  
  2         18  
113 2     2   558 use Net::Async::Slack::Event::PresenceChange;
  2         5  
  2         42  
114 2     2   567 use Net::Async::Slack::Event::PresenceQuery;
  2         8  
  2         13  
115 2     2   701 use Net::Async::Slack::Event::PresenceSub;
  2         6  
  2         12  
116 2     2   556 use Net::Async::Slack::Event::ReactionAdded;
  2         4  
  2         12  
117 2     2   610 use Net::Async::Slack::Event::ReactionRemoved;
  2         9  
  2         15  
118 2     2   546 use Net::Async::Slack::Event::ReconnectURL;
  2         6  
  2         12  
119 2     2   754 use Net::Async::Slack::Event::ResourcesAdded;
  2         6  
  2         13  
120 2     2   610 use Net::Async::Slack::Event::ResourcesRemoved;
  2         6  
  2         12  
121 2     2   639 use Net::Async::Slack::Event::ScopeDenied;
  2         4  
  2         13  
122 2     2   703 use Net::Async::Slack::Event::ScopeGranted;
  2         5  
  2         13  
123 2     2   1002 use Net::Async::Slack::Event::Shortcut;
  2         8  
  2         12  
124 2     2   982 use Net::Async::Slack::Event::SlashCommands;
  2         7  
  2         81  
125 2     2   600 use Net::Async::Slack::Event::StarAdded;
  2         5  
  2         14  
126 2     2   563 use Net::Async::Slack::Event::StarRemoved;
  2         6  
  2         12  
127 2     2   539 use Net::Async::Slack::Event::SubteamCreated;
  2         15  
  2         12  
128 2     2   593 use Net::Async::Slack::Event::SubteamMembersChanged;
  2         7  
  2         16  
129 2     2   552 use Net::Async::Slack::Event::SubteamSelfAdded;
  2         5  
  2         11  
130 2     2   528 use Net::Async::Slack::Event::SubteamSelfRemoved;
  2         5  
  2         16  
131 2     2   528 use Net::Async::Slack::Event::SubteamUpdated;
  2         6  
  2         12  
132 2     2   572 use Net::Async::Slack::Event::TeamDomainChange;
  2         5  
  2         12  
133 2     2   570 use Net::Async::Slack::Event::TeamJoin;
  2         4  
  2         13  
134 2     2   552 use Net::Async::Slack::Event::TeamMigrationStarted;
  2         6  
  2         12  
135 2     2   579 use Net::Async::Slack::Event::TeamPlanChange;
  2         5  
  2         17  
136 2     2   575 use Net::Async::Slack::Event::TeamPrefChange;
  2         6  
  2         12  
137 2     2   548 use Net::Async::Slack::Event::TeamProfileChange;
  2         6  
  2         19  
138 2     2   557 use Net::Async::Slack::Event::TeamProfileDelete;
  2         5  
  2         11  
139 2     2   547 use Net::Async::Slack::Event::TeamProfileReorder;
  2         6  
  2         13  
140 2     2   546 use Net::Async::Slack::Event::TeamRename;
  2         6  
  2         27  
141 2     2   563 use Net::Async::Slack::Event::TokensRevoked;
  2         7  
  2         16  
142 2     2   570 use Net::Async::Slack::Event::URLVerification;
  2         5  
  2         11  
143 2     2   547 use Net::Async::Slack::Event::UserChange;
  2         5  
  2         18  
144 2     2   589 use Net::Async::Slack::Event::UserResourceDenied;
  2         8  
  2         12  
145 2     2   560 use Net::Async::Slack::Event::UserResourceGranted;
  2         4  
  2         12  
146 2     2   2839 use Net::Async::Slack::Event::UserResourceRemoved;
  2         5  
  2         13  
147 2     2   729 use Net::Async::Slack::Event::UserTyping;
  2         5  
  2         12  
148 2     2   1123 use Net::Async::Slack::Event::ViewSubmission;
  2         6  
  2         12  
149 2     2   1018 use Net::Async::Slack::Event::WorkflowStepEdit;
  2         6  
  2         13  
150              
151 2     2   13 use List::Util qw(min);
  2         5  
  2         193  
152 2     2   14 use Log::Any qw($log);
  2         5  
  2         23  
153              
154             =head1 METHODS
155              
156             =head2 events
157              
158             This is the stream of events, as a L.
159              
160             Example usage:
161              
162             $rtm->events
163             ->filter(type => 'message')
164             ->sprintf_methods('> %s', $_->text)
165             ->say
166             ->await;
167              
168             =cut
169              
170             sub events {
171 0     0 1   my ($self) = @_;
172 0   0       $self->{events} //= do {
173 0           $self->ryu->source
174             }
175             }
176              
177             =head2 handle_unfurl_domain
178              
179             Registers a handler for URLs.
180              
181             Takes the following named parameters:
182              
183             =over 4
184              
185             =item * C - which host/domain to respond to, e.g. C for L
186              
187             =item * C - a callback, expected to take a L instance and return a L with a Slack message
188              
189             =back
190              
191             Example usage:
192              
193             $sock->handle_unfurl_domain(
194             domain => 'service.local',
195             handler => async sub ($uri) {
196             my ($id) = $uri->path =~ m{/id/([0-9]+)}
197             or return undef;
198             return +{
199             blocks => [ {
200             "type" => "section",
201             "text" => {
202             "type" => "mrkdwn",
203             "text" => "Request with ID `$id`",
204             },
205             } ]
206             };
207             }
208             );
209              
210             Returns the L instance to allow chaining.
211              
212             =cut
213              
214             sub handle_unfurl_domain {
215 0     0 1   my ($self, %args) = @_;
216             $self->{unfurl_domain}{
217             delete $args{domain} || die 'need a domain'
218             } = $args{handler}
219 0 0 0       or die 'need a handler';
220 0           return $self;
221             }
222              
223             =head2 last_frame_epoch
224              
225             Returns the floating-point timestamp for the last frame we received. Will be
226             C if we have no frames yet.
227              
228             =cut
229              
230             sub last_frame_epoch {
231 0     0 1   my ($self) = @_;
232 0           return $self->{last_frame_epoch};
233             }
234              
235             =head1 METHODS - Internal
236              
237             You may not need to call these directly. If I'm wrong and you find yourself having
238             to do that, please complain via the usual channels.
239              
240             =head2 connect
241              
242             Establishes the connection. Called by the top-level L instance.
243              
244             =cut
245              
246 0     0 1   async sub connect {
247 0           my ($self, %args) = @_;
248 0 0 0       my $uri = delete($args{uri}) // $self->wss_uri or die 'no websocket URI available';
249 0           my $prev = delete $self->{ws};
250             $self->add_child(
251 0           $self->{ws} = Net::Async::WebSocket::Client->new(
252             on_frame => $self->curry::weak::on_frame,
253             on_ping_frame => $self->curry::weak::on_ping_frame,
254             on_close_frame => $self->curry::weak::on_close_frame,
255             )
256             );
257 0           $log->tracef('URL for websockets will be %s', "$uri");
258             my $res = await $self->{ws}->connect(
259 0           url => "$uri",
260             );
261 0 0         if($prev) {
262 0           $log->tracef('Closing previous websocket connection');
263             try {
264 0     0     $prev->send_close_frame('')->then(async sub {
265 0           $prev->close_now;
266 0 0         $self->remove_child($prev) if $prev->parent;
267             })->retain;
268 0           } catch($e) {
269             $log->errorf('Unable to clean up previous connection: %s', $e);
270             }
271             }
272 0           $self->event_mangler;
273 0           return $res;
274             }
275              
276             sub on_ping_frame {
277 0     0 0   my ($self, $ws, $bytes) = @_;
278 0           $self->connection_watchdog_nudge;
279 0           $ws->send_pong_frame('');
280             }
281              
282             sub on_close_frame {
283 0     0 0   my ($self) = @_;
284 0           $log->debugf('Received close frame');
285 0           $self->trigger_reconnect_if_needed
286             }
287              
288 0     0 0   async sub reconnect {
289 0           my ($self) = @_;
290 0           my $sleep = 0;
291 0           my $count = 0;
292 0           while(1) {
293 0           ++$count;
294             try {
295             $log->debugf('Attempting reconnect, try %d', $count);
296             my ($uri) = await Future->wait_any(
297             $self->slack->socket,
298             $self->loop->timeout_future(after => 30),
299             );
300             await Future->wait_any(
301             $self->connect(
302             uri => $uri
303             ),
304             $self->loop->timeout_future(after => 30),
305             );
306             return;
307 0           } catch($e) {
308             $sleep = min(30.0, ($sleep || 0.008) * 2);
309             $log->errorf('Failed to reconnect for socket mode, will try again in %.3fs: %s', $sleep, $e);
310             await $self->loop->delay_future(after => $sleep);
311             }
312             }
313             }
314              
315             sub on_close {
316 0     0 0   my ($self) = @_;
317 0           $self->trigger_reconnect_if_needed;
318             }
319              
320             sub trigger_reconnect_if_needed {
321 0     0 0   my ($self) = @_;
322 0           $log->tracef('trigger_reconnect_if_needed');
323 0           $self->connection_watchdog->stop;
324             return $self->{reconnecting} ||= $self->reconnect->on_ready(sub {
325             delete $self->{reconnecting}
326 0   0 0     });
  0            
327             }
328              
329             sub connection_watchdog {
330 0     0 0   my ($self) = @_;
331 0   0       $self->{connection_watchdog} ||= do {
332             $self->add_child(
333             my $timer = IO::Async::Timer::Countdown->new(
334             delay => 30,
335             on_expire => $self->$curry::weak(sub {
336 0     0     my ($self) = @_;
337 0           $self->trigger_reconnect_if_needed
338 0           }),
339             )
340             );
341 0           $timer->start;
342 0           $timer
343             };
344             }
345              
346             sub connection_watchdog_nudge {
347 0     0 0   my ($self) = @_;
348 0           my $timer = $self->connection_watchdog;
349 0           $timer->reset;
350 0 0         $timer->start if $timer->is_expired;
351 0           $self->{last_frame_epoch} = $self->loop->time;
352 0           $timer
353             }
354              
355             sub on_frame {
356 0     0 0   my ($self, $ws, $bytes) = @_;
357 0           $self->connection_watchdog_nudge;
358              
359             # Empty frame is used for PING, send a response back
360 0 0         if(!length($bytes)) {
361 0           $ws->send_frame('');
362 0           return;
363             }
364              
365 0   0       my $text = eval { Encode::decode_utf8($bytes) } // do {
  0            
366 0           $log->errorf('Invalid UTF8 received from Slack: %v02x', $bytes);
367 0           return;
368             };
369             try {
370             $log->tracef("<< %s", $text);
371             my $data = decode_json_text($text);
372             if($data->{type} eq 'disconnect') {
373             $log->debugf('Received disconnection notification, reason: %s (debug info: %s)', $data->{reason}, $data->{debug_info});
374             $self->trigger_reconnect_if_needed;
375             }
376              
377             my $pending;
378              
379             my $env_id = $data->{envelope_id};
380             if($env_id) {
381             if($data->{accepts_response_payload}) {
382              
383             # If the caller marks our future as done, send the result over as a payload
384             $pending = $self->loop->new_future->on_done($self->$curry::weak(sub {
385 0     0     my ($self, $payload) = @_;
386 0           $self->send_response($env_id, $payload)->retain;
387 0           return;
388             }));
389             # ... but auto-acknowledge before the deadline if they don't, for back-compatibility
390             my $f = $self->loop->delay_future(
391             after => 1.5
392             )->on_done($self->$curry::weak(sub {
393 0     0     my ($self) = @_;
394 0           $self->send_response($env_id)->retain;
395 0           return;
396             }));
397             # Make sure only one of the actions completes
398             Future->wait_any($pending, $f)->retain;
399             } else {
400             $self->send_response($env_id)->retain;
401             }
402             }
403              
404             if(my $type = $data->{payload}{type}) {
405             if($type eq 'event_callback') {
406             my $ev = Net::Async::Slack::EventType->from_json(
407             $data->{payload}{event}
408             );
409             $ev->{envelope_id} = $env_id;
410             $ev->{response_future} = $pending if $pending;
411             $log->tracef("Have event [%s], emitting", $ev->type);
412             $self->events->emit($ev);
413             } else {
414             if(my $ev = Net::Async::Slack::EventType->from_json(
415             $data->{payload}
416             )) {
417             $ev->{envelope_id} = $env_id;
418             $ev->{response_future} = $pending if $pending;
419             $log->tracef("Have event [%s], emitting", $ev->type);
420             $self->events->emit($ev);
421             } else {
422             $log->errorf('Failed to locate event type from payload %s', $data->{payload});
423             }
424             }
425             } elsif($type = $data->{type}) {
426             $log->tracef("Have generic/unknown event [%s], emitting", $data);
427             if(my $ev = Net::Async::Slack::EventType->from_json(
428             $data
429             )) {
430             $self->events->emit($ev);
431             } else {
432             $log->errorf('Unable to find event type from %s', $data);
433             }
434             }
435 0           } catch ($e) {
436             $log->errorf("Exception in websocket raw frame handling: %s (original text %s)", $e, $text);
437             }
438             }
439              
440             sub send_response {
441 0     0 0   my ($self, $env_id, $payload) = @_;
442 0 0         die 'need env_id' unless $env_id;
443 0 0         my $data = encode_json_utf8({
444             envelope_id => $env_id,
445             ($payload ? (payload => $payload) : ()),
446             });
447 0           $log->tracef(">> %s", $data);
448 0           return $self->ws->send_frame(
449             buffer => $data,
450             masked => 1
451             );
452             }
453              
454             sub next_id {
455 0     0 0   my ($self, $id) = @_;
456 0   0       $self->{last_id} = $id // ++$self->{last_id};
457             }
458              
459             sub configure {
460 0     0 1   my ($self, %args) = @_;
461 0           for my $k (qw(slack wss_uri)) {
462 0 0         $self->{$k} = delete $args{$k} if exists $args{$k};
463             }
464 0           $self->next::method(%args);
465             }
466              
467             sub ping_timer {
468 0     0 0   my ($self) = @_;
469 0   0       $self->{ping_timer} ||= do {
470             $self->add_child(
471             my $timer = IO::Async::Timer::Countdown->new(
472             delay => 10,
473 0     0     on_expire => $self->$curry::weak(sub { shift->trigger_ping }),
  0            
474             )
475             );
476 0           $timer
477             }
478             }
479              
480             sub event_mangler {
481 0     0 0   my ($self) = @_;
482 0     0     $self->{event_handling} //= $self->events->map($self->$curry::weak(async sub {
483 0           my ($self, $ev) = @_;
484             try {
485             if(my $code = $self->can($ev->type)) {
486             await $self->$code($ev);
487             } else {
488             $log->tracef('Ignoring event %s', $ev->type);
489             }
490 0           } catch ($e) {
491             $log->errorf('Event handling on %s failed: %s', $ev, $e);
492             }
493 0   0       }))->ordered_futures(
494             low => 16,
495             high => 100,
496             );
497             }
498              
499 0     0 0   async sub link_shared {
500 0           my ($self, $ev) = @_;
501 0           my %uri_map;
502 0           for my $link ($ev->{links}->@*) {
503 0 0         if(my $handler = $self->{unfurl_domain}{$link->{domain}}) {
504 0           my $uri = URI->new($link->{url});
505 0           $log->tracef('Unfurling URL %s', $uri);
506 0           my $unfurled = await $handler->($uri);
507 0 0         $uri_map{$uri} = $unfurled if $unfurled;
508             }
509             }
510 0 0         return unless keys %uri_map;
511             my $res = await $self->slack->chat_unfurl(
512             channel => $ev->{channel_id} // $ev->{channel}->id,
513             ts => $ev->{message_ts},
514 0           unfurls => \%uri_map,
515             );
516 0 0         die 'invalid URI unfurling' unless $res->{ok};
517 0           return;
518             }
519              
520             sub trigger_ping {
521 0     0 0   my ($self, %args) = @_;
522 0           my $id = $self->next_id($args{id});
523 0           $self->ws->send_frame(
524             buffer => encode_json_utf8({
525             type => 'ping',
526             id => $id,
527             }),
528             masked => 1
529             );
530 0           $self->ping_timer->reset;
531 0 0         $self->ping_timer->start if $self->ping_timer->is_expired;
532             }
533              
534             sub _add_to_loop {
535 0     0     my ($self, $loop) = @_;
536             $self->add_child(
537 0           $self->{ryu} = Ryu::Async->new
538             );
539             # $self->ping_timer->start;
540 0   0       $self->{last_id} //= 0;
541             }
542              
543 0     0 0   sub slack { shift->{slack} }
544              
545 0     0 0   sub wss_uri { shift->{wss_uri} }
546              
547 0     0 0   sub ws { shift->{ws} }
548              
549 0     0 0   sub ryu { shift->{ryu} }
550              
551             1;
552              
553             =head1 AUTHOR
554              
555             Tom Molesworth
556              
557             =head1 LICENSE
558              
559             Copyright Tom Molesworth 2016-2024. Licensed under the same terms as Perl itself.
560