File Coverage

blib/lib/AnyEvent/Discord.pm
Criterion Covered Total %
statement 543 821 66.1
branch 43 314 13.6
condition 4 52 7.6
subroutine 92 118 77.9
pod n/a
total 682 1305 52.2


line stmt bran cond sub pod time code
1             package AnyEvent::Discord;
2 4     4   109624 use v5.14;
  4         38  
3 4     4   2206 use Moops;
  4         206386  
  4         28  
4              
5 4     4   814399 class AnyEvent::Discord 0.6 {
  4     1   138  
  4     3   41  
  4         8  
  4         261  
  4         2456  
  4         8799  
  4         16  
  4         7501  
  4         8  
  4         39  
  4         587  
  4         11  
  4         212  
  4         25  
  4         9  
  4         406  
  4         137  
  4         33  
  4         9  
  4         65  
  4         21318  
  4         8  
  4         37  
  4         4189  
  4         15205  
  4         20  
  4         189174  
  4         13  
  4         44  
  4         2748  
  4         36099  
  4         40  
  4         3226  
  4         12238  
  4         28  
  4         6425  
  4         12406  
  4         36  
  4         601390  
  4         16  
  4         23  
  4         10  
  4         98  
  4         22  
  4         10  
  4         190  
  4         21  
  4         13  
  4         515  
  1         4460  
  0         0  
  3         13695  
  0            
6 4     4   2464 use Algorithm::Backoff::Exponential;
  4         19985  
  4         160  
7 4     4   1985 use AnyEvent::Discord::Payload;
  4         15  
  4         229  
8 1     1   794 use AnyEvent::WebSocket::Client;
  1     3   179425  
  1         70  
  3         1944  
  3         517760  
  3         168  
9 1     1   10 use Data::Dumper;
  1     3   3  
  1         69  
  3         31  
  3         10  
  3         248  
10 1     1   7 use JSON qw(decode_json encode_json);
  1     3   3  
  1         11  
  3         22  
  3         8  
  3         38  
11 1     1   991 use LWP::UserAgent;
  1     3   47301  
  1         62  
  3         2889  
  3         139338  
  3         214  
12 1     1   10 use HTTP::Request;
  1     3   3  
  1         35  
  3         37  
  3         7  
  3         111  
13 1     1   7 use HTTP::Headers;
  1     3   3  
  1         617  
  3         19  
  3         8  
  3         1777  
14              
15 1         16 our $VERSION = '0.6';
  3         44  
16 1         5 has version => ( is => 'ro', isa => Str, default => $VERSION );
  3         17  
17              
18 1         1228 has token => ( is => 'rw', isa => Str, required => 1 );
  3         3644  
19 1         1818 has base_uri => ( is => 'rw', isa => Str, default => 'https://discordapp.com/api' );
  3         5152  
20 1         1489 has socket_options => ( is => 'rw', isa => HashRef, default => sub { { max_payload_size => 1024 * 1024 } } );
  0         0  
  3         4497  
  3         122  
21 1         1637 has verbose => ( is => 'rw', isa => Num, default => 0 );
  3         4626  
22 1         1486 has user_agent => ( is => 'rw', isa => Str, default => sub { 'Perl-AnyEventDiscord/' . shift->VERSION } );
  0         0  
  3         4422  
  3         281  
23              
24 1         1528 has guilds => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         4878  
  3         128  
25 1         508 has channels => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         1613  
  3         12258  
26 1         528 has users => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         1488  
  3         121  
27              
28             # UserAgent
29 1         479 has _ua => ( is => 'rw', default => sub { LWP::UserAgent->new() } );
  0         0  
  3         1408  
  3         168  
30             # Caller-defined event handlers
31 1         438 has _events => ( is => 'ro', isa => HashRef, default => sub { {} } );
  0         0  
  3         1661  
  3         386  
32             # Internal-defined event handlers
33 1         456 has _internal_events => ( is => 'ro', isa => HashRef, builder => '_build_internal_events' );
  3         1409  
34             # WebSocket
35 1         463 has _socket => ( is => 'rw' );
  3         1415  
36             # Heartbeat timer
37 1         408 has _heartbeat => ( is => 'rw' );
  3         1233  
38             # Last Sequence
39 1         452 has _sequence => ( is => 'rw', isa => Num, default => 0 );
  3         1235  
40             # True if caller manually disconnected, to avoid reconnection
41 1         1578 has _force_disconnect => ( is => 'rw', isa => Bool, default => 0 );
  3         4492  
42             # Host the backoff algorithm for reconnection
43 1         1490 has _backoff => ( is => 'ro', default => sub { Algorithm::Backoff::Exponential->new( initial_delay => 4 ) } );
  0         0  
  3         4467  
  3         29415  
44              
45 1 50   1   1815 method _build_internal_events() {
  1 50   3   5  
  1     3   392  
  1         435  
  3         166  
  3         25  
  3         8  
  3         5515  
  3         7  
  3         1151  
  3         1368  
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   109 'guild_member_remove' => [sub { $self->_event_guild_member_remove(@_); }]
  0         0  
53             };
54             }
55              
56 1 50   1   3573 method on(Str $event_type, CodeRef $handler) {
  1 50   1   3  
  1 50   1   196  
  1 50   5   22  
  1 50   3   2  
  1 50       186  
  1 50       7  
  1 50       9  
  1         272  
  1         326  
  5         3282  
  5         18  
  5         19  
  5         16  
  5         10  
  5         17  
  5         12  
  5         15  
  5         15  
  5         8  
  5         14  
  5         8  
  5         11  
  3         10547  
  3         7  
  3         569  
  3         24  
  3         8  
  3         544  
  3         25  
  3         11  
  3         804  
  3         793  
57 5         13 $event_type = lc($event_type);
58 5         29 $self->_debug('Requesting attach of handler ' . $handler . ' to event ' . $event_type);
59              
60 5   100     82 $self->_events->{$event_type} //= [];
61 5 100       10 return if (scalar(grep { $_ eq $handler } @{$self->_events->{$event_type}}) > 0);
  1         6  
  5         24  
62              
63 4         22 $self->_debug('Attaching handler ' . $handler . ' to event ' . $event_type);
64 4         28 push( @{$self->_events->{$event_type}}, $handler );
  4         21  
65             }
66              
67 1 50   1   7657 method off(Str $event_type, CodeRef $handler?) {
  1 50   1   3  
  1 50   1   213  
  1 50   3   8  
  1 50   3   2  
  1 50       168  
  1 100       11  
  1 50       12  
  1 100       499  
  1         170  
  3         1398  
  3         10  
  3         9  
  3         10  
  3         10  
  3         6  
  3         10  
  3         5  
  3         8  
  3         9  
  2         4  
  2         6  
  2         3  
  3         4  
  3         22350  
  3         9  
  3         591  
  3         26  
  3         8  
  3         528  
  3         24  
  3         7  
  3         1288  
  3         572  
68 3         8 $event_type = lc($event_type);
69 3   100     21 $self->_debug('Requesting detach of handler ' . ($handler or 'n/a') . ' from event ' . $event_type);
70 3 50       28 if ($self->_events->{$event_type}) {
71 3 100       8 if ($handler) {
72 2         5 my $index = 0;
73 2         3 while ($index < scalar(@{$self->_events->{$event_type}})) {
  4         17  
74 2 100       10 if ($self->_events->{$event_type}->[$index] eq $handler) {
75 1         8 $self->_debug('Detaching handler ' . $handler . ' from event ' . $event_type);
76 1         76 splice( @{$self->_events->{$event_type}}, $index, 1 );
  1         9  
77             }
78 2         5 $index++;
79             }
80             } else {
81 1         3 $self->_debug('Detaching ' . scalar(@{$self->_events->{$event_type}}) . ' handler(s) from event ' . $event_type);
  1         9  
82 1         9 delete($self->_events->{$event_type});
83             }
84             }
85             }
86              
87 1 0   1   1111 method connect() {
  1 0   0   10  
  1     3   1039  
  1         169  
  0         0  
  0         0  
  0         0  
  3         3509  
  3         10  
  3         3235  
  3         532  
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             }
126 0         0 );
127             }
128 0         0 });
129              
130             # Event handler
131             $socket->on('each_message', sub {
132 0         0 my ($c, $message) = @_;
133 0         0 $self->_trace('ws in: ' . $message->{'body'});
134 0         0 my $payload;
135             try {
136 0         0 $payload = AnyEvent::Discord::Payload->from_json($message->{'body'});
137             } catch {
138 0         0 $self->_debug($_);
139 0         0 return;
140 0         0 };
141 0 0 0     0 unless ($payload and defined $payload->op) {
142 0         0 $self->_debug('Invalid payload received from Discord: ' . $message->{'body'});
143 0         0 return;
144             }
145 0 0 0     0 $self->_sequence(0 + $payload->s) if ($payload->s and $payload->s > 0);
146              
147 0 0       0 if ($payload->op == 10) {
    0          
148 0         0 $self->_event_hello($payload);
149             } elsif ($payload->d) {
150 0 0       0 if ($payload->d->{'author'}) {
151 0         0 my $user = $payload->d->{'author'};
152 0         0 $self->users->{$user->{'id'}} = $user->{'username'};
153             }
154 0         0 $self->_handle_event($payload);
155             }
156 0         0 });
157              
158 0         0 $self->_discord_identify();
159 0         0 $self->_debug('Completed connection sequence');
160 0         0 $self->_backoff->success();
161 0         0 });
162             }
163              
164 1 0   1   2110 method send($channel_id, $content) {
  1 0   0   4  
  1 0   3   195  
  1 0       171  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6109  
  3         90  
  3         600  
  3         465  
165 0         0 $self->_discord_api('POST', 'channels/' . $channel_id . '/messages', encode_json({content => $content}));
166             }
167              
168 1 0   1   1635 method typing($channel_id) {
  1 0   0   4  
  1 0   3   214  
  1         180  
  0         0  
  0         0  
  0         0  
  0         0  
  3         5027  
  3         6  
  3         637  
  3         552  
169             return AnyEvent->timer(
170             after => 0,
171             interval => 5,
172             cb => sub {
173 0     0   0 $self->_discord_api('POST', 'channels/' . $channel_id . '/typing');
174             },
175 0         0 );
176             }
177              
178 1 0   1   1074 method close() {
  1 0   0   3  
  1     3   156  
  1         155  
  0         0  
  0         0  
  0         0  
  3         3244  
  3         21  
  3         519  
  3         519  
179 0         0 $self->_force_disconnect(1);
180 0         0 $self->{'_heartbeat'} = undef;
181 0         0 $self->{'_sequence'} = 0;
182 0         0 $self->_socket->close();
183             }
184              
185             # Make an HTTP request to the Discord API
186 1 0   1   3912 method _discord_api(Str $method, Str $path, $payload?) {
  1 0   1   4  
  1 0   1   231  
  1 0   0   9  
  1 0   3   4  
  1 0       162  
  1 0       9  
  1 0       4  
  1 0       383  
  1 0       200  
  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         11544  
  3         7  
  3         595  
  3         25  
  3         5  
  3         520  
  3         23  
  3         5  
  3         1090  
  3         468  
187 0         0 my $headers = HTTP::Headers->new(
188             Authorization => 'Bot ' . $self->token,
189             User_Agent => $self->user_agent,
190             Content_Type => 'application/json',
191             );
192 0         0 my $request = HTTP::Request->new(
193             uc($method),
194             join('/', $self->base_uri, $path),
195             $headers,
196             $payload,
197             );
198 0         0 $self->_trace('api req: ' . $request->as_string());
199 0         0 my $res = $self->_ua->request($request);
200 0         0 $self->_trace('api res: ' . $res->as_string());
201 0 0       0 if ($res->is_success()) {
202 0 0       0 if ($res->header('Content-Type') eq 'application/json') {
203 0         0 return decode_json($res->decoded_content());
204             } else {
205 0         0 return $res->decoded_content();
206             }
207             }
208 0         0 return;
209             }
210              
211             # Send the 'identify' event to the Discord websocket
212 1 0   1   1171 method _discord_identify() {
  1 0   0   3  
  1     3   167  
  1         179  
  0         0  
  0         0  
  0         0  
  3         3596  
  3         8  
  3         510  
  3         512  
213 0         0 $self->_debug('Sending identify');
214 0         0 $self->_ws_send_payload(AnyEvent::Discord::Payload->from_hashref({
215             op => 2,
216             d => {
217             token => $self->token,
218             compress => JSON::false,
219             large_threshold => 250,
220             shard => [0, 1],
221             properties => {
222             '$os' => 'linux',
223             '$browser' => $self->user_agent(),
224             '$device' => $self->user_agent(),
225             }
226             }
227             }));
228             }
229              
230             # Send a payload to the Discord websocket
231 1 0   1   8706 method _ws_send_payload(AnyEvent::Discord::Payload $payload) {
  1 0   1   3  
  1 0   0   289  
  1 0   3   9  
  1 0       3  
  1         205  
  1         157  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         22775  
  3         8  
  3         496  
  3         23  
  3         6  
  3         581  
  3         481  
232 0 0       0 unless ($self->_socket) {
233 0         0 $self->_debug('Attempted to send payload to disconnected socket');
234 0         0 return;
235             }
236 0         0 my $msg = $payload->as_json;
237 0         0 $self->_trace('ws out: ' . $msg);
238 0         0 $self->_socket->send($msg);
239             }
240              
241             # Look up the gateway endpoint using the Discord API
242 1 0   1   1554 method _lookup_gateway() {
  1 0   0   5  
  1     3   272  
  1         169  
  0         0  
  0         0  
  0         0  
  3         3567  
  3         6  
  3         732  
  3         488  
243 0         0 my $payload = $self->_discord_api('GET', 'gateway');
244 0 0 0     0 die 'Invalid gateway returned by API' unless ($payload and $payload->{url} and $payload->{url} =~ /^wss/);
      0        
245              
246             # Add the requested version and encoding to the provided URL
247 0         0 my $gateway = $payload->{url};
248 0 0       0 $gateway .= '/' unless ($gateway =~/\/$/);
249 0         0 $gateway .= '?v=6&encoding=json';
250 0         0 return $gateway;
251             }
252              
253             # Dispatch an internal event type
254 1 0   1   2777 method _handle_internal_event(Str $type) {
  1 0   1   3  
  1 0   0   342  
  1 0   3   9  
  1 0       2  
  1         301  
  1         159  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6974  
  3         9  
  3         535  
  3         23  
  3         7  
  3         760  
  3         453  
255 0         0 foreach my $event_source (qw(_internal_events _events)) {
256 0 0       0 if ($self->{$event_source}->{$type}) {
257             map {
258 0 0       0 $self->_debug('Sending ' . ( $event_source =~ /internal/ ? 'internal' : 'caller' ) . ' event ' . $type);
259 0         0 $_->($self);
260 0         0 } @{ $self->{$event_source}->{$type} };
  0         0  
261             }
262             }
263             }
264              
265             # Dispatch a Discord event type
266 1 50   1   3461 method _handle_event(AnyEvent::Discord::Payload $payload) {
  1 50   1   2  
  1 50   4   350  
  1 50   3   9  
  1 50       2  
  1         350  
  1         163  
  4         2865  
  4         12  
  4         10  
  4         12  
  4         8  
  4         19  
  4         8  
  4         6  
  3         7262  
  3         9  
  3         529  
  3         22  
  3         25  
  3         1063  
  3         514  
267 4         14 my $type = lc($payload->t);
268 4         16 $self->_debug('Got event ' . $type);
269 4         33 foreach my $event_source (qw(_internal_events _events)) {
270 8 100       1742 if ($self->{$event_source}->{$type}) {
271             map {
272 4 100       24 $self->_debug('Sending ' . ( $event_source =~ /internal/ ? 'internal' : 'caller' ) . ' event ' . $type);
273 4         93 $_->($self, $payload->d, $payload->op);
274 4         7 } @{ $self->{$event_source}->{$type} };
  4         12  
275             }
276             }
277             }
278              
279             # Send debug messages to console if verbose is >=1
280 1 50   1   2640 method _debug(Str $message) {
  1 50   1   3  
  1 50   22   246  
  1 50   3   9  
  1 50       2  
  1         128  
  1         203  
  22         52  
  22         63  
  22         51  
  22         52  
  22         46  
  22         64  
  22         30  
  22         34  
  3         6210  
  3         5  
  3         550  
  3         23  
  3         8  
  3         343  
  3         540  
281 22 50       442 say $message if ($self->verbose);
282             }
283              
284             # Send trace messages to console if verbose is 2
285 1 0   1   2668 method _trace(Str $message) {
  1 0   1   4  
  1 0   0   214  
  1 0   3   8  
  1 0       3  
  1         146  
  1         169  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6562  
  3         7  
  3         574  
  3         23  
  3         7  
  3         328  
  3         555  
286 0 0       0 say $message if ($self->verbose == 2);
287             }
288              
289             # Called when Discord provides the 'hello' event
290 1 0   1   2982 method _event_hello(AnyEvent::Discord::Payload $payload) {
  1 0   1   2  
  1 0   0   202  
  1 0   3   9  
  1 0       2  
  1         250  
  1         164  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         7181  
  3         18  
  3         566  
  3         25  
  3         9  
  3         672  
  3         661  
291 0         0 $self->_debug('Received hello event');
292 0         0 my $interval = $payload->d->{'heartbeat_interval'}/1e3;
293             $self->_heartbeat(
294             AnyEvent->timer(
295             after => $interval,
296             interval => $interval,
297             cb => sub {
298 0     0   0 $self->_debug('Heartbeat');
299 0         0 $self->_ws_send_payload(AnyEvent::Discord::Payload->from_hashref({
300             op => 1,
301             d => $self->_sequence()
302             }));
303             }
304             )
305 0         0 );
306             }
307              
308             # GUILD_CREATE event
309 1 0 0 1   4071 method _event_guild_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   234  
  1 0   0   8  
  1 0   3   3  
  1 0       203  
  1 0       8  
  1 0       2  
  1 0       345  
  1 0       169  
  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         12038  
  3         6  
  3         650  
  3         23  
  3         6  
  3         612  
  3         28  
  3         8  
  3         1019  
  3         547  
