Module: Rubycord::API

Defined in:
lib/rubycord/api.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/v9"
CDN_URL =

The URL of Discord's CDN

"https://cdn.discordapp.com"

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



220
221
222
# File 'lib/rubycord/api.rb', line 220

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

.acknowledge_message(token, channel_id, message_id) ⇒ Object

Acknowledge that a message has been received The last acknowledged message will be sent in the ready packet, so this is an easy way to catch up on messages



296
297
298
299
300
301
302
303
304
305
# File 'lib/rubycord/api.rb', line 296

def acknowledge_message(token, channel_id, message_id)
  request(
    :channels_cid_messages_mid_ack,
    nil, # This endpoint is unavailable for bot accounts and thus isn't subject to its rate limit requirements.
    :post,
    "#{api_base}/channels/#{channel_id}/messages/#{message_id}/ack",
    nil,
    Authorization: token
  )
end

.api_baseString

Returns the currently used API base URL.

Returns:

  • (String)

    the currently used API base URL.



18
19
20
# File 'lib/rubycord/api.rb', line 18

def api_base
  @api_base || APIBASE
end

.api_base=(value) ⇒ Object

Sets the API base URL to something.



23
24
25
# File 'lib/rubycord/api.rb', line 23

def api_base=(value)
  @api_base = value
end

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

Make an icon URL from application and icon IDs



190
191
192
# File 'lib/rubycord/api.rb', line 190

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



215
216
217
# File 'lib/rubycord/api.rb', line 215

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

Make a banner URL from server and banner IDs



205
206
207
# File 'lib/rubycord/api.rb', line 205

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:



33
34
35
# File 'lib/rubycord/api.rb', line 33

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=.



38
39
40
# File 'lib/rubycord/api.rb', line 38

def bot_name=(value)
  @bot_name = value
end

.cdn_urlString

Returns the currently used CDN url.

Returns:

  • (String)

    the currently used CDN url



28
29
30
# File 'lib/rubycord/api.rb', line 28

def cdn_url
  @cdn_url || CDN_URL
end

.create_oauth_application(token, name, redirect_uris) ⇒ Object

Create an OAuth application



257
258
259
260
261
262
263
264
265
266
267
# File 'lib/rubycord/api.rb', line 257

def create_oauth_application(token, name, redirect_uris)
  request(
    :oauth2_applications,
    nil,
    :post,
    "#{api_base}/oauth2/applications",
    {name: name, redirect_uris: redirect_uris}.to_json,
    Authorization: token,
    content_type: :json
  )
end

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

Make an emoji icon URL from emoji ID



210
211
212
# File 'lib/rubycord/api.rb', line 210

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

.gateway(token) ⇒ Object

Get the gateway to be used



308
309
310
311
312
313
314
315
316
# File 'lib/rubycord/api.rb', line 308

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



320
321
322
323
324
325
326
327
328
# File 'lib/rubycord/api.rb', line 320

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.



161
162
163
164
165
166
# File 'lib/rubycord/api.rb', line 161

def handle_preemptive_rl(headers, mutex, key)
  Rubycord::LOGGER.ratelimit "RL bucket depletion detected! Date: #{headers[:date]} Reset: #{headers[:x_ratelimit_reset]}"
  delta = headers[:x_ratelimit_reset_after].to_f
  Rubycord::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



185
186
187
# File 'lib/rubycord/api.rb', line 185

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

.login(email, password) ⇒ Object

Login to the server



233
234
235
236
237
238
239
240
241
242
# File 'lib/rubycord/api.rb', line 233

def (email, password)
  request(
    :auth_login,
    nil,
    :post,
    "#{api_base}/auth/login",
    email: email,
    password: password
  )
end

.logout(token) ⇒ Object

Logout from the server



245
246
247
248
249
250
251
252
253
254
# File 'lib/rubycord/api.rb', line 245

def logout(token)
  request(
    :auth_logout,
    nil,
    :post,
    "#{api_base}/auth/logout",
    nil,
    Authorization: token
  )
end

.mutex_wait(mutex) ⇒ Object

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



70
71
72
73
# File 'lib/rubycord/api.rb', line 70

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

.oauth_application(token) ⇒ Object

Get the bot's OAuth application's information



283
284
285
286
287
288
289
290
291
# File 'lib/rubycord/api.rb', line 283

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

.raw_request(type, attributes) ⇒ Object

Performs a RestClient request.

Parameters:

  • type (Symbol)

    The type of HTTP request to use.

  • attributes (Array)

    The attributes for the request.



78
79
80
81
82
83
84
85
86
87
88
# File 'lib/rubycord/api.rb', line 78

def raw_request(type, attributes)
  RestClient.send(type, *attributes)
rescue RestClient::Forbidden => e
  # HACK: for #request, dynamically inject restclient's response into NoPermission - this allows us to rate limit
  noprm = Rubycord::Errors::NoPermission.new
  noprm.define_singleton_method(:_rc_response) { e.response }
  raise noprm, "The bot doesn't have the required permission to do this!"
