Module: OnyxCord::Cache

Includes:
Stores::Gateway
Included in:
Bot
Defined in:
lib/onyxcord/cache/manager.rb,
lib/onyxcord/cache/stores/gateway.rb

Overview

This mixin module does caching stuff for the library. It conveniently separates the logic behind the caching (like, storing the user hashes or making API calls to retrieve things) from the Bot that actually uses it.

Defined Under Namespace

Modules: Stores

Constant Summary collapse

CACHE_STORES =
{
  users: :@users,
  voice_regions: :@voice_regions,
  servers: :@servers,
  channels: :@channels,
  pm_channels: :@pm_channels,
  thread_members: :@thread_members,
  server_previews: :@server_previews,
  request_members: :@request_members_rl
}.freeze

Instance Method Summary collapse

Instance Method Details

#cache_enabled?(key) ⇒ Boolean

Returns:

  • (Boolean)


41
42
43
# File 'lib/onyxcord/cache/manager.rb', line 41

def cache_enabled?(key)
  @cache_policy.fetch(key, true)
end

#cache_statsObject



45
46
47
48
49
50
# File 'lib/onyxcord/cache/manager.rb', line 45

def cache_stats
  CACHE_STORES.each_with_object({}) do |(key, ivar), stats|
    store = instance_variable_get(ivar)
    stats[key] = store.respond_to?(:count) ? store.count : 0
  end
end

#channel(id, server = nil) ⇒ Channel? Also known as: group_channel

Gets a channel given its ID. This queries the internal channel cache, and if the channel doesn't exist in there, it will get the data from Discord.

Parameters:

  • id (Integer)

    The channel ID for which to search for.

  • server (Server) (defaults to: nil)

    The server for which to search the channel for. If this isn't specified, it will be inferred using the API

Returns:

  • (Channel, nil)

    The channel identified by the ID.

Raises:

  • OnyxCord::Errors::NoPermission



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/onyxcord/cache/manager.rb', line 91

def channel(id, server = nil)
  id = id.resolve_id

  debug("Obtaining data for channel with id #{id}")
  return @channels[id] if @channels&.[](id)

  begin
    response = REST::Channel.resolve(token, id)
  rescue OnyxCord::Errors::UnknownChannel
    return nil
  end
  channel = Channel.new(OnyxCord::Internal::JSON.parse(response), self, server)
  @channels[id] = channel if cache_enabled?(:channels)
  channel
end

#ensure_channel(data, server = nil) ⇒ Channel

Ensures a given channel object is cached and if not, cache it from the given data hash.

Parameters:

  • data (Hash)

    A data hash representing a channel.

  • server (Server, nil) (defaults to: nil)

    The server the channel is on, if known.

Returns:

  • (Channel)

    the channel represented by the data hash.



237
238
239
240
241
242
243
244
245
246
# File 'lib/onyxcord/cache/manager.rb', line 237

def ensure_channel(data, server = nil)
  return Channel.new(data, self, server) unless cache_enabled?(:channels)

  @channels ||= {}
  if @channels.include?(data['id'].to_i)
    @channels[data['id'].to_i]
  else
    @channels[data['id'].to_i] = Channel.new(data, self, server)
  end
end

#ensure_server(data, force_cache = false) ⇒ Server

Ensures a given server object is cached and if not, cache it from the given data hash.

Parameters:

  • data (Hash)

    A data hash representing a server.

  • force_cache (true, false) (defaults to: false)

    Whether the object in cache should be updated with the given data if it already exists.

Returns:

  • (Server)

    the server represented by the data hash.



220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/onyxcord/cache/manager.rb', line 220

def ensure_server(data, force_cache = false)
  return Server.new(data, self) unless cache_enabled?(:servers)

  @servers ||= {}
  if @servers.include?(data['id'].to_i)
    server = @servers[data['id'].to_i]
    server.update_data(data) if force_cache
    server
  else
    @servers[data['id'].to_i] = Server.new(data, self)
  end
end

#ensure_thread_member(data) ⇒ Object

