class MijDiscord::Bot

Constants

EVENTS
UNAVAILABLE_SERVER_TIMEOUT

Attributes

auth[R]
cache[R]
gateway[R]
profile[R]
shard_key[R]

Public Class Methods

new(client_id:, token:, type: :bot, name: nil, shard_id: nil, num_shards: nil, ignore_bots: false, ignore_self: true) click to toggle source
# File lib/mij-discord/bot.rb, line 97
def initialize(client_id:, token:, type: :bot, name: nil,
shard_id: nil, num_shards: nil, ignore_bots: false, ignore_self: true)
  @auth = AuthInfo.new(client_id, token, type, name)

  @cache = MijDiscord::Cache::BotCache.new(self)

  @shard_key = [shard_id, num_shards] if num_shards
  @gateway = MijDiscord::Core::Gateway.new(self, @auth, @shard_key)

  @ignore_bots, @ignore_self, @ignored_ids = ignore_bots, ignore_self, Set.new

  @unavailable_servers = 0

  @event_dispatchers = {}
end

Public Instance Methods

accept_invite(invite) click to toggle source
# File lib/mij-discord/bot.rb, line 215
def accept_invite(invite)
  code = parse_invite_code(invite)
  MijDiscord::Core::API::Invite.accept(@auth, code)
  nil
end
add_event(type, key = nil, **filter, &block) click to toggle source
# File lib/mij-discord/bot.rb, line 312
def add_event(type, key = nil, **filter, &block)
  raise ArgumentError, "Invalid event type: #{type}" unless EVENTS[type]

  event = (@event_dispatchers[type] ||= MijDiscord::Events::EventDispatcher.new(EVENTS[type], self))
  event.add_callback(key, filter, &block)
end
application() click to toggle source
# File lib/mij-discord/bot.rb, line 202
def application
  raise 'Cannot get OAuth application for non-bot user' unless @auth.bot?

  response = MijDiscord::Core::API.oauth_application(@auth)
  MijDiscord::Data::Application.new(JSON.parse(response), self)
end
channel(id, server = nil) click to toggle source
# File lib/mij-discord/bot.rb, line 150
def channel(id, server = nil)
  gateway_check
  @cache.get_channel(id, server)
end
channels() click to toggle source
# File lib/mij-discord/bot.rb, line 145
def channels
  gateway_check
  @cache.list_channels
end
connect(async = true) click to toggle source
# File lib/mij-discord/bot.rb, line 113
def connect(async = true)
  @gateway.run_async
  @gateway.sync unless async
  nil
end
connected?() click to toggle source
# File lib/mij-discord/bot.rb, line 131
def connected?
  @gateway.open?
end
create_server(name, region = 'eu-central') click to toggle source
# File lib/mij-discord/bot.rb, line 228
def create_server(name, region = 'eu-central')
  response = API::Server.create(@auth, name, region)
  id = JSON.parse(response)['id'].to_i

  loop do
    server = @cache.get_server(id, local: true)
    return server if server

    sleep(0.1)
  end
end
disconnect(no_sync = false) click to toggle source
# File lib/mij-discord/bot.rb, line 124
def disconnect(no_sync = false)
  @gateway.stop(no_sync)
  nil
end
Also aliased as: shutdown
dm_channel(id)
Alias for: pm_channel
emoji(server_id, id) click to toggle source
# File lib/mij-discord/bot.rb, line 197
def emoji(server_id, id)
  gateway_check
  server(server_id)&.emoji(id)
end
emojis(server_id) click to toggle source
# File lib/mij-discord/bot.rb, line 192
def emojis(server_id)
  gateway_check
  server(server_id)&.emojis
end
events(type) click to toggle source
# File lib/mij-discord/bot.rb, line 326
def events(type)
  raise ArgumentError, "Invalid event type: #{type}" unless EVENTS[type]

  @event_dispatchers[type]&.callbacks || []
