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;

lib/AnyEvent/Discord.pm  view on Meta::CPAN

        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}));
  }

  method typing($channel_id) {
    return AnyEvent->timer(
      after    => 0,
      interval => 5,
      cb       => sub {
        $self->_discord_api('POST', 'channels/' . $channel_id . '/typing');
        AnyEvent->condvar->send();
      },
    );
  }

  method close() {
    $self->_force_disconnect(1);
    $self->{'_heartbeat'} = undef;
    $self->{'_sequence'} = undef;
    $self->_socket->close();
  }

  # Make an HTTP request to the Discord API
  method _discord_api(Str $method, Str $path, $payload?) {
    my $headers = HTTP::Headers->new(
      Authorization => 'Bot ' . $self->token,
      User_Agent    => $self->user_agent,
      Content_Type  => 'application/json',
    );
    my $request = HTTP::Request->new(
      uc($method),
      join('/', $self->base_uri, $path),
      $headers,
      $payload,
    );
    $self->_trace('api req: ' . $request->as_string());
    my $res = $self->_ua->request($request);
    $self->_trace('api res: ' . $res->as_string());
    if ($res->is_success()) {
      if ($res->header('Content-Type') eq 'application/json') {
        return decode_json($res->decoded_content());
      } else {
        return $res->decoded_content();
      }
    }
    return;
  }

  # Send the 'identify' event to the Discord websocket
  method _discord_identify() {
    $self->_debug('Sending identify');
    $self->_ws_send_payload(AnyEvent::Discord::Payload->from_hashref({
      op => 2,
      d  => {
        token           => $self->token,
        compress        => JSON::false,
        large_threshold => 250,
        shard           => [0, 1],
        properties => {
          '$os'      => 'linux',
          '$browser' => $self->user_agent(),
          '$device'  => $self->user_agent(),
        }
      }
    }));
  }

  # Send a payload to the Discord websocket
  method _ws_send_payload(AnyEvent::Discord::Payload $payload) {
    unless ($self->_socket) {
      $self->_debug('Attempted to send payload to disconnected socket');
      return;
    }
    my $msg = $payload->as_json;
    $self->_trace('ws out: ' . $msg);
    $self->_socket->send($msg);
  }

  # Look up the gateway endpoint using the Discord API
  method _lookup_gateway() {
    my $payload = $self->_discord_api('GET', 'gateway');
    die 'Invalid gateway returned by API' unless ($payload and $payload->{url} and $payload->{url} =~ /^wss/);

    # Add the requested version and encoding to the provided URL
    my $gateway = $payload->{url};
    $gateway .= '/' unless ($gateway =~/\/$/);
    $gateway .= '?v=6&encoding=json';
    return $gateway;
  }



( run in 1.888 second using v1.01-cache-2.11-cpan-140bd7fdf52 )