Ensures a given thread member object is cached.

Parameters:

  • data (Hash)

    Thread member data.



250
251
252
253
254
255
256
257
258
259
# File 'lib/onyxcord/cache/manager.rb', line 250

def ensure_thread_member(data)
  return unless cache_enabled?(:thread_members)

  thread_id = data['id'].to_i
  user_id = data['user_id'].to_i

  @thread_members ||= {}
  @thread_members[thread_id] ||= {}
  @thread_members[thread_id][user_id] = data.slice('join_timestamp', 'flags')
end

#ensure_user(data) ⇒ User

Ensures a given user object is cached and if not, cache it from the given data hash.

Parameters:

  • data (Hash)

    A data hash representing a user.

Returns:

  • (User)

    the user represented by the data hash.



204
205
206
207
208
209
210
211
212
213
# File 'lib/onyxcord/cache/manager.rb', line 204

def ensure_user(data)
  return User.new(data, self) unless cache_enabled?(:users)

  @users ||= {}
  if @users.include?(data['id'].to_i)
    @users[data['id'].to_i]
  else
    @users[data['id'].to_i] = User.new(data, self)
  end
end

#fetch_voice_regionsObject



73
74
75
76
77
78
79
80
81
82
# File 'lib/onyxcord/cache/manager.rb', line 73

def fetch_voice_regions
  regions_by_id = {}

  regions = OnyxCord::Internal::JSON.parse REST.voice_regions(token)
  regions.each do |data|
    regions_by_id[data['id']] = VoiceRegion.new(data)
  end

  regions_by_id
end

#find_channel(channel_name, server_name = nil, type: nil) ⇒ Array<Channel>

Finds a channel given its name and optionally the name of the server it is in.

Parameters:

  • channel_name (String)

    The channel to search for.

  • server_name (String) (defaults to: nil)

    The server to search for, or nil if only the channel should be searched for.

  • type (Integer, nil) (defaults to: nil)

    The type of channel to search for (0: text, 1: private, 2: voice, 3: group), or nil if any type of channel should be searched for

Returns:

  • (Array<Channel>)

    The array of channels that were found. May be empty if none were found.



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/onyxcord/cache/manager.rb', line 313

def find_channel(channel_name, server_name = nil, type: nil)
  results = []

  if /<#(?<id>\d+)>?/ =~ channel_name
    # Check for channel mentions separately
    return [channel(id)]
  end

  @servers.each_value do |server|
    server.channels.each do |channel|
      results << channel if channel.name == channel_name && (server_name || server.name) == server.name && (!type || (channel.type == type))
    end
  end

  results
end

#find_user(username) ⇒ Array<User> #find_user(username, discrim) ⇒ User?

Note:

This method only searches through users that have been cached. Users that have not yet been cached by the bot but still share a connection with the user (mutual server) will not be found.

Finds a user given its username or username & discriminator.

Examples:

Find users by name

bot.find_user('z64') #=> Array<User>

Find a user by name and discriminator

bot.find_user('z64', '2639') #=> User

Overloads:

  • #find_user(username) ⇒ Array<User>

    Find all cached users with a certain username.

    Parameters:

    • username (String)

      The username to look for.

    Returns:

    • (Array<User>)

      The array of users that were found. May be empty if none were found.

  • #find_user(username, discrim) ⇒ User?

    Find a cached user with a certain username and discriminator. Find a user by name and discriminator

    Parameters:

    • username (String)

      The username to look for.

    • discrim (String)

      The user's discriminator

    Returns:

    • (User, nil)

      The user that was found, or nil if none was found



347
348
349
350
351
352
# File 'lib/onyxcord/cache/manager.rb', line 347

def find_user(username, discrim = nil)
  users = @users.values.find_all { |e| e.username == username }
  return users.find { |u| u.discrim == discrim } if discrim

  users
end

#init_cacheObject

Initializes this cache



28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/onyxcord/cache/manager.rb', line 28