end
handle_dispatch(type, data) click to toggle source
# File lib/mij-discord/bot.rb, line 388
def handle_dispatch(type, data)
  MijDiscord::LOGGER.debug('Dispatch') { "<#{type} #{data.inspect}>" }

  if @unavailable_servers > 0 && Time.now > @unavailable_servers_timeout
    MijDiscord::LOGGER.warn('Dispatch') { "Proceeding with #{@unavailable_servers} servers still unavailable" }

    @unavailable_servers = 0
    notify_ready
  end

  case type
    when :CONNECT
      trigger_event(:connect, self)

    when :DISCONNECT
      trigger_event(:disconnect, self)

    when :READY
      @cache.reset

      @profile = MijDiscord::Data::Profile.new(data['user'], self)
      @profile.update_presence('status' => :online)

      @unavailable_servers = 0
      @unavailable_servers_timeout = Time.now + UNAVAILABLE_SERVER_TIMEOUT

      data['guilds'].each do |sv|
        if sv['unavailable'].eql?(true)
          @unavailable_servers += 1
        else
          @cache.put_server(sv)
        end
      end

      data['private_channels'].each do |ch|
        @cache.put_channel(ch, nil)
      end

      notify_ready if @unavailable_servers.zero?

    when :SESSIONS_REPLACE
      # Do nothing with session replace because no idea what it does.

    when :PRESENCES_REPLACE
      # Do nothing with presences replace because no idea what it does.

    when :GUILD_MEMBERS_CHUNK
      server = @cache.get_server(data['guild_id'])
      server.update_members_chunk(data['members'])

    when :GUILD_CREATE
      server = @cache.put_server(data)

      if data['unavailable'].eql?(false)
        @unavailable_servers -= 1
        @unavailable_servers_timeout = Time.now + UNAVAILABLE_SERVER_TIMEOUT

        notify_ready if @unavailable_servers.zero?
        return
      end

      trigger_event(:create_server, self, server)

    when :GUILD_SYNC
      server = @cache.get_server(data['id'])
      server.update_synced_data(data)

    when :GUILD_UPDATE
      server = @cache.put_server(data, update: true)
      trigger_event(:update_server, self, server)

    when :GUILD_DELETE
      server = @cache.remove_server(data['id'])

      if data['unavailable'].eql?(true)
        MijDiscord::LOGGER.warn('Dispatch') { "Server <#{data['id']}> died due to outage" }
        return
      end

      trigger_event(:delete_server, self, server)

    when :CHANNEL_CREATE
      channel = @cache.put_channel(data, nil)
      trigger_event(:create_channel, self, channel)

    when :CHANNEL_UPDATE
      channel = @cache.put_channel(data, nil, update: true)
      trigger_event(:update_channel, self, channel)

    when :CHANNEL_DELETE
      channel = @cache.remove_channel(data['id'])
      trigger_event(:delete_channel, self, channel)

    when :WEBHOOKS_UPDATE
      channel = @cache.get_channel(data['channel_id'], nil)
      trigger_event(:update_webhooks, self, channel)

    when :CHANNEL_PINS_UPDATE
      channel = @cache.get_channel(data['channel_id'], nil)
      trigger_event(:update_pins, self, channel)

    when :CHANNEL_PINS_ACK
      # Do nothing with pins acknowledgement

    when :CHANNEL_RECIPIENT_ADD
      channel = @cache.get_channel(data['channel_id'], nil)
      recipient = channel.update_recipient(add: data['user'])
      trigger_event(:add_recipient, self, channel, recipient)

    when :CHANNEL_RECIPIENT_REMOVE
      channel = @cache.get_channel(data['channel_id'], nil)
      recipient = channel.update_recipient(remove: data['user'])
      trigger_event(:remove_recipient, self, channel, recipient)

    when :GUILD_MEMBER_ADD
      server = @cache.get_server(data['guild_id'])
      member = server.update_member(data, :add)
      trigger_event(:create_member, self, member, server)

    when :GUILD_MEMBER_UPDATE
      server = @cache.get_server(data['guild_id'])
      member = server.update_member(data, :update)
      trigger_event(:update_member, self, member, server)

    when :GUILD_MEMBER_REMOVE
      server = @cache.get_server(data['guild_id'])
      member = server.update_member(data, :remove)
      trigger_event(:delete_member, self, member, server)

    when :GUILD_ROLE_CREATE
      server = @cache.get_server(data['guild_id'])
      role = server.cache.put_role(data['role'])
      trigger_event(:create_role, self, server, role)

    when :GUILD_ROLE_UPDATE
      server = @cache.get_server(data['guild_id'])
      role = server.cache.put_role(data['role'], update: true)
      trigger_event(:update_role, self, server, role)

    when :GUILD_ROLE_DELETE
      server = @cache.get_server(data['guild_id'])
      role = server.cache.remove_role(data['role_id'])
      trigger_event(:delete_role, self, server, role)

    when :GUILD_EMOJIS_UPDATE
      server = @cache.get_server(data['guild_id'])
      old_emojis = server.emojis
      server.update_emojis(data)
      trigger_event(:update_emoji, self, server, old_emojis, server.emojis)

    when :GUILD_BAN_ADD
      server = @cache.get_server(data['guild_id'])
      user = @cache.get_user(data['user']['id'], local: @auth.user?)
      user ||= MijDiscord::Data::User.new(data['user'], self)
      trigger_event(:ban_user, self, server, user)

    when :GUILD_BAN_REMOVE
      server = @cache.get_server(data['guild_id'])
      user = @cache.get_user(data['user']['id'], local: @auth.user?)
      user ||= MijDiscord::Data::User.new(data['user'], self)
      trigger_event(:unban_user, self, server, user)

    when :MESSAGE_CREATE
      channel = @cache.get_channel(data['channel_id'], nil)
      message = channel.cache.put_message(data)

      return if ignored_user?(data['author']['id'])
      trigger_event(:create_message, self, message)

      if message.channel.private?
        trigger_event(:private_message, self, message)
      else
        trigger_event(:channel_message, self, message)
      end

    when :MESSAGE_ACK
      # Do nothing with message acknowledgement

    when :MESSAGE_UPDATE
      author = data['author']
      return if author.nil?

      channel = @cache.get_channel(data['channel_id'], nil)
      message = channel.cache.put_message(data, update: true)

      return if ignored_user?(author['id'])
      trigger_event(:edit_message, self, message)

    when :MESSAGE_DELETE
      channel = @cache.get_channel(data['channel_id'], nil)
      channel.cache.remove_message(data['id'])

      trigger_event(:delete_message, self, data)

    when :MESSAGE_DELETE_BULK
      messages = data['ids'].map {|x| {'id' => x, 'channel_id' => data['channel_id']} }
      messages.each {|x| trigger_event(:delete_message, self, x) }

    when :MESSAGE_REACTION_ADD
      channel = @cache.get_channel(data['channel_id'], nil)
      message = channel.cache.get_message(data['message_id'], local: true)
      message.update_reaction(add: data) if message

      return if ignored_user?(data['user_id'])
      trigger_event(:add_reaction, self, data)
      trigger_event(:toggle_reaction, self, data)

    when :MESSAGE_REACTION_REMOVE
      channel = @cache.get_channel(data['channel_id'], nil)
      message = channel.cache.get_message(data['message_id'], local: true)
      message.update_reaction(remove: data) if message

      return if ignored_user?(data['user_id'])
      trigger_event(:remove_reaction, self, data)
      trigger_event(:toggle_reaction, self, data)

    when :MESSAGE_REACTION_REMOVE_ALL
      channel = @cache.get_channel(data['channel_id'], nil)
      message = channel.cache.get_message(data['message_id'], local: true)
      message.update_reaction(clear: true) if message

      trigger_event(:clear_reactions, self, data)

    when :TYPING_START
      begin
        return if ignored_user?(data['user_id'])
        trigger_event(:start_typing, self, data)
      rescue MijDiscord::Errors::Forbidden
        # Ignoring the channel we can't access
        # Why is this even sent? :S
      end

    when :USER_UPDATE
      user = @cache.put_user(data, update: true)
      @profile.update_data(data) if @profile == user

      trigger_event(:update_user, self, user)

    when :PRESENCE_UPDATE
      # Bullshit logic due to Discord gateway sending this in a stupid way
      # TODO: Rewrite relevant parts for better handling?
      if data['guild_id']
        server = @cache.get_server(data['guild_id'])
        user = server.cache.get_member(data['user']['id'])

        user&.update_presence(data)
        user&.update_data(data)
      else
        user = @cache.get_user(data['user']['id'])

        user&.update_presence(data)
        user&.update_data(data['user'])

        if @profile == user
          @profile.update_presence(data)
          @profile.update_data(data['user'])
        end
      end

      trigger_event(:update_presence, self, data)

    when :VOICE_STATE_UPDATE
      server = @cache.get_server(data['guild_id'])
      state = server.update_voice_state(data)
      trigger_event(:update_voice_state, self, state)

    else
      MijDiscord::LOGGER.warn('Dispatch') { "Unhandled gateway event type: #{type}" }
      trigger_event(:unhandled, self, type, data)
  end
