File Coverage

blib/lib/AnyEvent/Discord.pm
Criterion Covered Total %
statement 502 771 65.1
branch 43 314 13.6
condition 4 52 7.6
subroutine 91 117 77.7
pod n/a
total 640 1254 51.0


line stmt bran cond sub pod time code
1 4     4   106325 use v5.14;
  4         31  
2 4     4   2133 use Moops;
  4         207575  
  4         28  
3              
4 4     4   820872 class AnyEvent::Discord {
  4     4   133  
  4         48  
  4         7  
  4         269  
  4         2253  
  4         8771  
  4         18  
  4         7441  
  4         10  
  4         33  
  4         600  
  4         15  
  4         211  
  4         26  
  4         12  
  4         386  
  4         140  
  4         27  
  4         8  
  4         61  
  4         21187  
  4         16  
  4         37  
  4         4177  
  4         15205  
  4         21  
  4         194215  
  4         10  
  4         44  
  4         2808  
  4         38429  
  4         47  
  4         3615  
  4         12682  
  4         33  
  4         7182  
  4         12959  
  4         44  
  4         612624  
  4         18  
  4         25  
  4         11  
  4         103  
  4         22  
  4         9  
  4         183  
  4         24  
  4         10  
  4         563  
  4         18420  
  0         0  
5 4     4   2556 use Algorithm::Backoff::Exponential;
  4         20881  
  4         175  
6 4     4   2008 use AnyEvent::Discord::Payload;
  4         16  
  4         206  
7 1     1   617 use AnyEvent::WebSocket::Client;
  1     3   178044  
  1         69  
  3         1876  
  3         522485  
  3         224  
8 1     1   14 use Data::Dumper;
  1     3   2  
  1         74  
  3         31  
  3         7  
  3         221  
9 1     1   6 use JSON qw(decode_json encode_json);
  1     3   2  
  1         13  
  3         23  
  3         6  
  3         28  
10 1     1   946 use LWP::UserAgent;
  1     3   47425  
  1         50  
  3         2726  
  3         138457  
  3         148  
11 1     1   8 use HTTP::Request;
  1     3   2  
  1         36  
  3         26  
  3         7  
  3         101  
12 1     1   12 use HTTP::Headers;
  1     3   3  
  1         626  
  3         18  
  3         9  
  3         1778  
13              
14 4         57 our $VERSION = '0.4';
15 4         23 has version => ( is => 'ro', isa => Str, default => $VERSION );
16              
17 4         4766 has token => ( is => 'rw', isa => Str, required => 1 );
18 4         7099 has base_uri => ( is => 'rw', isa => Str, default => 'https://discordapp.com/api' );
19 4         5978 has socket_options => ( is => 'rw', isa => HashRef, default => sub { { max_payload_size => 1024 * 1024 } } );
  3         128  
20 4         6173 has verbose => ( is => 'rw', isa => Num, default => 0 );
21 4         6108 has user_agent => ( is => 'rw', isa => Str, default => sub { 'Perl-AnyEventDiscord/' . shift->VERSION } );
  3         227  
22              
23 4         5931 has guilds => ( is => 'ro', isa => HashRef, default => sub { {} } );
  3         128  
24 4         2134 has channels => ( is => 'ro', isa => HashRef, default => sub { {} } );
  3         13232  
25 4         1971 has users => ( is => 'ro', isa => HashRef, default => sub { {} } );
  3         112  
26              
27             # UserAgent
28 4         1957 has _ua => ( is => 'rw', default => sub { LWP::UserAgent->new() } );
  3         177  
29             # Caller-defined event handlers
30 4         1759 has _events => ( is => 'ro', isa => HashRef, default => sub { {} } );
  3         473  
31             # Internal-defined event handlers
32 4         1872 has _internal_events => ( is => 'ro', isa => HashRef, builder => '_build_internal_events' );
33             # WebSocket
34 4         1879 has _socket => ( is => 'rw' );
35             # Heartbeat timer
36 4         1661 has _heartbeat => ( is => 'rw' );
37             # Last Sequence
38 4         1607 has _sequence => ( is => 'rw', isa => Num, default => 0 );
39             # True if caller manually disconnected, to avoid reconnection
40 4         5958 has _force_disconnect => ( is => 'rw', isa => Bool, default => 0 );
41             # Host the backoff algorithm for reconnection
42 4         5951 has _backoff => ( is => 'ro', default => sub { Algorithm::Backoff::Exponential->new( initial_delay => 4 ) } );
  3         29905  
43              
44 1 50   1   1832 method _build_internal_events() {
  1 50   3   4  
  1     3   409  
  4         1770  
  3         181  
  3         29  
  3         7  
  3         5507  
  3         10  
  3         1133  
45             return {
46 0     0   0 'guild_create' => [sub { $self->_event_guild_create(@_); }],
47 0     0   0 'guild_delete' => [sub { $self->_event_guild_delete(@_); }],
48 0     0   0 'channel_create' => [sub { $self->_event_channel_create(@_); }],
49 0     0   0 'channel_delete' => [sub { $self->_event_channel_delete(@_); }],
50 0     0   0 'guild_member_create' => [sub { $self->_event_guild_member_create(@_); }],
51 3     0   108 'guild_member_remove' => [sub { $self->_event_guild_member_remove(@_); }]
  0         0  
52             };
53             }
54              
55 1 50   1   3492 method on(Str $event_type, CodeRef $handler) {
  1 50   1   2  
  1 50   1   264  
  1 50   5   11  
  1 50   3   3  
  1 50       176  
  1 50       7  
  1 50       5  
  1         266  
  4         991  
  5         3265  
  5         20  
  5         16  
  5         17  
  5         7  
  5         18  
  5         11  
  5         14  
  5         12  
  5         10  
  5         14  
  5         9  
  5         7  
  3         10529  
  3         8  
  3         577  
  3         24  
  3         6  
  3         538  
  3         25  
  3         7  
  3         850  
56 5         12 $event_type = lc($event_type);
57 5         30 $self->_debug('Requesting attach of handler ' . $handler . ' to event ' . $event_type);
58              
59 5   100     75 $self->_events->{$event_type} //= [];
60 5 100       11 return if (scalar(grep { $_ eq $handler } @{$self->_events->{$event_type}}) > 0);
  1         6  
  5         22  
61              
62 4         24 $self->_debug('Attaching handler ' . $handler . ' to event ' . $event_type);
63 4         28 push( @{$self->_events->{$event_type}}, $handler );
  4         23  
64             }
65              
66 1 50   1   7323 method off(Str $event_type, CodeRef $handler?) {
  1 50   1   3  
  1 50   1   245  
  1 50   3   8  
  1 50   3   4  
  1 50       170  
  1 100       8  
  1 50       2  
  1 100       452  
  4         687  
  3         1360  
  3         11  
  3         7  
  3         10  
  3         9  
  3         4  
  3         10  
  3         8  
  3         8  
  3         9  
  2         3  
  2         7  
  2         2  
  3         4  
  3         22185  
  3         8  
  3         580  
  3         24  
  3         8  
  3         518  
  3         25  
  3         7  
  3         1292  
67 3         7 $event_type = lc($event_type);
68 3   100     21 $self->_debug('Requesting detach of handler ' . ($handler or 'n/a') . ' from event ' . $event_type);
69 3 50       42 if ($self->_events->{$event_type}) {
70 3 100       10 if ($handler) {
71 2         4 my $index = 0;
72 2         3 while ($index < scalar(@{$self->_events->{$event_type}})) {
  4         19  
73 2 100       13 if ($self->_events->{$event_type}->[$index] eq $handler) {
74 1         8 $self->_debug('Detaching handler ' . $handler . ' from event ' . $event_type);
75 1         80 splice( @{$self->_events->{$event_type}}, $index, 1 );
  1         9  
76             }
77 2         5 $index++;
78             }
79             } else {
80 1         3 $self->_debug('Detaching ' . scalar(@{$self->_events->{$event_type}}) . ' handler(s) from event ' . $event_type);
  1         9  
81 1         11 delete($self->_events->{$event_type});
82             }
83             }
84             }
85              
86 1 0   1   1089 method connect() {
  1 0   0   3  
  1     3   1470  
  4         670  
  0         0  
  0         0  
  0         0  
  3         3399  
  3         53  
  3         3304  
87 0         0 my $gateway = $self->_lookup_gateway();
88              
89 0         0 $self->_debug('Connecting to ' . $gateway);
90              
91 0         0 my $ws = AnyEvent::WebSocket::Client->new($self->socket_options);
92             $ws->connect($gateway)->cb(sub {
93 0     0   0 my $socket = eval { shift->recv };
  0         0  
94 0 0       0 if ($@) {
95 0         0 $self->_debug('Received error connecting: ' . $@);
96 0         0 $self->_handle_internal_event('error', $@);
97 0         0 return;
98             }
99 0         0 $self->_debug('Connected to ' . $gateway);
100              
101 0         0 $self->_socket($socket);
102            
103             # If we send malformed content, bail out
104             $socket->on('parse_error', sub {
105 0         0 my ($c, $error) = @_;
106 0         0 $self->_debug(Data::Dumper::Dumper($error));
107 0         0 die $error;
108 0         0 });
109              
110             # Handle reconnection
111             $socket->on('finish', sub {
112 0         0 my ($c) = @_;
113 0         0 $self->_debug('Received disconnect');
114 0         0 $self->_handle_internal_event('disconnected');
115 0 0       0 unless ($self->_force_disconnect()) {
116 0         0 my $seconds = $self->_backoff->failure();
117 0         0 $self->_debug('Reconnecting in ' . $seconds);
118 0         0 my $reconnect;
119             $reconnect = AnyEvent->timer(
120             after => $seconds,
121             cb => sub {
122 0         0 $self->connect();
123 0         0 $reconnect = undef;
124             }
125 0         0 );
126             }
127 0         0 });
128              
129             # Event handler
130             $socket->on('each_message', sub {
131 0         0 my ($c, $message) = @_;
132 0         0 $self->_trace('ws in: ' . $message->{'body'});
133 0         0 my $payload;
134             try {
135 0         0 $payload = AnyEvent::Discord::Payload->from_json($message->{'body'});
136             } catch {
137 0         0 $self->_debug($_);
138 0         0 return;
139 0         0 };
140 0 0 0     0 unless ($payload and defined $payload->op) {
141 0         0 $self->_debug('Invalid payload received from Discord: ' . $message->{'body'});
142 0         0 return;
143             }
144 0 0 0     0 $self->_sequence(0 + $payload->s) if ($payload->s and $payload->s > 0);
145              
146 0 0       0 if ($payload->op == 10) {
    0          
147 0         0 $self->_event_hello($payload);
148             } elsif ($payload->d) {
149 0 0       0 if ($payload->d->{'author'}) {
150 0         0 my $user = $payload->d->{'author'};
151 0         0 $self->users->{$user->{'id'}} = $user->{'username'};
152             }
153 0         0 $self->_handle_event($payload);
154             }
155 0         0 });
156              
157 0         0 $self->_discord_identify();
158 0         0 $self->_debug('Completed connection sequence');
159 0         0 $self->_backoff->success();
160 0         0 });
161             }
162              
163 1 0   1   2099 method send($channel_id, $content) {
  1 0   0   2  
  1 0   3   193  
  4 0       646  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6078  
  3         90  
  3         594  
164 0         0 $self->_discord_api('POST', 'channels/' . $channel_id . '/messages', encode_json({content => $content}));
165             }
166              
167 1 0   1   1655 method typing($channel_id) {
  1 0   0   3  
  1 0   3   207  
  4         652  
  0         0  
  0         0  
  0         0  
  0         0  
  3         5106  
  3         9  
  3         639  
168             return AnyEvent->timer(
169             after => 0,
170             interval => 5,
171             cb => sub {
172 0     0   0 $self->_discord_api('POST', 'channels/' . $channel_id . '/typing');
173             },
174 0         0 );
175             }
176              
177 1 0   1   1131 method close() {
  1 0   0   4  
  1     3   150  
  4         664  
  0         0  
  0         0  
  0         0  
  3         3260  
  3         6  
  3         487  
178 0         0 $self->_force_disconnect(1);
179 0         0 $self->{'_heartbeat'} = undef;
180 0         0 $self->{'_sequence'} = 0;
181 0         0 $self->_socket->close();
182             }
183              
184             # Make an HTTP request to the Discord API
185 1 0   1   3894 method _discord_api(Str $method, Str $path, $payload?) {
  1 0   1   2  
  1 0   1   225  
  1 0   0   8  
  1 0   3   2  
  1 0       156  
  1 0       10  
  1 0       2  
  1 0       404  
  4 0       650  
  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         12076  
  3         8  
  3         578  
  3         25  
  3         8  
  3         495  
  3         25  
  3         7  
  3         1115  
186 0         0 my $headers = HTTP::Headers->new(
187             Authorization => 'Bot ' . $self->token,
188             User_Agent => $self->user_agent,
189             Content_Type => 'application/json',
190             );
191 0         0 my $request = HTTP::Request->new(
192             uc($method),
193             join('/', $self->base_uri, $path),
194             $headers,
195             $payload,
196             );
197 0         0 $self->_trace('api req: ' . $request->as_string());
198 0         0 my $res = $self->_ua->request($request);
199 0         0 $self->_trace('api res: ' . $res->as_string());
200 0 0       0 if ($res->is_success()) {
201 0 0       0 if ($res->header('Content-Type') eq 'application/json') {
202 0         0 return decode_json($res->decoded_content());
203             } else {
204 0         0 return $res->decoded_content();
205             }
206             }
207 0         0 return;
208             }
209              
210             # Send the 'identify' event to the Discord websocket
211 1 0   1   1144 method _discord_identify() {
  1 0   0   3  
  1     3   166  
  4         673  
  0         0  
  0         0  
  0         0  
  3         3652  
  3         7  
  3         543  
212 0         0 $self->_debug('Sending identify');
213 0         0 $self->_ws_send_payload(AnyEvent::Discord::Payload->from_hashref({
214             op => 2,
215             d => {
216             token => $self->token,
217             compress => JSON::false,
218             large_threshold => 250,
219             shard => [0, 1],
220             properties => {
221             '$os' => 'linux',
222             '$browser' => $self->user_agent(),
223             '$device' => $self->user_agent(),
224             }
225             }
226             }));
227             }
228              
229             # Send a payload to the Discord websocket
230 1 0   1   7508 method _ws_send_payload(AnyEvent::Discord::Payload $payload) {
  1 0   1   3  
  1 0   0   195  
  1 0   3   8  
  1 0       1  
  1         203  
  4         611  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         23238  
  3         9  
  3         517  
  3         24  
  3         7  
  3         584  
231 0 0       0 unless ($self->_socket) {
232 0         0 $self->_debug('Attempted to send payload to disconnected socket');
233 0         0 return;
234             }
235 0         0 my $msg = $payload->as_json;
236 0         0 $self->_trace('ws out: ' . $msg);
237 0         0 $self->_socket->send($msg);
238             }
239              
240             # Look up the gateway endpoint using the Discord API
241 1 0   1   1186 method _lookup_gateway() {
  1 0   0   4  
  1     3   284  
  4         627  
  0         0  
  0         0  
  0         0  
  3         3562  
  3         7  
  3         770  
242 0         0 my $payload = $self->_discord_api('GET', 'gateway');
243 0 0 0     0 die 'Invalid gateway returned by API' unless ($payload and $payload->{url} and $payload->{url} =~ /^wss/);
      0        
244              
245             # Add the requested version and encoding to the provided URL
246 0         0 my $gateway = $payload->{url};
247 0 0       0 $gateway .= '/' unless ($gateway =~/\/$/);
248 0         0 $gateway .= '?v=6&encoding=json';
249 0         0 return $gateway;
250             }
251              
252             # Dispatch an internal event type
253 1 0   1   2228 method _handle_internal_event(Str $type) {
  1 0   1   2  
  1 0   0   225  
  1 0   3   8  
  1 0       2  
  1         260  
  4         638  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         7111  
  3         10  
  3         573  
  3         34  
  3         6  
  3         827  
254 0         0 foreach my $event_source (qw(_internal_events _events)) {
255 0 0       0 if ($self->{$event_source}->{$type}) {
256             map {
257 0 0       0 $self->_debug('Sending ' . ( $event_source =~ /internal/ ? 'internal' : 'caller' ) . ' event ' . $type);
258 0         0 $_->($self);
259 0         0 } @{ $self->{$event_source}->{$type} };
  0         0  
260             }
261             }
262             }
263              
264             # Dispatch a Discord event type
265 1 50   1   2428 method _handle_event(AnyEvent::Discord::Payload $payload) {
  1 50   1   2  
  1 50   4   186  
  1 50   3   7  
  1 50       2  
  1         337  
  4         623  
  4         2865  
  4         13  
  4         15  
  4         11  
  4         6  
  4         18  
  4         10  
  4         5  
  3         7340  
  3         7  
  3         604  
  3         25  
  3         6  
  3         1014  
266 4         15 my $type = lc($payload->t);
267 4         15 $self->_debug('Got event ' . $type);
268 4         58 foreach my $event_source (qw(_internal_events _events)) {
269 8 100       1828 if ($self->{$event_source}->{$type}) {
270             map {
271 4 100       27 $self->_debug('Sending ' . ( $event_source =~ /internal/ ? 'internal' : 'caller' ) . ' event ' . $type);
272 4         100 $_->($self, $payload->d, $payload->op);
273 4         8 } @{ $self->{$event_source}->{$type} };
  4         13  
274             }
275             }
276             }
277              
278             # Send debug messages to console if verbose is >=1
279 1 50   1   2077 method _debug(Str $message) {
  1 50   1   2  
  1 50   22   275  
  1 50   3   7  
  1 50       3  
  1         117  
  4         812  
  22         52  
  22         64  
  22         51  
  22         55  
  22         30  
  22         66  
  22         33  
  22         30  
  3         6236  
  3         7  
  3         537  
  3         25  
  3         6  
  3         342  
280 22 50       742 say $message if ($self->verbose);
281             }
282              
283             # Send trace messages to console if verbose is 2
284 1 0   1   2038 method _trace(Str $message) {
  1 0   1   3  
  1 0   0   187  
  1 0   3   7  
  1 0       2  
  1         126  
  4         698  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         6457  
  3         8  
  3         613  
  3         23  
  3         8  
  3         345  
285 0 0       0 say $message if ($self->verbose == 2);
286             }
287              
288             # Called when Discord provides the 'hello' event
289 1 0   1   2410 method _event_hello(AnyEvent::Discord::Payload $payload) {
  1 0   1   3  
  1 0   0   181  
  1 0   3   8  
  1 0       3  
  1         240  
  4         696  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  3         7234  
  3         7  
  3         664  
  3         26  
  3         8  
  3         683  
290 0         0 $self->_debug('Received hello event');
291 0         0 my $interval = $payload->d->{'heartbeat_interval'}/1e3;
292             $self->_heartbeat(
293             AnyEvent->timer(
294             after => $interval,
295             interval => $interval,
296             cb => sub {
297 0     0   0 $self->_debug('Heartbeat');
298 0         0 $self->_ws_send_payload(AnyEvent::Discord::Payload->from_hashref({
299             op => 1,
300             d => $self->_sequence()
301             }));
302             }
303             )
304 0         0 );
305             }
306              
307             # GUILD_CREATE event
308 1 0 0 1   3948 method _event_guild_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   238  
  1 0   0   8  
  1 0   3   2  
  1 0       173  
  1 0       8  
  1 0       3  
  1 0       320  
  4 0       669  
  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         12571  
  3         11  
  3         748  
  3         29  
  3         7  
  3         496  
  3         22  
  3         8  
  3         1029  
309 0         0 $self->guilds->{$data->{'id'}} = $data->{'name'};
310              
311             # We get channel and user information along with the guild, populate those
312             # at the same time
313 0         0 foreach my $channel (@{$data->{'channels'}}) {
  0         0  
314 0 0       0 if ($channel->{'type'} == 0) {
315 0         0 $self->channels->{$channel->{'id'}} = $channel->{'name'};
316             }
317             }
318 0         0 foreach my $user (@{$data->{'members'}}) {
  0         0  
319 0         0 $self->users->{$user->{'user'}->{'id'}} = $user->{'user'}->{'username'};
320             }
321             }
322              
323             # GUILD_DELETE event
324 1 0 0 1   3919 method _event_guild_delete($client, HashRef $data, Num $opcode?) {
  1 0 0 1   3  
  1 0   1   232  
  1 0   0   7  
  1 0   3   3  
  1 0       151  
  1 0       8  
  1 0       2  
  1 0       138  
  4 0       780  
  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         11720  
  3         8  
  3         692  
  3         26  
  3         5  
  3         540  
  3         24  
  3         9  
  3         420  
325 0         0 delete($self->guilds->{$data->{'id'}});
326             }
327              
328             # CHANNEL_CREATE event
329 1 0 0 1   3976 method _event_channel_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   3  
  1 0   1   233  
  1 0   0   13  
  1 0   3   2  
  1 0       154  
  1 0       7  
  1 0       3  
  1 0       148  
  4 0       686  
  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         11897  
  3         8  
  3         711  
  3         25  
  3         12  
  3         489  
  3         24  
  3         9  
  3         364  