def init_cache
  @cache_policy ||= OnyxCord.configuration.normalize_cache(OnyxCord.configuration.cache)
  sizes = OnyxCord.configuration.cache_sizes

  @users = cache_enabled?(:users) ? LruRedux::ThreadSafeCache.new(sizes.users) : nil
  @voice_regions = cache_enabled?(:voice_regions) ? {} : nil
  @servers = cache_enabled?(:servers) ? LruRedux::ThreadSafeCache.new(sizes.servers) : nil
  @channels = cache_enabled?(:channels) ? LruRedux::ThreadSafeCache.new(sizes.channels) : nil
  @pm_channels = cache_enabled?(:pm_channels) ? LruRedux::ThreadSafeCache.new(sizes.pm_channels) : nil
  @thread_members = cache_enabled?(:thread_members) ? LruRedux::ThreadSafeCache.new(sizes.thread_members) : nil
  @server_previews = cache_enabled?(:server_previews) ? LruRedux::ThreadSafeCache.new(sizes.server_previews) : nil
end

#invite(invite) ⇒ Invite

Gets information about an invite.

Parameters:

Returns:

  • (Invite)

    The invite with information about the given invite URL.



302
303
304
305
# File 'lib/onyxcord/cache/manager.rb', line 302

def invite(invite)
  code = resolve_invite_code(invite)
  Invite.new(OnyxCord::Internal::JSON.parse(REST::Invite.resolve(token, code)), self)
end

#member(server_or_id, user_id) ⇒ Member?

Gets a member by both IDs, or Server and user ID.

Parameters:

  • server_or_id (Server, Integer)

    The Server or server ID for which a member should be resolved

  • user_id (Integer)

    The ID of the user that should be resolved

Returns:

  • (Member, nil)

    The member identified by the IDs, or nil if none could be found



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/onyxcord/cache/manager.rb', line 151

def member(server_or_id, user_id)
  server_id = server_or_id.resolve_id
  user_id = user_id.resolve_id
  server = server_or_id.is_a?(Server) ? server_or_id : self.server(server_id)

  return server.member(user_id) if server.member_cached?(user_id)

  LOGGER.out("Resolving member #{server_id} on server #{user_id}")
  begin
    response = REST::Server.resolve_member(token, server_id, user_id)
  rescue OnyxCord::Errors::UnknownUser, OnyxCord::Errors::UnknownMember
    return nil
  end
  member = Member.new(OnyxCord::Internal::JSON.parse(response), server, self)
  server.cache_member(member)
end

#pm_channel(id) ⇒ Channel Also known as: private_channel

Creates a PM channel for the given user ID, or if one exists already, returns that one. It is recommended that you use User#pm instead, as this is mainly for internal use. However, usage of this method may be unavoidable if only the user ID is known.

Parameters:

  • id (Integer)

    The user ID to generate a private channel for.

Returns:

  • (Channel)

    A private channel for that user.



173
174
175
176
177
178
179
180
181
182
# File 'lib/onyxcord/cache/manager.rb', line 173

def pm_channel(id)
  id = id.resolve_id
  return @pm_channels[id] if @pm_channels&.[](id)

  debug("Creating pm channel with user id #{id}")
  response = REST::User.create_pm(token, id)
  channel = Channel.new(OnyxCord::Internal::JSON.parse(response), self)
  @pm_channels[id] = channel if cache_enabled?(:pm_channels)
  channel
end

#prune_cache!(*keys) ⇒ Object



52
53
54
55
56
57
58
59
60
61
# File 'lib/onyxcord/cache/manager.rb', line 52

def prune_cache!(*keys)
  keys = CACHE_STORES.keys if keys.empty?

  keys.each_with_object({}) do |key, pruned|
    ivar = CACHE_STORES.fetch(key)
    store = instance_variable_get(ivar)
    pruned[key] = store.respond_to?(:count) ? store.count : 0
    store&.clear
  end
end

#request_chunks(id) ⇒ Object

Requests member chunks for a given server ID.

Parameters:

  • id (Integer)

    The server ID to request chunks for.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/onyxcord/cache/manager.rb', line 263