rescue => exc
  MijDiscord::LOGGER.error('Dispatch') { 'An error occurred in dispatch handler' }
  MijDiscord::LOGGER.error('Dispatch') { exc }
end
handle_exception(type, exception, payload = nil) click to toggle source
# File lib/mij-discord/bot.rb, line 382
def handle_exception(type, exception, payload = nil)
  return if type == :event && payload&.is_a?(MijDiscord::Events::Exception)

  trigger_event(:exception, self, type, exception, payload)
end
handle_heartbeat() click to toggle source
# File lib/mij-discord/bot.rb, line 378
def handle_heartbeat
  trigger_event(:heartbeat, self)
end
ignore_user(user) click to toggle source
# File lib/mij-discord/bot.rb, line 332
def ignore_user(user)
  @ignored_ids << user.to_id
  nil
end
ignored_user?(user) click to toggle source
# File lib/mij-discord/bot.rb, line 342
def ignored_user?(user)
  user = user.to_id

  return true if @ignore_self && user == @auth.id
  return true if @ignored_ids.include?(user)

  if @ignore_bots && (user = @cache.get_user(user, local: true))
    return true if user.bot_account?
  end

  false
end
inspect() click to toggle source
# File lib/mij-discord/bot.rb, line 663
def inspect
  MijDiscord.make_inspect(self, :auth)