310 0         0 $self->guilds->{$data->{'id'}} = $data->{'name'};
311              
312             # We get channel and user information along with the guild, populate those
313             # at the same time
314 0         0 foreach my $channel (@{$data->{'channels'}}) {
  0         0  
315 0 0       0 if ($channel->{'type'} == 0) {
316 0         0 $self->channels->{$channel->{'id'}} = $channel->{'name'};
317             }
318             }
319 0         0 foreach my $user (@{$data->{'members'}}) {
  0         0  
320 0         0 $self->users->{$user->{'user'}->{'id'}} = $user->{'user'}->{'username'};
321             }
322             }
323              
324             # GUILD_DELETE event
325 1 0 0 1   3940 method _event_guild_delete($client, HashRef $data, Num $opcode?) {
  1 0 0 1   3  
  1 0   1   229  
  1 0   0   8  
  1 0   3   2  
  1 0       170  
  1 0       10  
  1 0       2  
  1 0       132  
  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         11668  
  3         7  
  3         682  
  3         24  
  3         16  
  3         485  
  3         21  
  3         6  
  3         476  
  3         595  
326 0         0 delete($self->guilds->{$data->{'id'}});
327             }
328              
329             # CHANNEL_CREATE event
330 1 0 0 1   3962 method _event_channel_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   233  
  1 0   0   8  
  1 0   3   4  
  1 0       154  
  1 0       8  
  1 0       12  
  1 0       180  
  1 0       190  
  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         11823  
  3         10  
  3         731  
  3         26  
  3         6  
  3         496  
  3         24  
  3         6  
  3         397  
  3         496  