def request_chunks(id)
  id = id.resolve_id

  bucket = (@request_members_rl[id] ||= { mutex: Mutex.new, time: Time.at(0) })

  bucket[:mutex].synchronize do
    last = bucket[:time]
    now = Time.now

    if now < last
      duration = last - now

      LOGGER.info("Preemptively locking REQUEST_GUILD_MEMBERS for #{duration} seconds")
      sleep(duration)
    end

    @gateway.send_request_members(id, '', 0)
    bucket[:time] = (Time.now + 30)
  end
end

#resolve_invite_code(invite) ⇒ String

Gets the code for an invite.

Parameters:

  • invite (String, Invite)

    The invite to get the code for. Possible formats are:

    • An Invite object
    • The code for an invite
    • A fully qualified invite URL (e.g. https://discord.com/invite/0A37aN7fasF7n83q)
    • A short invite URL with protocol (e.g. https://discord.gg/0A37aN7fasF7n83q)
    • A short invite URL without protocol (e.g. discord.gg/0A37aN7fasF7n83q)

Returns:

  • (String)

    Only the code for the invite.



293
294
295
296
297
# File 'lib/onyxcord/cache/manager.rb', line 293

def resolve_invite_code(invite)
  invite = invite.code if invite.is_a? OnyxCord::Invite
  invite = invite[(invite.rindex('/') + 1)..] if invite.start_with?('http', 'discord.gg')
  invite
end

#server(id) ⇒ Server?

Note:

This can only resolve servers the bot is currently in.

Gets a server by its ID.

Parameters:

  • id (Integer)

    The server ID that should be resolved.

Returns:

  • (Server, nil)

    The server identified by the ID, or nil if it couldn't be found.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/onyxcord/cache/manager.rb', line 132

def server(id)
  id = id.resolve_id
  return @servers[id] if @servers&.[](id)

  LOGGER.out("Resolving server #{id}")
  begin
    response = REST::Server.resolve(token, id)
  rescue OnyxCord::Errors::NoPermission
    return nil
  end
  server = Server.new(OnyxCord::Internal::JSON.parse(response), self)
  @servers[id] = server if cache_enabled?(:servers)
  server
end

#server_preview(id) ⇒ ServerPreview?

Get a server preview. If the bot isn't a member of the server, the server must be discoverable.

Parameters:

Returns:

  • (ServerPreview, nil)

    the server preview, or nil if the server isn't accessible.



189
190
191
192
193
194
195
196
197
198
199
# File 'lib/onyxcord/cache/manager.rb', line 189

def server_preview(id)
  id = id.resolve_id
  return @server_previews[id] if @server_previews&.[](id)

  response = OnyxCord::Internal::JSON.parse(REST::Server.preview(token, id))
  preview = ServerPreview.new(response, self)
  @server_previews[id] = preview if cache_enabled?(:server_previews)
  preview
rescue StandardError
  nil
end

#user(id) ⇒ User?

Note:

This can only resolve users known by the bot (i.e. that share a server with the bot).

Gets a user by its ID.

Parameters:

  • id (Integer)

    The user ID that should be resolved.

Returns:

  • (User, nil)

    The user identified by the ID, or nil if it couldn't be found.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/onyxcord/cache/manager.rb', line 113

def user(id)
  id = id.resolve_id
  return @users[id] if @users&.[](id)

  LOGGER.out("Resolving user #{id}")
  begin
    response = REST::User.resolve(token, id)
  rescue OnyxCord::Errors::UnknownUser
    return nil
  end
  user = User.new(OnyxCord::Internal::JSON.parse(response), self)
  @users[id] = user if cache_enabled?(:users)
  user
end

#voice_regionsObject

Returns or caches the available voice regions



64
65
66
67
68
69
70
71
# File 'lib/onyxcord/cache/manager.rb', line 64

def voice_regions
  return fetch_voice_regions unless cache_enabled?(:voice_regions)

  @voice_regions ||= {}
  return @voice_regions unless @voice_regions.empty?

  @voice_regions = fetch_voice_regions
end