Module: OnyxCord::REST

Defined in:
lib/onyxcord/rest/client.rb

Overview

List of methods representing endpoints in Discord's API

Defined Under Namespace

Modules: Application, Channel, Interaction, Invite, Server, User, Webhook

Constant Summary collapse

APIBASE =

The base URL of the Discord REST API.

'https://discord.com/api/v10'
CDN_URL =

The URL of Discord's CDN

'https://cdn.discordapp.com'
MAX_RAW_RETRIES =

Maximum number of retries for 502 Bad Gateway errors

3
MAX_429_RETRIES =

Maximum number of retries for 429 Rate Limit errors

5

Class Method Summary collapse

Class Method Details

.achievement_icon_url(application_id, achievement_id, icon_hash, format = 'webp') ⇒ Object

Make an achievement icon URL from application ID, achievement ID, and icon hash



380
381
382
# File 'lib/onyxcord/rest/client.rb', line 380

def achievement_icon_url(application_id, achievement_id, icon_hash, format = 'webp')
  "#{cdn_url}/app-assets/#{application_id}/achievements/#{achievement_id}/icons/#{icon_hash}.#{format}"
end

.api_baseString

Returns the currently used API base URL.

Returns:

  • (String)

    the currently used API base URL.



29
30
31
# File 'lib/onyxcord/rest/client.rb', line 29

def api_base
  @api_base || APIBASE
end

.api_base=(value) ⇒ Object

Sets the API base URL to something.



34
35
36
# File 'lib/onyxcord/rest/client.rb', line 34

def api_base=(value)
  @api_base = value
end

.app_cover_url(app_id, cover_id, format = 'webp') ⇒ Object

make a cover image URL from application and cover IDs.



418
419
420
# File 'lib/onyxcord/rest/client.rb', line 418

def app_cover_url(app_id, cover_id, format = 'webp')
  "#{cdn_url}/app-icons/#{app_id}/#{cover_id}.#{format}"
end

.app_icon_url(app_id, icon_id, format = 'webp') ⇒ Object

Make an icon URL from application and icon IDs



345
346
347
# File 'lib/onyxcord/rest/client.rb', line 345

def app_icon_url(app_id, icon_id, format = 'webp')
  "#{cdn_url}/app-icons/#{app_id}/#{icon_id}.#{format}"
end

.asset_url(application_id, asset_id, format = 'webp') ⇒ Object

Make an asset URL from application and asset IDs



375
376
377
# File 'lib/onyxcord/rest/client.rb', line 375

def asset_url(application_id, asset_id, format = 'webp')
  "#{cdn_url}/app-assets/#{application_id}/#{asset_id}.#{format}"
end

.async_rate_limiterObject



82
83
84
# File 'lib/onyxcord/rest/client.rb', line 82

def async_rate_limiter
  @async_rate_limiter ||= ::OnyxCord::Internal::RateLimiter::AsyncRest.new
end

.async_rate_limiter_statsObject



90
91
92
# File 'lib/onyxcord/rest/client.rb', line 90

def async_rate_limiter_stats
  async_rate_limiter.stats
end

.avatar_decoration_url(avatar_decoration_id, format = 'png') ⇒ Object

make an avatar decoration URL from an avatar decoration ID.



393
394
395
# File 'lib/onyxcord/rest/client.rb', line 393

def avatar_decoration_url(avatar_decoration_id, format = 'png')
  "#{cdn_url}/avatar-decoration-presets/#{avatar_decoration_id}.#{format}"
end

Make a banner URL from server and banner IDs



365
366
367
# File 'lib/onyxcord/rest/client.rb', line 365

def banner_url(server_id, banner_id, format = 'webp')
  "#{cdn_url}/banners/#{server_id}/#{banner_id}.#{format}"
end

.bot_nameString

Returns the bot name, previously specified using bot_name=.

Returns:



44
45
46
# File 'lib/onyxcord/rest/client.rb', line 44

def bot_name
  @bot_name
end

.bot_name=(value) ⇒ Object

Sets the bot name to something. Used in user_agent. For the bot's username, see Profile#username=.



49
50
51
# File 'lib/onyxcord/rest/client.rb', line 49

def bot_name=(value)
  @bot_name = value
end

.cdn_urlString

Returns the currently used CDN url.

Returns:

  • (String)

    the currently used CDN url



39
40
41
# File 'lib/onyxcord/rest/client.rb', line 39