330 0         0 $self->channels->{$data->{'id'}} = $data->{'name'};
331             }
332              
333             # CHANNEL_DELETE event
334 1 0 0 1   3885 method _event_channel_delete($client, HashRef $data, Num $opcode?) {
  1 0 0 1   3  
  1 0   1   247  
  1 0   0   8  
  1 0   3   2  
  1 0       160  
  1 0       33  
  1 0       3  
  1 0       142  
  4 0       646  
  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         12411  
  3         17  
  3         652  
  3         25  
  3         11  
  3         497  
  3         25  
  3         7  
  3         372  
335 0         0 delete($self->channels->{$data->{'id'}});
336             }
337              
338             # GUILD_MEMBER_CREATE event
339 1 0 0 1   3969 method _event_guild_member_create($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   233  
  1 0   0   39  
  1 0   3   2  
  1 0       157  
  1 0       8  
  1 0       2  
  1 0       140  
  4 0       664  
  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         12030  
  3         8  
  3         672  
  3         35  
  3         7  
  3         521  
  3         23  
  3         6  
  3         421  
340 0         0 $self->users->{$data->{'id'}} = $data->{'username'};
341             }
342              
343             # GUILD_MEMBER_REMOVE event
344 1 0 0 1   3946 method _event_guild_member_remove($client, HashRef $data, Num $opcode?) {
  1 0 0 1   2  
  1 0   1   256  
  1 0   0   7  
  1 0   3   3  
  1 0       153  
  1 0       7  
  1 0       2  
  1 0       150  
  4 0       682  
  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         11959  
  3         9  
  3         694  
  3         68  
  3         14  
  3         496  
  3         23  
  3         23  
  3         479  
345 0         0 delete($self->users->{$data->{'id'}});
346             }
347             }
348              
349             1;
350              
351             =pod
352              
353             =head1 NAME
354              
355             AnyEvent::Discord - Provides an AnyEvent interface to the Discord bot API
356              
357             =head1 SYNOPSIS
358              
359             use AnyEvent::Discord;
360             my $client = AnyEvent::Discord->new({ token => 'mydiscordbottoken' });
361             $client->on('ready', sub { warn 'Connected'; });
362             $client->on('message_create', sub {
363             my ($client, $data) = @_;
364             warn '[' . $client->channels->{$data->{channel_id}} . ']' .
365             '(' . $data->{author}->{username} . ') - ' .
366             $data->{content};
367             });
368             $client->connect();
369             AnyEvent->condvar->recv;
370              
371             =head1 DESCRIPTION
372              
373             This module provides an AnyEvent interface for the Discord API over the REST
374             and WebSocket APIs. It is designed to be somewhat similar to the SlackRTM and
375             XMPP modules, with a subset of their far more mature functionality.
376              
377             To get started, one needs to create a new application in the Discord Developer
378             Portal (https://discord.com/developers). Once an application is created, a token
379             can be captured by navigating to the "Bot" tab on the left side and selecting
380             'Click to Reveal Token'. That generated token is the same token required by this
381             module.
382              
383             =head1 CONFIGURATION ACCESSORS
384              
385             =over 4
386              
387             =item token (String) (required)
388              
389             The token generated by the Discord Application portal, under Bot.
390              
391             =item base_uri (String) (optional)
392              
393             The base URI for communicating with the Discord API.
394              
395             =item socket_options (HashRef) (optional)
396              
397             Used to override options to sent to AnyEvent::WebSocket::Client, if needed.
398              
399             =item verbose (Num) (defaults to 0)
400              
401             Verbose output, writes internal debug information at 1, additionally writes
402             network conversation at 2.
403              
404             =back
405              
406             =head1 DATA ACCESSORS
407              
408             =over 4
409              
410             =item guilds
411              
412             Available/created/seen guilds, as a hashmap of id => name
413              
414             =item channels
415              
416             Available/created/seen channels, as a hashmap of id => name
417              
418             =item users
419              
420             Available/created/seen users, as a hashmap of id => name
421              
422             =back
423              
424             =head1 PUBLIC METHODS
425              
426             =over 4
427              
428             =item new(\%arguments)
429              
430             Instantiate the AnyEvent::Discord client. The hashref of arguments matches the
431             configuration accessors listed above. A common invocation looks like:
432              
433             my $client = AnyEvent::Discord->new({ token => 'ABCDEF' });
434              
435             =item on($event_type, \&handler)
436              
437             Attach an event handler to a defined event type. If an invalid event type is
438             specified, no error will occur -- this is mostly to be able to handle events
439             that are created after this module is published. This is an append method, so
440             calling on() for an event multiple times will call each callback assigned. If
441             the handler already exists for an event, no error will be returned, but the
442             handler will not be called twice.
443              
444             Discord event types: https://discord.com/developers/docs/topics/gateway#list-of-intents
445              
446             Opcodes: https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-opcodes
447              
448             These events receive the parameters client, data object (d) and the opcode (op).
449              
450             sub event_responder {
451             my ($client, $data, $opcode) = @_;
452             return;
453             }
454              
455             Internal event types:
456              
457             =over 4
458              
459             =item disconnected
460              
461             Receives no parameters, just notifies a disconnection will occur. It will
462             auto reconnect.
463              
464             =item error
465              
466             Receives an error message as a parameter, allows internal handling of errors
467             that are not a hard failure.
468              
469             =back
470              
471             =item off($event_type, \&handler?)
472              
473             Detach an event handler from a defined event type. If the handler does not
474             exist for the event, no error will be returned. If no handler is provided, all
475             handlers for the event type will be removed.
476              
477             =item connect()
478              
479             Start connecting to the Discord API and return immediately. In a new AnyEvent
480             application, this would come before executing "AnyEvent->condvar->recv". This
481             method will retrieve the available gateway endpoint, create a connection,
482             identify itself, begin a heartbeat, and once complete, Discord will fire a
483             'ready' event to the handler.
484              
485             =item send($channel_id, $content)
486              
487             Send a message to the provided channel.
488              
489             =item typing($channel_id)
490              
491             Starts the "typing..." indicator in the provided channel. This method issues the
492             typing request, and starts a timer on the caller's behalf to keep the indicator
493             active. Returns an instance of that timer to allow the caller to undef it when
494             the typing indicator should be stopped.
495              
496             my $instance = $client->typing($channel);
497             # ... perform some actions
498             $instance = undef;
499             # typing indicator disappears
500              
501             =item close()
502              
503             Close the connection to the server.
504              
505             =back
506              
507             =head1 CAVEATS
508              
509             This is incredibly unfinished.
510              
511             =head1 AUTHOR
512              
513             Nick Melnick <nmelnick@cpan.org>
514              
515             =head1 COPYRIGHT AND LICENSE
516              
517             This software is copyright (c) 2021, Nick Melnick.
518              
519             This is free software; you can redistribute it and/or modify it under the same
520             terms as the Perl 5 programming language system itself.
521              
522             =cut