Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/vox/cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

require 'vox/cache/base'
require 'vox/cache/memory'
require 'vox/cache/manager'
27 changes: 27 additions & 0 deletions lib/vox/cache/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Vox
# Module that contains tools for managing a cache.
module Cache
# Noop cache that only provides interface methods
class Base
# Retrieve a value from the cache by key
# @param _key [String] This will typically be an ID.
# @return [nil]
def get(_key); end

# Set a value in the cache
# @param _key [String]
# @param value [Object]
# @return [Object] The provided value.
def set(_key, value)
value
end

# Delete a value from the cache
# @param _key [String]
# @return [nil]
def delete(_key); end
end
end
end
71 changes: 71 additions & 0 deletions lib/vox/cache/manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

# frozen_string_literal: true

require 'vox/cache/memory'

module Vox
module Cache
# Manages caches, index by symbol keys. Used by {Vox::Client}.
class Manager
attr_accessor :default

# @yield [self]
def initialize(default: Memory, **options)
@caches = options
@default = default
yield(self) if block_given?
end

# Retrieve a key from a managed cache.
# @param cache [Symbol]
# @param key [String]
def get(cache, key, &block)
cache_or_default(cache)

@caches[cache].get(key, &block)
end

# Set a key for a managed cache.
# @param cache [Symbol]
# @param key [String]
# @param value [Object]
# @return [Object] The value that was set
def set(cache, key, value)
cache_or_default(cache)

@caches[cache].set(key, value)
end

# Retrieve a cache by name.
# @param name [Symbol]
# @return [Cache::Base]
def [](name)
@caches[name] ||= cache_or_default(name)
end

# Remove a key from a managed cache.
# @param cache [Symbol]
# @param key [String]
# @return [nil]
def delete(cache, key)
cache_or_default(cache)

@caches[cache].delete(key)
end

private

# Returns a cache if it exists, or create a new one from a the given
# default.
# @param cache [Symbol]
def cache_or_default(cache)
@caches[cache] ||= if @default_cache.respond_to?(:call)
@default.call
else
@default.new
end
end
end
end
end
37 changes: 37 additions & 0 deletions lib/vox/cache/memory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require 'vox/cache/base'

module Vox
module Cache
# A cache that uses a hash as the storage method.
class Memory < Base
def initialize
@data = {}
super
end

# Retrieve a value from the cache by key
# @param key [String] This will typically be an ID.
# @return [Object]
def get(key)
@data[key]
end

# Set a value in the cache
# @param key [String]
# @param value [Object]
# @return [Object] The provided value.
def set(key, value)
@data[key] = value
end

# Delete a value from the cache
# @param key [String]
# @return [nil]
def delete(key)
@data.delete(key)
end
end
end
end
195 changes: 195 additions & 0 deletions lib/vox/client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# frozen_string_literal: true

require 'vox/http/client'
require 'vox/gateway/client'
require 'vox/cache'
require 'vox/objects'

module Vox
# A client that bridges the gateway, http, and stateful object components.
class Client
include EventEmitter

attr_reader :http, :gateway

def initialize(token:, cache_manager: Cache::Manager.new, gateway_options: {}, http_options: {})
@cache_manager = cache_manager
@http = HTTP::Client.new(**{ token: token }.merge(http_options))
@gateway = create_gateway(token: token, options: gateway_options)

setup_gateway_caching
end

# Retrieve an object from the cache or set it with a provided block.
def cache(cache_key, key, cached: true)
if cached
@cache_manager.get(cache_key, key) || @cache_manager.set(cache_key, key, yield)
else
@cache_manager.set(cache_key, key, yield)
end
end

# @!visibility private
def cache_upsert(cache_key, key, data)
obj = @cache_manager.get(cache_key, key)
if obj
obj.update_data(data.to_hash)
else
@cache_manager.set(cache_key, key, data)
nil
end
end

# @return [User]
def user(id, cached: true)
cache(:user, id.to_s, cached) { User.new(self, @http.get_user(id)) }
end

# @return [Profile]
def current_user(cached: true)
data = cache(:user, :@me, cached: cached) { Profile.new(self, @http.get_current_user) }
@cache_manager.set(:user, data.id, data)
end

# @return [Guild]
def guild(id, cached: true)
cache(:guild, id.to_s, cached: cached) { Guild.new(self, @http.get_guild(id)) }
end

# @return [Channel]
def channel(id, cached: true)
cache(:channel, id.to_s, cached: cached) { Channel.new(self, @http.get_channel(id)) }
end

# @return [Member]
def member(guild_id, user_id, cached: true)
cache(:member, [guild_id.to_s, user_id.to_s], cached: cached) do
Member.new(self, @http.get_guild_member(guild_id, user_id), guild(guild_id, cached: cached))
end
end

# @return [Role, nil]
def role(role_id)
@cache_manager.get(:role, role_id.to_s)
end