def cdn_url
  @cdn_url || CDN_URL
end

.discovery_splash_url(server_id, splash_id, format = 'webp') ⇒ Object

Make a discovery splash URL from server and splash IDs



360
361
362
# File 'lib/onyxcord/rest/client.rb', line 360

def discovery_splash_url(server_id, splash_id, format = 'webp')
  "#{cdn_url}/discovery-splashes/#{server_id}/#{splash_id}.#{format}"
end

.emoji_icon_url(emoji_id, format = 'webp') ⇒ Object

Make an emoji icon URL from emoji ID



370
371
372
# File 'lib/onyxcord/rest/client.rb', line 370

def emoji_icon_url(emoji_id, format = 'webp')
  "#{cdn_url}/emojis/#{emoji_id}.#{format}"
end

.gateway(token) ⇒ Object

Get the gateway to be used



453
454
455
456
457
458
459
460
461
# File 'lib/onyxcord/rest/client.rb', line 453

def gateway(token)
  request(
    :gateway,
    nil,
    :get,
    "#{api_base}/gateway",
    Authorization: token
  )
end

.gateway_bot(token) ⇒ Object

Get the gateway to be used, with additional information for sharding and session start limits



465
466
467
468
469
470
471
472
473
# File 'lib/onyxcord/rest/client.rb', line 465

def gateway_bot(token)
  request(
    :gateway_bot,
    nil,
    :get,
    "#{api_base}/gateway/bot",
    Authorization: token
  )
end

.handle_preemptive_rl(headers, mutex, key) ⇒ Object

Handles pre-emptive rate limiting by waiting the given mutex by the difference of the Date header to the X-Ratelimit-Reset header, thus making sure we don't get 429'd in any subsequent requests.



297
298
299
300
301
302
# File 'lib/onyxcord/rest/client.rb', line 297

def handle_preemptive_rl(headers, mutex, key)
  OnyxCord::LOGGER.ratelimit "RL bucket depletion detected! Date: #{headers[:date]} Reset: #{headers[:x_ratelimit_reset]}"
  delta = headers[:x_ratelimit_reset_after].to_f
  OnyxCord::LOGGER.warn("Locking RL mutex (key: #{key}) for #{delta} seconds pre-emptively")
  sync_wait(delta, mutex)
end

.icon_url(server_id, icon_id, format = 'webp') ⇒ Object

Make an icon URL from server and icon IDs



340
341
342
# File 'lib/onyxcord/rest/client.rb', line 340

def icon_url(server_id, icon_id, format = 'webp')
  "#{cdn_url}/icons/#{server_id}/#{icon_id}.#{format}"
end

.mutex_wait(mutex) ⇒ Object

Wait for a specified mutex to unlock and do nothing with it afterwards.



100
101
102
103
# File 'lib/onyxcord/rest/client.rb', line 100

def mutex_wait(mutex)
  mutex.lock
  mutex.unlock
end

.nameplate_url(nameplate_asset, format = 'webm') ⇒ Object

make a nameplate URL from the nameplate asset.



403
404
405
# File 'lib/onyxcord/rest/client.rb', line 403

def nameplate_url(nameplate_asset, format = 'webm')
  "#{cdn_url}/assets/collectibles/#{nameplate_asset.delete_suffix('/')}/asset.#{format}"
end

.oauth_application(token) ⇒ Object

Get the bot's OAuth application's information



442
443
444
445
446
447
448
449
450
# File 'lib/onyxcord/rest/client.rb', line 442

def oauth_application(token)
  request(
    :oauth2_applications_me,
    nil,
    :get,
    "#{api_base}/applications/@me",
    Authorization: token
  )
end

.rate_limiterObject



78
79
80
# File 'lib/onyxcord/rest/client.rb', line 78

def rate_limiter
  @rate_limiter ||= ::OnyxCord::Internal::RateLimiter::Rest.new
end

.rate_limiter_statsObject



86
87
88
# File 'lib/onyxcord/rest/client.rb', line 86

def rate_limiter_stats
  rate_limiter.stats
end

.raw_request(type, url, body = nil, **headers) ⇒ OnyxCord::Internal::HTTP::Response

Performs a raw HTTP request using HTTPX.

Parameters:

  • type (Symbol)

    The type of HTTP request to use.

  • url (String)

    The URL to request.

  • body (String, Hash, nil) (defaults to: nil)

    The request body.

  • headers (Hash)

    Additional headers.

