File Coverage

blib/lib/AnyEvent/Discord.pm
Criterion Covered Total %
statement 543 826 65.7
branch 43 314 13.6
condition 4 55 7.2
subroutine 92 118 77.9
pod n/a
total 682 1313 51.9


line stmt bran cond sub pod time code
1             package AnyEvent::Discord;
2 4     4   109664 use v5.14;
  4         33  
3 4     4   2101 use Moops;
  4         211814  
  4         30  
4              
5 4     4   823492 class AnyEvent::Discord 0.7 {
  4     1   139  
  4     3   33  
  4         11  
  4         277  
  4         2178  
  4         8750  
  4         19  
  4         7279  
  4         8  
  4         34  
  4         569  
  4         9  
  4         200  
  4         24  
  4         9  
  4         372  
  4         154  
  4         24  
  4         8  
  4         48  
  4         20900  
  4         12  
  4         32  
  4         3986  
  4         15056  
  4         19  
  4         191685  
  4         11  
  4         41  
  4         2801  
  4         37991  
  4         42  
  4         3182  
  4         12388  
  4         28  
  4         7096  
  4         12622  
  4         36  
  4         603859  
  4         16  
  4         25  
  4         9  
  4         152  
  4         26  
  4         11  
  4         239  
  4         27  
  4         8  
  4         487  
  1         4474  
  0         0  
  3         13527  
  0            
6 4     4   2249 use Algorithm::Backoff::Exponential;
  4         21549  
  4         164  
7 4     4   1858 use AnyEvent::Discord::Payload;
  4         14  
  4         197  
8 1     1   746 use AnyEvent::WebSocket::Client;
  1     3   191177  
  1         53  
  3         1646  
  3         488685  
  3         163  
9 1     1   10 use Data::Dumper;
  1     3   3  
  1         71  
  3         31  
  3         8  
  3         257  
10 1     1   8 use JSON qw(decode_json encode_json);
  1     3   2  
  1         9  
  3         23  
  3         8  
  3         32  
11 1     1   949 use LWP::UserAgent;
  1     3   54648  
  1         62  
  3         2687  
  3         157279  
  3         147  
12 1     1   10 use HTTP::Request;
  1     3   2  
  1         33  
  3         27  
  3         8  
  3         100  
13 1     1   6 use HTTP::Headers;
  1     3   3  
  1         563  
  3         19  
  3         7  
  3         1709  
14              
15 1         13 our $VERSION = '0.7';
  3         45  
16 1         5 has version => ( is => 'ro', isa => Str, default => $VERSION );
  3         19  
17              
18 1         1231 has token => ( is => 'rw', isa => Str, required => 1 );
  3         3477  
19 1         1697 has base_uri => ( is => 'rw', isa => Str, default => 'https://discordapp.com/api' );
  3         5075  
20 1         1513 has socket_options => ( is => 'rw', isa => HashRef, default => sub { { max_payload_size => 1024 * 1024 } } );
  0         0  
  3         4482  
  3         129  
21 1         1588 has verbose => ( is => 'rw', isa => Num, default => 0 );
  3         4685  
22 1         1479 has user_agent => ( is => 'rw', isa => Str, default => sub { 'Perl-AnyEventDiscord/' . shift->VERSION } );
  0         0  
  3         4512  
  3         251  
23              
24 1         1545 has guilds => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         4450  
  3         135  
25 1         511 has channels => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         1620  
  3         12655  
26 1         471 has users => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         1406  
  3         123  
27              
28             # UserAgent
29 1         488 has _ua => ( is => 'rw', default => sub { LWP::UserAgent->new() } );
  0         0  
  3         1472  
  3         159  
30             # Caller-defined event handlers
31 1         433 has _events => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         1397  
  3         388  
32             # Internal-defined event handlers
33 1         459 has _internal_events => ( is => 'ro', isa => HashRef, builder => '_build_internal_events' );
  3         1369  
34             # WebSocket
35 1         455 has _socket => ( is => 'rw' );
  3         1345  
36             # Heartbeat timer
37 1         444 has _heartbeat => ( is => 'rw' );
  3         1284  
38             # Last Sequence
39 1         475 has _sequence => ( is => 'rw', isa => Num, default => 0 );
  3         1268  
40             # True if caller manually disconnected, to avoid reconnection
41 1         1478 has _force_disconnect => ( is => 'rw', isa => Bool, default => 0 );
  3         4447  
42             # Host the backoff algorithm for reconnection
43 1         1466 has _backoff => ( is => 'ro', default => sub { Algorithm::Backoff::Exponential->new( initial_delay => 1 ) } );
  0         0  
  3         4487  
  3         30264  
44              
45 1 50   1   1736 method _build_internal_events() {
  1 50   3   3  
  1     3   400  
  1         451  
  3         174  
  3         28  
  3         8  
  3         5179  
  3         16  
  3         1186  
  3         1305  
46             return {
47 0     0   0 'guild_create' => [sub { $self->_event_guild_create(@_); }],
48 0     0   0 'guild_delete' => [sub { $self->_event_guild_delete(@_); }],
49 0     0   0 'channel_create' => [sub { $self->_event_channel_create(@_); }],
50 0     0   0 'channel_delete' => [sub { $self->_event_channel_delete(@_); }],
51 0     0   0 'guild_member_create' => [sub { $self->_event_guild_member_create(@_); }],
52 3     0   113 'guild_member_remove' => [sub { $self->_event_guild_member_remove(@_); }]
  0         0  
53             };
54             }
55              
56 1 50   1   3463 method on(Str $event_type, CodeRef $handler) {
  1 50   1   3  
  1 50   1   196  
  1 50   5   8  
  1 50   3   2  
  1 50       185  
  1 50       8  
  1 50       2  
  1         245  
  1         251  
  5         3258  
  5         19  
  5         15  
  5         17  
  5         9  
  5         17  
  5         9  
  5         15  
  5         15  
  5         9  
  5         15  
  5         8  
  5         9  
  3         13398  
  3         8  
  3         562  
  3         34  
  3         9  
  3         515  
  3         24  
  3         8  
  3         805  
  3         718  
57 5         12 $event_type = lc($event_type);
58 5         28 $self->_debug('Requesting attach of handler ' . $handler . ' to event ' . $event_type);
59              
60 5   100     86 $self->_events->{$event_type} //= [];
61 5 100       9 return if (scalar(grep { $_ eq $handler } @{$self->_events->{$event_type}}) > 0);
  1         7  
  5         36  
62              
63 4         27 $self->_debug('Attaching handler ' . $handler . ' to event ' . $event_type);
64 4         27 push( @{$self->_events->{$event_type}}, $handler );
  4         21  
65             }
66              
67 1 50   1   7350 method off(Str $event_type, CodeRef $handler?) {
  1 50   1   2  
  1 50   1   200  
  1 50   3   8  
  1 50   3   3  
  1 50       152  
  1 100       8  
  1 50       3  
  1 100       432  
  1         222  
  3         1427  
  3         11  
  3         8  
  3         9  
  3         8  
  3         5  
  3         11  
  3         5  
  3         8  
  3         8  
  2         3  
  2         6  
  2         4  
  3         5  
  3         21435  
  3         9  
  3         665  
  3         24  
  3         9  
  3         466  
  3         24  
  3         16  
  3         1318  
  3         543  
68 3         6 $event_type = lc($event_type);
69 3   100     23 $self->_debug('Requesting detach of handler ' . ($handler or 'n/a') . ' from event ' . $event_type);
70 3 50       30 if ($self->_events->{$event_type}) {
71 3 100       7 if ($handler) {
72 2         3 my $index = 0;
73 2         5 while ($index < scalar(@{$self->_events->{$event_type}})) {
  4         16  
74 2 100       10 if ($self->_events->{$event_type}->[$index] eq $handler) {
75 1         7 $self->_debug('Detaching handler ' . $handler . ' from event ' . $event_type);
76 1         61 splice( @{$self->_events->{$event_type}}, $index, 1 );
  1         9  
77             }
78 2         5 $index++;
79             }
80             } else {
81 1         4 $self->_debug('Detaching ' . scalar(@{$self->_events->{$event_type}}) . ' handler(s) from event ' . $event_type);
  1         8  
82 1         10 delete($self->_events->{$event_type});
83             }
84             }
85             }
86              
87 1 0   1   1115 method connect() {
  1 0   0   2  
  1     3   1096  
  1         170  
  0         0  
  0         0  
  0         0  
  3         3437  
  3         9  
  3         3294  
  3         513  
88 0         0 my $gateway = $self->_lookup_gateway();
89              
90 0         0 $self->_debug('Connecting to ' . $gateway);
91              
92 0         0 my $ws = AnyEvent::WebSocket::Client->new($self->socket_options);
93             $ws->connect($gateway)->cb(sub {
94 0     0   0 my $socket = eval { shift->recv };
  0         0  
95 0 0       0 if ($@) {
96 0         0 $self->_debug('Received error connecting: ' . $@);
97 0         0 $self->_handle_internal_event('error', $@);
98 0         0 return;
99             }
100 0         0 $self->_debug('Connected to ' . $gateway);
101              
102 0         0 $self->_socket($socket);
103            
104             # If we send malformed content, bail out
105             $socket->on('parse_error', sub {
106 0         0 my ($c, $error) = @_;
107 0         0 $self->_debug(Data::Dumper::Dumper($error));
108 0         0 die $error;
109 0         0 });
110              
111             # Handle reconnection
112             $socket->on('finish', sub {
113 0         0 my ($c) = @_;
114 0         0 $self->_debug('Received disconnect');
115 0         0 $self->_handle_internal_event('disconnected');
116 0 0       0 unless ($self->_force_disconnect()) {
117 0         0 my $seconds = $self->_backoff->failure();
118 0         0 $self->_debug('Reconnecting in ' . $seconds);
119 0         0 my $reconnect;
120             $reconnect = AnyEvent->timer(
121             after => $seconds,
122             cb => sub {
123 0         0 $self->connect();
124 0         0 $reconnect = undef;
125 0         0 AnyEvent->condvar->send();
126             }
127 0         0 );
128             }
129 0         0 });
130              
131             # Event handler
132             $socket->on('each_message', sub {
133 0         0 my ($c, $message) = @_;
134 0         0 $self->_trace('ws in: ' . $message->{'body'});
135 0         0 my $payload;
136             try {
137 0         0 $payload = AnyEvent::Discord::Payload->from_json($message->{'body'});
138             } catch {
139 0         0 $self->_debug($_);
140 0         0 return;
141 0         0 };
142 0 0 0     0 unless ($payload and defined $payload->op) {
143 0         0 $self->_debug('Invalid payload received from Discord: ' . $message->{'body'});
144 0         0 return;
145             }
146 0 0 0     0 $self->_sequence(0 + $payload->s) if ($payload->s and $payload->s > 0);
147              
148 0 0       0 if ($payload->op == 10) {
    0          
149 0         0 $self->_event_hello($payload);
150             } elsif ($payload->d) {
151 0 0       0 if ($payload->d->{'author'}) {
152 0         0 my $user = $payload->d->{'author'};
153 0         0 $self->users->{$user->{'id'}} = $user->{'username'};
154             }
155 0         0 $self->_handle_event($payload);
156             }
157 0         0 });
158              
159 0         0 $self->_discord_identify();
160 0         0 $self->_debug('Completed connection sequence');
161 0         0 $self->_backoff->success();
162 0         0 AnyEvent->condvar->send();
163 0         0 });
164             }
165              
166 1 0   1   2130 method send($channel_id, $content) {
  1 0   0   3  
  1 0   3   195  
  1 0       155  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6600  
  3         83  
  3         595  
  3         479  
167 0         0 return $self->_discord_api('POST', 'channels/' . $channel_id . '/messages', encode_json({content => $content}));
168             }
169              
170 1 0   1   1621 method typing($channel_id) {
  1 0   0   3  
  1 0   3   258  
  1         164  
  0         0  
  0         0  
  0         0  
  0         0  
  3         5154  
  3         16  
  3         727  
  3         487  
171             return AnyEvent->timer(
172             after => 0,
173             interval => 5,
174             cb => sub {
175 0     0   0 $self->_discord_api('POST', 'channels/' . $channel_id . '/typing');
176 0         0 AnyEvent->condvar->send();
177             },
178 0         0 );
179             }
180              
181 1 0   1   1142 method close() {
  1 0   0   3  
  1     3   153  
  1         155  
  0         0  
  0         0  
  0         0  
  3         3334  
  3         19  
  3         505  
  3         479  
182 0         0 $self->_force_disconnect(1);
183 0         0 $self->{'_heartbeat'} = undef;
184 0         0 $self->{'_sequence'} = undef;
185 0         0 $self->_socket->close();
186             }
187              
188             # Make an HTTP request to the Discord API
189 1 0   1   4002 method _discord_api(Str $method, Str $path, $payload?) {
  1 0   1   3  
  1 0   1   212  
  1 0   0   8  
  1 0   3   3  
  1 0       171  
  1 0       8  
  1 0       2  
  1 0       376  
  1 0       176  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         11958  
  3         17  
  3         624  
  3         24  
  3         6  
  3         518  
  3         23  
  3         7  
  3         1162  
  3         548  
190 0         0 my $headers = HTTP::Headers->new(
191             Authorization => 'Bot ' . $self->token,
192             User_Agent => $self->user_agent,
193             Content_Type => 'application/json',
194             );
195 0         0 my $request = HTTP::Request->new(
196             uc($method),
197             join('/', $self->base_uri, $path),
198             $headers,
199             $payload,
200             );
201 0         0 $self->_trace('api req: ' . $request->as_string());
202 0         0 my $res = $self->_ua->request($request);
203 0         0 $self->_trace('api res: ' . $res->as_string());
204 0 0       0 if ($res->is_success()) {
205 0 0       0 if ($res->header('Content-Type') eq 'application/json') {
206 0         0 return decode_json($res->decoded_content());
207             } else {
208 0         0 return $res->decoded_content();
209             }
210             }
211 0         0 return;
212             }
213              
214             # Send the 'identify' event to the Discord websocket
215 1 0   1   1187 method _discord_identify() {
  1 0   0   2  
  1     3   169  
  1         170  
  0         0  
  0         0  
  0         0  
  3         3645  
  3         8  
  3         564  
  3         496  
216 0         0 $self->_debug('Sending identify');
217 0         0 $self->_ws_send_payload(AnyEvent::Discord::Payload->from_hashref({
218             op => 2,
219             d => {
220             token => $self->token,
221             compress => JSON::false,
222             large_threshold => 250,
223             shard => [0, 1],
224             properties => {
225             '$os' => 'linux',
226             '$browser' => $self->user_agent(),
227             '$device' => $self->user_agent(),
228             }
229             }
230             }));
231             }
232              
233             # Send a payload to the Discord websocket
234 1 0   1   8022 method _ws_send_payload(AnyEvent::Discord::Payload $payload) {
  1 0   1   3  
  1 0   0   202  
  1 0   3   8  
  1 0       3  
  1         218  
  1         150  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         22712  
  3         7  
  3         470  
  3         56  
  3         10  
  3         636  
  3         444  
235 0 0       0 unless ($self->_socket) {
236 0         0 $self->_debug('Attempted to send payload to disconnected socket');
237 0         0 return;
238             }
239 0         0 my $msg = $payload->as_json;
240 0         0 $self->_trace('ws out: ' . $msg);
241 0         0 $self->_socket->send($msg);
242             }
243              
244             # Look up the gateway endpoint using the Discord API
245 1 0   1   1279 method _lookup_gateway() {
  1 0   0   5  
  1     3   255  
  1         155  
  0         0  
  0         0  
  0         0  
  3         3633  
  3         8  
  3         736  
  3         458  
246 0         0 my $payload = $self->_discord_api('GET', 'gateway');
247 0 0 0     0 die 'Invalid gateway returned by API' unless ($payload and $payload->{url} and $payload->{url} =~ /^wss/);
      0        
248              
249             # Add the requested version and encoding to the provided URL
250 0         0 my $gateway = $payload->{url};
251 0 0       0 $gateway .= '/' unless ($gateway =~/\/$/);
252 0         0 $gateway .= '?v=6&encoding=json';
253 0         0 return $gateway;
254             }
255              
256             # Dispatch an internal event type
257 1 0   1   2365 method _handle_internal_event(Str $type) {
  1 0   1   3  
  1 0   0   231  
  1 0   3   8  
  1 0       3  
  1         298  
  1         174  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6844  
  3         49  
  3         559  
  3         23  
  3         7  
  3         850  
  3         445  
258 0         0 foreach my $event_source (qw(_internal_events _events)) {
259 0 0       0 if ($self->{$event_source}->{$type}) {
260             map {
261 0 0       0 $self->_debug('Sending ' . ( $event_source =~ /internal/ ? 'internal' : 'caller' ) . ' event ' . $type);
262 0         0 $_->($self);
263 0         0 } @{ $self->{$event_source}->{$type} };
  0         0  
264             }
265             }
266             }
267              
268             # Dispatch a Discord event type
269 1 50   1   2445 method _handle_event(AnyEvent::Discord::Payload $payload) {
  1 50   1   3  
  1 50   4   216  
  1 50   3   9  
  1 50       2  
  1         285  
  1         156  
  4         2808  
  4         13  
  4         10  
  4         11  
  4         6  
  4         20  
  4         7  
  4         6  
  3         7201  
  3         8  
  3         539  
  3         24  
  3         6  
  3         956  
  3         479  
270 4         15 my $type = lc($payload->t);
271 4         16 $self->_debug('Got event ' . $type);
272 4         34 foreach my $event_source (qw(_internal_events _events)) {
273 8 100       1627 if ($self->{$event_source}->{$type}) {
274             map {
275 4 100       22 $self->_debug('Sending ' . ( $event_source =~ /internal/ ? 'internal' : 'caller' ) . ' event ' . $type);
276 4         145 $_->($self, $payload->d, $payload->op);
277 4         7 } @{ $self->{$event_source}->{$type} };
  4         13  
278             }
279             }
280             }
281              
282             # Send debug messages to console if verbose is >=1
283 1 50   1   2211 method _debug(Str $message) {
  1 50   1   3  
  1 50   22   170  
  1 50   3   8  
  1 50       2  
  1         133  
  1         154  
  22         50  
  22         51  
  22         48  
  22         47  
  22         35  
  22         65  
  22         36  
  22         30  
  3         6477  
  3         7  
  3         550  
  3         22  
  3         17  
  3         351  
  3         486  
284 22 50       454 say time . ' ' . $message if ($self->verbose);
285             }
286              
287             # Send trace messages to console if verbose is 2
288 1 0   1   2094 method _trace(Str $message) {
  1 0   1   13  
  1 0   0   198  
  1 0   3   8  
  1 0       2  
  1         187  
  1         186  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6329  
  3         7  
  3         599  
  3         23  
  3         15  
  3         448  
  3         516  
289 0 0 0     0 say time . ' ' . $message if ($self->verbose and $self->verbose == 2);
290             }
291              
292             # Called when Discord provides the 'hello' event
293 1 0   1   2441 method _event_hello(AnyEvent::Discord::Payload $payload) {
  1 0   1   3  
  1 0   0   190  
  1 0   3   9  
  1 0       2  
  1         255  
  1         181  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         7237  
  3         8  
  3         563  
  3         24  
  3         7  
  3         754  
  3         605  
294 0         0 $self->_debug('Received hello event');
295 0         0 my $interval = $payload->d->{'heartbeat_interval'};
296             my $timer = AnyEvent->timer(
297             after => $interval * rand() / 1000,
298             interval => $interval / 1000,
299             cb => sub {
300 0     0   0 $self->_debug('Heartbeat');
301 0         0 $self->_ws_send_payload(AnyEvent::Discord::Payload->from_hashref({
302             op => 1,
303             d => $self->_sequence()
304             }));
305 0         0 AnyEvent->condvar->send();
306             }
307 0         0 );
308 0         0 $self->_heartbeat($timer);
309             }
310              
311             # GUILD_CREATE event
312 1 0 0 1   4220 method _event_guild_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   229  
  1 0   0   8  
  1 0   3   2  
  1 0       196  
  1 0       8  
  1 0       2  
  1 0       358  
  1 0       161  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         12669  
  3         7  
  3         691  
  3         23  
  3         7  
  3         576  
  3         33  
  3         9  
  3         1069  
  3         512  
313 0         0 $self->guilds->{$data->{'id'}} = $data->{'name'};
314              
315             # We get channel and user information along with the guild, populate those
316             # at the same time
317 0         0 foreach my $channel (@{$data->{'channels'}}) {
  0         0  
318 0 0       0 if ($channel->{'type'} == 0) {
319 0         0 $self->channels->{$channel->{'id'}} = $channel->{'name'};
320             }
321             }
322 0         0 foreach my $user (@{$data->{'members'}}) {
  0         0  
323 0         0 $self->users->{$user->{'user'}->{'id'}} = $user->{'user'}->{'username'};
324             }
325             }
326              
327             # GUILD_DELETE event
328 1 0 0 1   3973 method _event_guild_delete($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   263  
  1 0   0   8  
  1 0   3   2  
  1 0       166  
  1 0       8  
  1 0       3  
  1 0       131  
  1 0       164  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         12299  
  3         7  
  3         693  
  3         34  
  3         8  
  3         548  
  3         22  
  3         6  
  3         391  
  3         504  
329 0         0 delete($self->guilds->{$data->{'id'}});
330             }
331              
332             # CHANNEL_CREATE event
333 1 0 0 1   4050 method _event_channel_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   244  
  1 0   0   8  
  1 0   3   2  
  1 0       161  
  1 0       9  
  1 0       2  
  1 0       123  
  1 0       164  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         12273  
  3         7  
  3         764  
  3         24  
  3         10  
  3         524  
  3         24  
  3         6  
  3         428  
  3         499  