end
invite(invite) click to toggle source
# File lib/mij-discord/bot.rb, line 209
def invite(invite)
  code = parse_invite_code(invite)
  response = MijDiscord::Core::API::Invite.resolve(@auth, code, true)
  MijDiscord::Data::Invite.new(JSON.parse(response), self)
end
make_invite_url(server: nil, permissions: nil) click to toggle source
# File lib/mij-discord/bot.rb, line 221
def make_invite_url(server: nil, permissions: nil)
  url = "https://discordapp.com/oauth2/authorize?scope=bot&client_id=#{@auth.id}".dup
  url << "&permissions=#{permissions.to_i}" if permissions.respond_to?(:to_i)
  url << "&guild_id=#{server.to_id}" if server.respond_to?(:to_id)
  url
end
member(server_id, id) click to toggle source
# File lib/mij-discord/bot.rb, line 177
def member(server_id, id)
  gateway_check
  server(server_id)&.member(id)
end
members(server_id) click to toggle source
# File lib/mij-discord/bot.rb, line 172
def members(server_id)
  gateway_check
  server(server_id)&.members
end
parse_invite_code(invite) click to toggle source
# File lib/mij-discord/bot.rb, line 240
def parse_invite_code(invite)
  case invite
    when %r[^(?:https?://)?discord\.gg/(\w+)$]i then $1
    when %r[^https?://discordapp\.com/invite/(\w+)$]i then $1
    when %r[^([a-zA-Z0-9]+)$] then $1
    when MijDiscord::Data::Invite then invite.code
    else raise ArgumentError, 'Invalid invite format'
  end
end
parse_mention(mention, server_id = nil, type: nil) click to toggle source
# File lib/mij-discord/bot.rb, line 282
def parse_mention(mention, server_id = nil, type: nil)
  gateway_check

  mention = mention.to_s.strip

  if !type.nil? && mention =~ /^(\d+)$/
    parse_mention_id($1, type, server_id)

  elsif mention =~ /^<@!?(\d+)>$/
    return nil if type && type != :user
    parse_mention_id($1, :user, server_id)

  elsif mention =~ /^<#(\d+)>$/
    return nil if type && type != :channel
    parse_mention_id($1, :channel, server_id)

  elsif mention =~ /^<@&(\d+)>$/
    return nil if type && type != :role
    parse_mention_id($1, :role, server_id)

  elsif mention =~ /^<(a?):(\w+):(\d+)>$/
    return nil if type && type != :emoji
    parse_mention_id($1, :emoji, server_id) || begin
      em_data = { 'id' => $3.to_i, 'name' => $2, 'animated' => !$1.empty? }
      MijDiscord::Data::Emoji.new(em_data, nil)
    end

  end
end
parse_mention_id(mention, type, server_id = nil) click to toggle source
# File lib/mij-discord/bot.rb, line 250
def parse_mention_id(mention, type, server_id = nil)
  case type
    when :user
      return server_id ? member(server_id, mention) : user(mention)

    when :channel
      return channel(mention, server_id)

    when :role
      role = role(server_id, mention)
      return role if role

      servers.each do |sv|
        role = sv.role(mention)
        return role if role
      end

    when :emoji
      emoji = emoji(server_id, mention)
      return emoji if emoji

      servers.each do |sv|
        emoji = sv.emoji(mention)
        return emoji if emoji
      end

    else raise TypeError, "Invalid mention type '#{type}'"
  end

  nil
end
pm_channel(id) click to toggle source
# File lib/mij-discord/bot.rb, line 155
def pm_channel(id)
  gateway_check
  @cache.get_pm_channel(id)
end
Also aliased as: dm_channel
remove_event(type, key) click to toggle source
# File lib/mij-discord/bot.rb, line 319
def remove_event(type, key)
  raise ArgumentError, "Invalid event type: #{type}" unless EVENTS[type]

  @event_dispatchers[type]&.remove_callback(key)
  nil
end
role(server_id, id) click to toggle source
# File lib/mij-discord/bot.rb, line 187
def role(server_id, id)
  gateway_check
  server(server_id)&.role(id)
end
roles(server_id) click to toggle source
# File lib/mij-discord/bot.rb, line 182
def roles(server_id)
  gateway_check
  server(server_id)&.roles
end
server(id) click to toggle source
# File lib/mij-discord/bot.rb, line 140
def server(id)
  gateway_check
  @cache.get_server(id)
end
servers() click to toggle source
# File lib/mij-discord/bot.rb, line 135
def servers
  gateway_check
  @cache.list_servers
end
shutdown(no_sync = false)
Alias for: disconnect
sync() click to toggle source
# File lib/mij-discord/bot.rb, line 119
def sync
  @gateway.sync
  nil
end
unignore_user(user) click to toggle source
# File lib/mij-discord/bot.rb, line 337
def unignore_user(user)
  @ignored_ids.delete(user.to_id)
  nil
end
update_presence(status: nil, game: nil) click to toggle source
# File lib/mij-discord/bot.rb, line 355
def update_presence(status: nil, game: nil)
  gateway_check

  status = case status
    when nil then @profile.status
    when :online, :idle, :dnd, :online then status
    else raise ArgumentError, 'Invalid status'
  end

  game = case game
    when nil then @profile.game
    when false then nil
    when String, Hash
      MijDiscord::Data::Game.construct(game)
    when MijDiscord::Data::Game then game
    else raise ArgumentError, 'Invalid game'
  end&.to_hash

  @gateway.send_status_update(status, nil, game, false)
  @profile.update_presence('status' => status, 'game' => game)
  nil
end
user(id) click to toggle source
# File lib/mij-discord/bot.rb, line 167
def user(id)
  gateway_check
  @cache.get_user(id)
end
users() click to toggle source
# File lib/mij-discord/bot.rb, line 162
def users
  gateway_check
  @cache.list_users
end

Private Instance Methods

gateway_check() click to toggle source
# File lib/mij-discord/bot.rb, line 669
def gateway_check
  raise 'A gateway connection is required for this action' unless connected?
end
notify_ready() click to toggle source
# File lib/mij-discord/bot.rb, line 673
def notify_ready
  @gateway.notify_ready

  trigger_event(:ready, self)

  if @auth.user?
    guilds = @cache.list_servers.map(&:id)
    @gateway.send_request_guild_sync(guilds)
  end
end
trigger_event(name, *args) click to toggle source
# File lib/mij-discord/bot.rb, line 684
def trigger_event(name, *args)
  @event_dispatchers[name]&.trigger(args)
end