Returns:



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/onyxcord/rest/client.rb', line 111

def raw_request(type, url, body = nil, **headers)
  headers[:user_agent] = user_agent

  response = ::OnyxCord::Internal::HTTP.request(type, url, body, **headers)

  if response.code == 403
    route = request_diagnostic(type, url, body, headers)
    noprm = OnyxCord::Errors::NoPermission.new(
      "The bot doesn't have the required permission to do this!",
      status: response.code,
      headers: response.headers,
      route: route,
      body: response.body,
      response: response
    )
    noprm.define_singleton_method(:_response) { response }
    raise noprm
  end

  # Retry on 502 Bad Gateway (with limit and backoff)
  if response.code == 502
    retries = caller.count { |c| c.include?('raw_request') }
    if retries >= MAX_RAW_RETRIES
      OnyxCord::LOGGER.warn("Got #{MAX_RAW_RETRIES} consecutive 502s — giving up on #{url}")
      return response
    end
    OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
    sleep(2**retries)
    return raw_request(type, url, body, **headers)
  end

  response
end

.request(key, major_parameter, type, *attributes) ⇒ Object

Make an API request, including rate limit handling.



146
147
148
149
150
151
152
# File 'lib/onyxcord/rest/client.rb', line 146

def request(key, major_parameter, type, *attributes)
  if Async::Task.current?
    request_async(key, major_parameter, type, *attributes)
  else
    OnyxCord::Internal::AsyncRuntime.run { request_async(key, major_parameter, type, *attributes) }
  end
end

.request_async(key, major_parameter, type, *attributes) ⇒ Object

Async version of request.



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/onyxcord/rest/client.rb', line 155

def request_async(key, major_parameter, type, *attributes)
  url = attributes.shift
  headers_or_body = attributes

  body = nil
  headers = {}

  headers_or_body.each do |arg|
    if arg.is_a?(Hash)
      headers.merge!(arg)
    elsif body.nil?
      body = arg
    end
  end

  content_type = headers.delete(:content_type)
  headers['content-type'] = 'application/json' if content_type == :json

  headers['user-agent'] = user_agent

  retries = 0
  max_retries = key == :gateway ? 0 : 3
  retryable_codes = [500, 502, 503, 504].freeze

  begin
    async_rate_limiter.before_request(key, major_parameter)

    response = nil
    loop do
      begin
        response = ::OnyxCord::Internal::HTTP.request(type, url, body, **headers)
      rescue StandardError => e
        retries += 1
        raise unless retries < max_retries

        ::OnyxCord::Internal::HTTP.reset!
        OnyxCord::LOGGER.warn("Temporary HTTP failure while sending request (#{e.class}), retrying")
        OnyxCord::Internal::AsyncRuntime.sleep(retries * 0.5)
        next
      end

      break unless retryable_codes.include?(response.code)

      retries += 1
      break unless retries < max_retries

      OnyxCord::LOGGER.warn("Got HTTP #{response.code} while sending a request, retrying")
      OnyxCord::Internal::AsyncRuntime.sleep(retries * 0.5)
    end

    if response.code == 403
      route = request_diagnostic(type, url, body, headers)
      noprm = OnyxCord::Errors::NoPermission.new(
        "The bot doesn't have the required permission to do this!",
        status: response.code,
        headers: response.headers,
        route: route,
        body: response.body,
        response: response
      )
      noprm.define_singleton_method(:_response) { response }
      raise noprm
    end

    if response.code >= 400 && response.code != 429
      data = begin
        OnyxCord::Internal::JSON.parse(response.body)
      rescue StandardError
        nil
      end

      route = request_diagnostic(type, url, body, headers)
      unless data
        raise OnyxCord::Errors::HTTPError.new(
          "HTTP #{response.code} #{route}: #{response.body}",
          status: response.code,
          headers: response.headers,
          route: route,
          body: response.body,
          response: response
        )
      end

      err_klass = OnyxCord::Errors.error_class_for(data['code'] || 0)
      e = err_klass.new(data['message'], data['errors'], status: response.code, headers: response.headers, route: route, body: response.body, response: response)
      if e.is_a?(OnyxCord::Errors::UnknownMessage)
        OnyxCord::LOGGER.warn('Ignoring stale Discord message reference.')
      else
        OnyxCord::LOGGER.error(e.full_message)
      end
      raise e
    end
  rescue OnyxCord::Errors::NoPermission => e
    async_rate_limiter.record_response(key, major_parameter, e._response.headers) if e.respond_to?(:_response)
    raise e
  else
    async_rate_limiter.record_response(key, major_parameter, response.headers)
  end

  if response.code == 429
    retries_for_429 = caller.count { |c| c.include?('request_async') && c.include?('429') }
    trace("429 #{key} #{major_parameter}")
    async_rate_limiter.handle_rate_limit(key, major_parameter, response)

    if retries_for_429 >= MAX_429_RETRIES
      raise OnyxCord::Errors::HTTPError.new(
        "Rate limited after #{MAX_429_RETRIES} retries: #{key} #{major_parameter}",
        status: 429,
        headers: response.headers,
        body: response.body,
        response: response
      )
    end

    retry_after = response.headers[:x_ratelimit_reset_after]&.to_f || response.headers[:retry_after]&.to_f
    if retry_after && retry_after.positive?
      OnyxCord::LOGGER.warn("Rate limited — waiting #{retry_after}s before retry ##{retries_for_429 + 1}")
      OnyxCord::Internal::AsyncRuntime.sleep(retry_after)
    end
    return request_async(key, major_parameter, type, url, body, headers)
  end

  if response.code == 202 && response.body
    body_data = OnyxCord::Internal::JSON.parse(response.body)

    if body_data['code'] == 110_000
      case body_data['retry_after']
      when 0, 1, nil
        OnyxCord::Internal::AsyncRuntime.sleep(rand(4.5..5.0))
      else
        OnyxCord::Internal::AsyncRuntime.sleep(body_data['retry_after'])
      end

      return request_async(key, major_parameter, type, url, body, headers)
    end
  end

  response