334 0         0 $self->channels->{$data->{'id'}} = $data->{'name'};
335             }
336              
337             # CHANNEL_DELETE event
338 1 0 0 1   4007 method _event_channel_delete($client, HashRef $data, Num $opcode?) {
  1 0 0 1   13  
  1 0   1   212  
  1 0   0   9  
  1 0   3   3  
  1 0       174  
  1 0       8  
  1 0       3  
  1 0       125  
  1 0       163  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         12477  
  3         8  
  3         748  
  3         32  
  3         8  
  3         526  
  3         23  
  3         7  
  3         422  
  3         483  
339 0         0 delete($self->channels->{$data->{'id'}});
340             }
341              
342             # GUILD_MEMBER_CREATE event
343 1 0 0 1   4149 method _event_guild_member_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   3  
  1 0   1   244  
  1 0   0   10  
  1 0   3   3  
  1 0       171  
  1 0       8  
  1 0       2  
  1 0       148  
  1 0       162  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         12292  
  3         19  
  3         679  
  3         25  
  3         7  
  3         537  
  3         24  
  3         6  
  3         474  
  3         481  
344 0         0 $self->users->{$data->{'id'}} = $data->{'username'};
345             }
346              
347             # GUILD_MEMBER_REMOVE event
348 1 0 0 1   4070 method _event_guild_member_remove($client, HashRef $data, Num $opcode?) {
  1 0 0 1   3  
  1 0   1   213  
  1 0   0   17  
  1 0   3   3  
  1 0       167  
  1 0       8  
  1 0       2  
  1 0       142  
  1 0       162  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         12245  
  3         8  
  3         646  
  3         25  
  3         17  
  3         527  
  3         25  
  3         6  
  3         447  
  3         471  
349 0         0 delete($self->users->{$data->{'id'}});
350             }
351             }
352              
353             1;
354              
355             =pod
356              
357             =head1 NAME
358              
359             AnyEvent::Discord - Provides an AnyEvent interface to the Discord bot API
360              
361             =head1 SYNOPSIS
362              
363             use AnyEvent::Discord;
364             my $client = AnyEvent::Discord->new({ token => 'mydiscordbottoken' });
365             $client->on('ready', sub { warn 'Connected'; });
366             $client->on('message_create', sub {
367             my ($client, $data) = @_;
368             warn '[' . $client->channels->{$data->{channel_id}} . ']' .
369             '(' . $data->{author}->{username} . ') - ' .
370             $data->{content};
371             });
372             $client->connect();
373             AnyEvent->condvar->recv;
374              
375             =head1 DESCRIPTION
376              
377             This module provides an AnyEvent interface for the Discord API over the REST
378             and WebSocket APIs. It is designed to be somewhat similar to the SlackRTM and
379             XMPP modules, with a subset of their far more mature functionality.
380              
381             To get started, one needs to create a new application in the Discord Developer
382             Portal (https://discord.com/developers). Once an application is created, a token
383             can be captured by navigating to the "Bot" tab on the left side and selecting
384             'Click to Reveal Token'. That generated token is the same token required by this
385             module.
386              
387             =head1 CONFIGURATION ACCESSORS
388              
389             =over 4
390              
391             =item token (String) (required)
392              
393             The token generated by the Discord Application portal, under Bot.
394              
395             =item base_uri (String) (optional)
396              
397             The base URI for communicating with the Discord API.
398              
399             =item socket_options (HashRef) (optional)
400              
401             Used to override options to sent to AnyEvent::WebSocket::Client, if needed.
402              
403             =item verbose (Num) (defaults to 0)
404              
405             Verbose output, writes internal debug information at 1, additionally writes
406             network conversation at 2.
407              
408             =back
409              
410             =head1 DATA ACCESSORS
411              
412             =over 4
413              
414             =item guilds
415              
416             Available/created/seen guilds, as a hashmap of id => name
417              
418             =item channels
419              
420             Available/created/seen channels, as a hashmap of id => name
421              
422             =item users
423              
424             Available/created/seen users, as a hashmap of id => name
425              
426             =back
427              
428             =head1 PUBLIC METHODS
429              
430             =over 4
431              
432             =item new(\%arguments)
433              
434             Instantiate the AnyEvent::Discord client. The hashref of arguments matches the
435             configuration accessors listed above. A common invocation looks like:
436              
437             my $client = AnyEvent::Discord->new({ token => 'ABCDEF' });
438              
439             =item on($event_type, \&handler)
440              
441             Attach an event handler to a defined event type. If an invalid event type is
442             specified, no error will occur -- this is mostly to be able to handle events
443             that are created after this module is published. This is an append method, so
444             calling on() for an event multiple times will call each callback assigned. If
445             the handler already exists for an event, no error will be returned, but the
446             handler will not be called twice.
447              
448             Discord event types: https://discord.com/developers/docs/topics/gateway#list-of-intents
449              
450             Opcodes: https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-opcodes
451              
452             These events receive the parameters client, data object (d) and the opcode (op).
453             The $client variable is this instance of AnyEvent::Discord, and the contents of
454             $data and $op are dependent on the event type.
455              
456             sub event_responder {
457             my ($client, $data, $opcode) = @_;
458             return;
459             }
460              
461             Internal event types:
462              
463             =over 4
464              
465             =item disconnected
466              
467             Receives no parameters, just notifies a disconnection will occur. It will
468             auto reconnect.
469              
470             =item error
471              
472             Receives an error message as a parameter, allows internal handling of errors
473             that are not a hard failure.
474              
475             =back
476              
477             =item off($event_type, \&handler?)
478              
479             Detach an event handler from a defined event type. If the handler does not
480             exist for the event, no error will be returned. If no handler is provided, all
481             handlers for the event type will be removed.
482              
483             =item connect()
484              
485             Start connecting to the Discord API and return immediately. In a new AnyEvent
486             application, this would come before executing "AnyEvent->condvar->recv". This
487             method will retrieve the available gateway endpoint, create a connection,
488             identify itself, begin a heartbeat, and once complete, Discord will fire a
489             'ready' event to the handler.
490              
491             =item send($channel_id, $content)
492              
493             Send a message to the provided channel.
494              
495             =item typing($channel_id)
496              
497             Starts the "typing..." indicator in the provided channel. This method issues the
498             typing request, and starts a timer on the caller's behalf to keep the indicator
499             active. Returns an instance of that timer to allow the caller to undef it when
500             the typing indicator should be stopped.
501              
502             my $instance = $client->typing($channel);
503             # ... perform some actions
504             $instance = undef;
505             # typing indicator disappears
506              
507             =item close()
508              
509             Close the connection to the server.
510              
511             =back
512              
513             =head1 CAVEATS
514              
515             This is incredibly unfinished.
516              
517             =head1 AUTHOR
518              
519             Nick Melnick <nmelnick@cpan.org>
520              
521             =head1 COPYRIGHT AND LICENSE
522              
523             This software is copyright (c) 2021, Nick Melnick.
524              
525             This is free software; you can redistribute it and/or modify it under the same
526             terms as the Perl 5 programming language system itself.
527              
528             =cut