AnyEvent-Discord
view release on metacpan or search on metacpan
lib/AnyEvent/Discord.pm view on Meta::CPAN
package AnyEvent::Discord;
use v5.14;
use Moops;
class AnyEvent::Discord 0.7 {
use Algorithm::Backoff::Exponential;
use AnyEvent::Discord::Payload;
use AnyEvent::WebSocket::Client;
use Data::Dumper;
use JSON qw(decode_json encode_json);
use LWP::UserAgent;
use HTTP::Request;
use HTTP::Headers;
our $VERSION = '0.7';
has version => ( is => 'ro', isa => Str, default => $VERSION );
has token => ( is => 'rw', isa => Str, required => 1 );
has base_uri => ( is => 'rw', isa => Str, default => 'https://discordapp.com/api' );
has socket_options => ( is => 'rw', isa => HashRef, default => sub { { max_payload_size => 1024 * 1024 } } );
has verbose => ( is => 'rw', isa => Num, default => 0 );
has user_agent => ( is => 'rw', isa => Str, default => sub { 'Perl-AnyEventDiscord/' . shift->VERSION } );
has guilds => ( is => 'ro', isa => HashRef, default => sub { {} } );
has channels => ( is => 'ro', isa => HashRef, default => sub { {} } );
has users => ( is => 'ro', isa => HashRef, default => sub { {} } );
# UserAgent
has _ua => ( is => 'rw', default => sub { LWP::UserAgent->new() } );
# Caller-defined event handlers
has _events => ( is => 'ro', isa => HashRef, default => sub { {} } );
# Internal-defined event handlers
has _internal_events => ( is => 'ro', isa => HashRef, builder => '_build_internal_events' );
# WebSocket
has _socket => ( is => 'rw' );
# Heartbeat timer
has _heartbeat => ( is => 'rw' );
# Last Sequence
has _sequence => ( is => 'rw', isa => Num, default => 0 );
# True if caller manually disconnected, to avoid reconnection
has _force_disconnect => ( is => 'rw', isa => Bool, default => 0 );
# Host the backoff algorithm for reconnection
has _backoff => ( is => 'ro', default => sub { Algorithm::Backoff::Exponential->new( initial_delay => 1 ) } );
method _build_internal_events() {
return {
'guild_create' => [sub { $self->_event_guild_create(@_); }],
'guild_delete' => [sub { $self->_event_guild_delete(@_); }],
'channel_create' => [sub { $self->_event_channel_create(@_); }],
'channel_delete' => [sub { $self->_event_channel_delete(@_); }],
'guild_member_create' => [sub { $self->_event_guild_member_create(@_); }],
'guild_member_remove' => [sub { $self->_event_guild_member_remove(@_); }]
};
}
method on(Str $event_type, CodeRef $handler) {
$event_type = lc($event_type);
$self->_debug('Requesting attach of handler ' . $handler . ' to event ' . $event_type);
$self->_events->{$event_type} //= [];
return if (scalar(grep { $_ eq $handler } @{$self->_events->{$event_type}}) > 0);
$self->_debug('Attaching handler ' . $handler . ' to event ' . $event_type);
push( @{$self->_events->{$event_type}}, $handler );
}
method off(Str $event_type, CodeRef $handler?) {
$event_type = lc($event_type);
$self->_debug('Requesting detach of handler ' . ($handler or 'n/a') . ' from event ' . $event_type);
if ($self->_events->{$event_type}) {
if ($handler) {
my $index = 0;
while ($index < scalar(@{$self->_events->{$event_type}})) {
if ($self->_events->{$event_type}->[$index] eq $handler) {
$self->_debug('Detaching handler ' . $handler . ' from event ' . $event_type);
splice( @{$self->_events->{$event_type}}, $index, 1 );
}
$index++;
}
} else {
$self->_debug('Detaching ' . scalar(@{$self->_events->{$event_type}}) . ' handler(s) from event ' . $event_type);
delete($self->_events->{$event_type});
}
}
}
method connect() {
my $gateway = $self->_lookup_gateway();
$self->_debug('Connecting to ' . $gateway);
my $ws = AnyEvent::WebSocket::Client->new($self->socket_options);
$ws->connect($gateway)->cb(sub {
my $socket = eval { shift->recv };
if ($@) {
$self->_debug('Received error connecting: ' . $@);
$self->_handle_internal_event('error', $@);
return;
}
$self->_debug('Connected to ' . $gateway);
$self->_socket($socket);
# If we send malformed content, bail out
$socket->on('parse_error', sub {
my ($c, $error) = @_;
$self->_debug(Data::Dumper::Dumper($error));
die $error;
});
# Handle reconnection
$socket->on('finish', sub {
my ($c) = @_;
$self->_debug('Received disconnect');
$self->_handle_internal_event('disconnected');
unless ($self->_force_disconnect()) {
my $seconds = $self->_backoff->failure();
$self->_debug('Reconnecting in ' . $seconds);
my $reconnect;
$reconnect = AnyEvent->timer(
after => $seconds,
cb => sub {
$self->connect();
$reconnect = undef;
AnyEvent->condvar->send();
}
);
}
});
# Event handler
$socket->on('each_message', sub {
my ($c, $message) = @_;
$self->_trace('ws in: ' . $message->{'body'});
my $payload;
try {
$payload = AnyEvent::Discord::Payload->from_json($message->{'body'});
} catch {
$self->_debug($_);
return;
};
unless ($payload and defined $payload->op) {
$self->_debug('Invalid payload received from Discord: ' . $message->{'body'});
return;
}
$self->_sequence(0 + $payload->s) if ($payload->s and $payload->s > 0);
if ($payload->op == 10) {
$self->_event_hello($payload);
} elsif ($payload->d) {
if ($payload->d->{'author'}) {
my $user = $payload->d->{'author'};
$self->users->{$user->{'id'}} = $user->{'username'};
}
$self->_handle_event($payload);
}
});
$self->_discord_identify();
$self->_debug('Completed connection sequence');
$self->_backoff->success();
AnyEvent->condvar->send();
});
}
method send($channel_id, $content) {
return $self->_discord_api('POST', 'channels/' . $channel_id . '/messages', encode_json({content => $content}));
( run in 1.432 second using v1.01-cache-2.11-cpan-39bf76dae61 )