end

.request_diagnostic(type, url, body, headers) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/onyxcord/rest/client.rb', line 320

def request_diagnostic(type, url, body, headers)
  clean_url = url.to_s.sub(%r{/webhooks/(\d+)/[^?]+}, '/webhooks/\1/[token]')
                 .sub(%r{/interactions/(\d+)/[^/]+}, '/interactions/\1/[token]')
  header_keys = headers.keys.map(&:to_s).sort.join(',')
  body_info = if body.is_a?(Hash)
                body.map do |key, value|
                  path = value.path if value.respond_to?(:path)
                  detail = path ? "file:#{File.basename(path)}" : value.to_s.bytesize
                  "#{key}=#{detail}"
                end.join(',')
              else
                body.nil? ? 'nil' : "#{body.class}:#{body.to_s.bytesize}"
              end

  "(#{type.to_s.upcase} #{clean_url} headers=#{header_keys} body=#{body_info})"
rescue StandardError => e
  "(diagnostic_failed=#{e.class}: #{e.message})"
end

.reset_mutexesObject

Resets all rate limit mutexes



70
71
72
73
74
75
76
# File 'lib/onyxcord/rest/client.rb', line 70

def reset_mutexes
  @rest_mutex ||= Mutex.new
  @rest_mutex.synchronize do
    @rate_limiter = ::OnyxCord::Internal::RateLimiter::Rest.new
    @async_rate_limiter = ::OnyxCord::Internal::RateLimiter::AsyncRest.new
  end
end

.role_icon_url(role_id, icon_hash, format = 'webp') ⇒ String

Parameters:

  • role_id (String, Integer)
  • icon_hash (String)
  • format ('webp', 'png', 'jpeg') (defaults to: 'webp')

Returns:



388
389
390
# File 'lib/onyxcord/rest/client.rb', line 388

def role_icon_url(role_id, icon_hash, format = 'webp')
  "#{cdn_url}/role-icons/#{role_id}/#{icon_hash}.#{format}"
end

.scheduled_event_cover_url(scheduled_event_id, cover_id, format = 'webp', size = nil) ⇒ Object

make a scheduled event cover URL from a scheduled event ID and a cover ID.



413
414
415
# File 'lib/onyxcord/rest/client.rb', line 413

def scheduled_event_cover_url(scheduled_event_id, cover_id, format = 'webp', size = nil)
  "#{cdn_url}/guild-events/#{scheduled_event_id}/#{cover_id}.#{format}#{"?size=#{size}" if size}"
end

.server_tag_badge_url(server_id, badge_id, format = 'webp') ⇒ Object

make a server tag badge URL from a server ID and badge ID.