331 0         0 $self->channels->{$data->{'id'}} = $data->{'name'};
332             }
333              
334             # CHANNEL_DELETE event
335 1 0 0 1   3907 method _event_channel_delete($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   274  
  1 0   0   9  
  1 0   3   7  
  1 0       183  
  1 0       7  
  1 0       4  
  1 0       130  
  1 0       166  
  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         11771  
  3         20  
  3         629  
  3         30  
  3         14  
  3         480  
  3         23  
  3         7  
  3         433  
  3         515  
336 0         0 delete($self->channels->{$data->{'id'}});
337             }
338              
339             # GUILD_MEMBER_CREATE event
340 1 0 0 1   4047 method _event_guild_member_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   243  
  1 0   0   9  
  1 0   3   3  
  1 0       181  
  1 0       9  
  1 0       2  
  1 0       142  
  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         12031  
  3         8  
  3         691  
  3         26  
  3         6  
  3         485  
  3         22  
  3         11  
  3         457  
  3         539  
341 0         0 $self->users->{$data->{'id'}} = $data->{'username'};
342             }
343              
344             # GUILD_MEMBER_REMOVE event
345 1 0 0 1   3965 method _event_guild_member_remove($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   252  
  1 0   0   8  
  1 0   3   6  
  1 0       153  
  1 0       7  
  1 0       10  
  1 0       154  
  1 0       171  
  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         11882  
  3         7  
  3         704  
  3         41  
  3         6  
  3         486  
  3         24  
  3         7  
  3         480  
  3         513  
346 0         0 delete($self->users->{$data->{'id'}});
347             }
348             }
349              
350             1;
351              
352             =pod
353              
354             =head1 NAME
355              
356             AnyEvent::Discord - Provides an AnyEvent interface to the Discord bot API
357              
358             =head1 SYNOPSIS
359              
360             use AnyEvent::Discord;
361             my $client = AnyEvent::Discord->new({ token => 'mydiscordbottoken' });
362             $client->on('ready', sub { warn 'Connected'; });
363             $client->on('message_create', sub {
364             my ($client, $data) = @_;
365             warn '[' . $client->channels->{$data->{channel_id}} . ']' .
366             '(' . $data->{author}->{username} . ') - ' .
367             $data->{content};
368             });
369             $client->connect();
370             AnyEvent->condvar->recv;
371              
372             =head1 DESCRIPTION
373              
374             This module provides an AnyEvent interface for the Discord API over the REST
375             and WebSocket APIs. It is designed to be somewhat similar to the SlackRTM and
376             XMPP modules, with a subset of their far more mature functionality.
377              
378             To get started, one needs to create a new application in the Discord Developer
379             Portal (https://discord.com/developers). Once an application is created, a token
380             can be captured by navigating to the "Bot" tab on the left side and selecting
381             'Click to Reveal Token'. That generated token is the same token required by this
382             module.
383              
384             =head1 CONFIGURATION ACCESSORS
385              
386             =over 4
387              
388             =item token (String) (required)
389              
390             The token generated by the Discord Application portal, under Bot.
391              
392             =item base_uri (String) (optional)
393              
394             The base URI for communicating with the Discord API.
395              
396             =item socket_options (HashRef) (optional)
397              
398             Used to override options to sent to AnyEvent::WebSocket::Client, if needed.
399              
400             =item verbose (Num) (defaults to 0)
401              
402             Verbose output, writes internal debug information at 1, additionally writes
403             network conversation at 2.
404              
405             =back
406              
407             =head1 DATA ACCESSORS
408              
409             =over 4
410              
411             =item guilds
412              
413             Available/created/seen guilds, as a hashmap of id => name
414              
415             =item channels
416              
417             Available/created/seen channels, as a hashmap of id => name
418              
419             =item users
420              
421             Available/created/seen users, as a hashmap of id => name
422              
423             =back
424              
425             =head1 PUBLIC METHODS
426              
427             =over 4
428              
429             =item new(\%arguments)
430              
431             Instantiate the AnyEvent::Discord client. The hashref of arguments matches the
432             configuration accessors listed above. A common invocation looks like:
433              
434             my $client = AnyEvent::Discord->new({ token => 'ABCDEF' });
435              
436             =item on($event_type, \&handler)
437              
438             Attach an event handler to a defined event type. If an invalid event type is
439             specified, no error will occur -- this is mostly to be able to handle events
440             that are created after this module is published. This is an append method, so
441             calling on() for an event multiple times will call each callback assigned. If
442             the handler already exists for an event, no error will be returned, but the
443             handler will not be called twice.
444              
445             Discord event types: https://discord.com/developers/docs/topics/gateway#list-of-intents
446              
447             Opcodes: https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-opcodes
448              
449             These events receive the parameters client, data object (d) and the opcode (op).
450              
451             sub event_responder {
452             my ($client, $data, $opcode) = @_;
453             return;
454             }
455              
456             Internal event types:
457              
458             =over 4
459              
460             =item disconnected
461              
462             Receives no parameters, just notifies a disconnection will occur. It will
463             auto reconnect.
464              
465             =item error
466              
467             Receives an error message as a parameter, allows internal handling of errors
468             that are not a hard failure.
469              
470             =back
471              
472             =item off($event_type, \&handler?)
473              
474             Detach an event handler from a defined event type. If the handler does not
475             exist for the event, no error will be returned. If no handler is provided, all
476             handlers for the event type will be removed.
477              
478             =item connect()
479              
480             Start connecting to the Discord API and return immediately. In a new AnyEvent
481             application, this would come before executing "AnyEvent->condvar->recv". This
482             method will retrieve the available gateway endpoint, create a connection,
483             identify itself, begin a heartbeat, and once complete, Discord will fire a
484             'ready' event to the handler.
485              
486             =item send($channel_id, $content)
487              
488             Send a message to the provided channel.
489              
490             =item typing($channel_id)
491              
492             Starts the "typing..." indicator in the provided channel. This method issues the
493             typing request, and starts a timer on the caller's behalf to keep the indicator
494             active. Returns an instance of that timer to allow the caller to undef it when
495             the typing indicator should be stopped.
496              
497             my $instance = $client->typing($channel);
498             # ... perform some actions
499             $instance = undef;
500             # typing indicator disappears
501              
502             =item close()
503              
504             Close the connection to the server.
505              
506             =back
507              
508             =head1 CAVEATS
509              
510             This is incredibly unfinished.
511              
512             =head1 AUTHOR
513              
514             Nick Melnick <nmelnick@cpan.org>
515              
516             =head1 COPYRIGHT AND LICENSE
517              
518             This software is copyright (c) 2021, Nick Melnick.
519              
520             This is free software; you can redistribute it and/or modify it under the same
521             terms as the Perl 5 programming language system itself.
522              
523             =cut