# @return [Emoji, nil]
def emoji(emoji_id)
@cache_manager.get(:emoji, emoji_id.to_s)
end

# @return [Webhook]
def webhook(webhook_id, token: nil, cached: true)
cache(:webhook, webhook_id.to_s, cached: cached) do
data = if token
@http.get_webhook_with_token(webhook_id, token)
else
@http.get_webhook(webhook_id)
end
Webhook.new(self, data)
end
end

# @return [Invite]
def invite(invite_code, cached: true)
cache(:invite, invite_code, cached) do
Invite.new(self, @http.get_invite(invite_code))
end
end

def connect(async: false)
@gateway.connect(async: async)
end

private

def create_gateway(token:, options:)
options[:url] ||= @http.get_gateway_bot[:url]
Vox::Gateway::Client.new(**{ token: token }.merge(**options))
end

def setup_gateway_caching
@gateway.on(:GUILD_CREATE, &method(:handle_guild_create))
@gateway.on(:GUILD_UPDATE, &method(:handle_guild_update))
@gateway.on(:GUILD_DELETE, &method(:handle_guild_delete))
@gateway.on(:GUILD_ROLE_CREATE, &method(:handle_guild_role_create))

@gateway.on(:CHANNEL_UPDATE, &method(:handle_channel_update))
@gateway.on(:CHANNEL_CREATE, &method(:handle_channel_create))
@gateway.on(:CHANNEL_DELETE, &method(:handle_channel_delete))
end

def handle_guild_create(data)
guild_data = Guild.new(self, data)
cache_upsert(:guild, data[:id], guild_data)

emit(:GUILD_CREATE, guild(data[:id]) || guild_data)
end

def handle_guild_update(data)
guild_data = Guild.new(self, data)
cache_upsert(:guild, guild_data.id, guild_data)

emit(:GUILD_UPDATE, guild(data[:id]) || guild_data)
end

def handle_guild_delete(data)
guild_data = @cache_manager.get(:guild, data[:id])

emit(:GUILD_DELETE, guild_data)
@cache_manager.delete(:guild, data[:id])
end

def handle_guild_role_create(data)
guild_data = guild(data[:guild_id])
role_data = Role.new(self, data[:role])
cache_upsert(:role, role_data[:id], role_data)
guild_data.roles << role_data if guild_data

emit(:GUILD_ROLE_CREATE, role(data[:id]) || role_data)
end

def handle_guild_role_update(data)
role_data = Role.new(self, data)
cache_upsert(:role, role_data)

emit(:GUILD_ROLE_UPDATE, role(data[id]) || data)
end

def handle_guild_role_delete(data)
role_data = role(data[:role_id]) || Role.new(self, data)
guild_data = guild(data[:guild_id])
guild_data.roles.delete(role_data)

emit(:GUILD_ROLE_DELETE, role_data)
end

def handle_channel_create(data)
channel_data = Channel.new(self, data)
cache_upsert(:channel, data[:id], data)

emit(:CHANNEL_CREATE, channel(data[:id]) || channel_data)
end

def handle_channel_update(data)
channel_data = Channel.new(self, data)
cache_upsert(:channel, data[:id], channel_data)

emit(:CHANNEL_UPDATE, channel(data[:id]) || channel_data)
end

def handle_channel_delete(data)
channel_data = Channel.new(data)
@cache_manager.delete(:channel, data[:id])

emit(:CHANNEL_DELETE, channel_data)
end

def handle_channel_pins_update(data)
channel_data = channel(data[:channel_id])
channel_data.update_data({ last_pin_timestamp: data[:last_pin_timestamp] })

emit(:CHANNEL_PINS_UPDATE, channel_data)
end
end
end
8 changes: 4 additions & 4 deletions lib/vox/http/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ class Client
'User-Agent': "DiscordBot (https://github.com/swarley/vox, #{Vox::VERSION})"
}.freeze

def initialize(token)
# @note The use of the positional token is deprecated and will be removed.
def initialize(depr_token = nil, token: nil, token_type: 'Bot')
token ||= depr_token
@conn = default_connection
@conn.authorization('Bot', token.delete_prefix('Bot '))
@conn.authorization(token_type, token.delete_prefix("#{token_type} "))
yield(@conn) if block_given?
end

Expand Down Expand Up @@ -102,8 +104,6 @@ def handle_response(resp, raw, req_id)

data = raw ? resp.body : MultiJson.load(resp.body, symbolize_keys: true)
case resp.status
when 204, 304
nil
when 200..300
data
when 400
Expand Down
12 changes: 12 additions & 0 deletions lib/vox/objects.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

require 'vox/objects/api_object'
require 'vox/objects/channel'
require 'vox/objects/emoji'
require 'vox/objects/guild'
require 'vox/objects/invite'
require 'vox/objects/message'
require 'vox/objects/role'
require 'vox/objects/user'
require 'vox/objects/webhook'
require 'vox/objects/permissions'
Loading