408
409
410
# File 'lib/onyxcord/rest/client.rb', line 408

def server_tag_badge_url(server_id, badge_id, format = 'webp')
  "#{cdn_url}/guild-tag-badges/#{server_id}/#{badge_id}.#{format}"
end

.splash_url(server_id, splash_id, format = 'webp') ⇒ Object

Make a splash URL from server and splash IDs



355
356
357
# File 'lib/onyxcord/rest/client.rb', line 355

def splash_url(server_id, splash_id, format = 'webp')
  "#{cdn_url}/splashes/#{server_id}/#{splash_id}.#{format}"
end

.static_nameplate_url(nameplate_asset, format = 'png') ⇒ Object

make a static nameplate URL from the nameplate asset.



398
399
400
# File 'lib/onyxcord/rest/client.rb', line 398

def static_nameplate_url(nameplate_asset, format = 'png')
  "#{cdn_url}/assets/collectibles/#{nameplate_asset.delete_suffix('/')}/static.#{format}"
end

.sync_wait(time, mutex) ⇒ Object

Wait a specified amount of time synchronised with the specified mutex.



95
96
97
# File 'lib/onyxcord/rest/client.rb', line 95

def sync_wait(time, mutex)
  mutex.synchronize { sleep time }
end

.team_icon_url(team_id, icon_id, format = 'webp') ⇒ Object

make a team icon URL from team and icon IDs.



423
424
425
# File 'lib/onyxcord/rest/client.rb', line 423

def team_icon_url(team_id, icon_id, format = 'webp')
  "#{cdn_url}/team-icons/#{team_id}/#{icon_id}.#{format}"
end

.trace(reason) ⇒ Object

Perform rate limit tracing. All this method does is log the current backtrace to the console with the :ratelimit level.

Parameters:

  • reason (String)

    the reason to include with the backtrace.



307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/onyxcord/rest/client.rb', line 307

def trace(reason)
  unless @trace
    OnyxCord::LOGGER.debug("trace was called with reason #{reason}, but tracing is not enabled")
    return
  end

  OnyxCord::LOGGER.ratelimit("Trace (#{reason}):")

  caller.each do |str|
    OnyxCord::LOGGER.ratelimit(" #{str}")
  end
end

.trace=(value) ⇒ Object

Changes the rate limit tracing behaviour. If rate limit tracing is on, a full backtrace will be logged on every RL hit.

Parameters:

  • value (true, false)

    whether or not to enable rate limit tracing



56
57
58
# File 'lib/onyxcord/rest/client.rb', line 56

def trace=(value)
  @trace = value
end

.update_oauth_application(token, name, redirect_uris, description = '', icon = nil) ⇒ Object

Deprecated.

Please use OnyxCord::REST::Application#update_current_application instead.

Change an OAuth application's properties



429
430
431
432
433
434
435
436
437
438
439
# File 'lib/onyxcord/rest/client.rb', line 429

def update_oauth_application(token, name, redirect_uris, description = '', icon = nil)
  request(
    :oauth2_applications,
    nil,
    :put,
    "#{api_base}/oauth2/applications",
    { name: name, redirect_uris: redirect_uris, description: description, icon: icon }.to_json,
    Authorization: token,
    content_type: :json
  )
end

.user_agentObject

Generate a user agent identifying this requester as onyxcord.



61
62
63
64
65
66
67
# File 'lib/onyxcord/rest/client.rb', line 61

def user_agent
  # This particular string is required by the Discord devs.
  required = "DiscordBot (https://github.com/kruldevb/OnyxCord, v#{OnyxCord::VERSION})"
  @bot_name ||= ''

  "#{required} httpx/#{HTTPX::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} onyxcord/#{OnyxCord::VERSION} #{@bot_name}"
end

.voice_regions(token) ⇒ Object

Get a list of available voice regions



476
477
478
479
480
481
482
483
484
485
# File 'lib/onyxcord/rest/client.rb', line 476

def voice_regions(token)
  request(
    :voice_regions,
    nil,
    :get,
    "#{api_base}/voice/regions",
    Authorization: token,
    content_type: :json
  )
end

.widget_url(server_id, style = 'shield') ⇒ Object

Make a widget picture URL from server ID



350
351
352
# File 'lib/onyxcord/rest/client.rb', line 350

def widget_url(server_id, style = 'shield')
  "#{api_base}/guilds/#{server_id}/widget.png?style=#{style}"
end