rescue RestClient::BadGateway
  Rubycord::LOGGER.warn("Got a 502 while sending a request! Not a big deal, retrying the request")
  retry
end

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

Make an API request, including rate limit handling.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/rubycord/api.rb', line 91

def request(key, major_parameter, type, *attributes)
  # Add a custom user agent
  attributes.last[:user_agent] = user_agent if attributes.last.is_a? Hash

  # The most recent Discord rate limit requirements require the support of major parameters, where a particular route
  # and major parameter combination (*not* the HTTP method) uniquely identifies a RL bucket.
  key = [key, major_parameter].freeze

  begin
    mutex = @mutexes[key] ||= Mutex.new

    # Lock and unlock, i.e. wait for the mutex to unlock and don't do anything with it afterwards
    mutex_wait(mutex)

    # If the global mutex happens to be locked right now, wait for that as well.
    mutex_wait(@global_mutex) if @global_mutex.locked?

    response = nil
    begin
      response = raw_request(type, attributes)
    rescue RestClient::Exception => e
      response = e.response

      if response.body && !e.is_a?(RestClient::TooManyRequests)
        data = JSON.parse(response.body)
        err_klass = Rubycord::Errors.error_class_for(data["code"] || 0)
        e = err_klass.new(data["message"], data["errors"])

        Rubycord::LOGGER.error(e.full_message)
      end

      raise e
    rescue Rubycord::Errors::NoPermission => e
      if e.respond_to?(:_rc_response)
        response = e._rc_response
      else
        Rubycord::LOGGER.warn("NoPermission doesn't respond_to? _rc_response!")
      end

      raise e
    ensure
      if response
        handle_preemptive_rl(response.headers, mutex, key) if response.headers[:x_ratelimit_remaining] == "0" && !mutex.locked?
      else
        Rubycord::LOGGER.ratelimit("Response was nil before trying to preemptively rate limit!")
      end
    end
  rescue RestClient::TooManyRequests => e
    # If the 429 is from the global RL, then we have to use the global mutex instead.
    mutex = @global_mutex if e.response.headers[:x_ratelimit_global] == "true"

    unless mutex.locked?
      response = JSON.parse(e.response)
      wait_seconds = response["retry_after"] ? response["retry_after"].to_f : e.response.headers[:retry_after].to_i
      Rubycord::LOGGER.ratelimit("Locking RL mutex (key: #{key}) for #{wait_seconds} seconds due to Discord rate limiting")
      trace("429 #{key.join(" ")}")

      # Wait the required time synchronized by the mutex (so other incoming requests have to wait) but only do it if
      # the mutex isn't locked already so it will only ever wait once
      sync_wait(wait_seconds, mutex)
    end

    retry
  end

  response
end

.reset_mutexesObject

Resets all rate limit mutexes



59
60
61
62
# File 'lib/rubycord/api.rb', line 59

def reset_mutexes
  @mutexes = {}
  @global_mutex = Mutex.new
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:



228
229
230
# File 'lib/rubycord/api.rb', line 228

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

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

Make a splash URL from server and splash IDs



200
201
202
# File 'lib/rubycord/api.rb', line 200

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

.sync_wait(time, mutex) ⇒ Object

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



65
66
67
# File 'lib/rubycord/api.rb', line 65

def sync_wait(time, mutex)
  mutex.synchronize { sleep time }
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.



171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/rubycord/api.rb', line 171

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

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

  caller.each do |str|
    Rubycord::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



45
46
47
# File 'lib/rubycord/api.rb', line 45

def trace=(value)
  @trace = value
end

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

Change an OAuth application's properties



270
271
272
273
274
275
276
277
278
279
280
# File 'lib/rubycord/api.rb', line 270

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 rubycord.



50
51
52
53
54
55
56
# File 'lib/rubycord/api.rb', line 50

def user_agent
  # This particular string is required by the Discord devs.
  required = "DiscordBot (https://github.com/dakurei-gems/rubycord, v#{Rubycord::VERSION})"
  @bot_name ||= ""

  "#{required} rest-client/#{RestClient::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} rubycord/#{Rubycord::VERSION} #{@bot_name}"
end

.validate_token(token) ⇒ Object

Validate a token (this request will fail if the token is invalid)



331
332
333
334
335
336
337
338
339
340
341
# File 'lib/rubycord/api.rb', line 331

def validate_token(token)
  request(
    :auth_login,
    nil,
    :post,
    "#{api_base}/auth/login",
    {}.to_json,
    Authorization: token,
    content_type: :json
  )
end

.voice_regions(token) ⇒ Object

Get a list of available voice regions



344
345
346
347
348
349
350
351
352
353
# File 'lib/rubycord/api.rb', line 344

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



195
196
197
# File 'lib/rubycord/api.rb', line 195

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