Class: Async::Discord::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/async/discord/client.rb

Overview

Async HTTP client for the Discord REST API.

Authenticated with a Bot token. All methods are fiber-safe and run naturally inside Falcon's async reactor.

client = Async::Discord::Client.new(token: "MTk...")
client.api.channels("123").messages.post(content: "hello")
client.api.users("@me").get

Constant Summary collapse

DEFAULT_BASE_URL =
"https://discord.com"
DEFAULT_MAX_RETRIES =

Retry defaults

3
DEFAULT_RETRY_BASE =
0.5
DEFAULT_MAX_RETRY_DELAY =
30
RATE_LIMIT_STATUS =

Status codes eligible for retry

429
GATEWAY_ERROR_STATUSES =
[502, 503, 504].freeze
DEFAULT_RESPONSE_SIZE_LIMIT =

Response size limits (bytes)

50 * 1024 * 1024
DEFAULT_ERROR_RESPONSE_SIZE_LIMIT =

50 MiB

512 * 1024

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token:, base_url: DEFAULT_BASE_URL, max_retries: DEFAULT_MAX_RETRIES, retry_base_delay: DEFAULT_RETRY_BASE, max_retry_delay: DEFAULT_MAX_RETRY_DELAY, response_size_limit: DEFAULT_RESPONSE_SIZE_LIMIT, error_response_size_limit: DEFAULT_ERROR_RESPONSE_SIZE_LIMIT) ⇒ Client

Returns a new instance of Client.



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/async/discord/client.rb', line 41

def initialize(token:, base_url: DEFAULT_BASE_URL,
               max_retries: DEFAULT_MAX_RETRIES,
               retry_base_delay: DEFAULT_RETRY_BASE,
               max_retry_delay: DEFAULT_MAX_RETRY_DELAY,
               response_size_limit: DEFAULT_RESPONSE_SIZE_LIMIT,
               error_response_size_limit: DEFAULT_ERROR_RESPONSE_SIZE_LIMIT)
  @token                     = token
  @base                      = base_url
  @max_retries               = max_retries
  @retry_base_delay          = retry_base_delay
  @max_retry_delay           = max_retry_delay
  @response_size_limit       = response_size_limit
  @error_response_size_limit = error_response_size_limit
  @headers = [
    ["authorization", "Bot #{token}"],
    ["content-type",  "application/json"],
    ["user-agent",    "AsyncDiscord (https://github.com/general-intelligence-systems/async-matrix, 1.0)"]
  ]
end

Instance Attribute Details

#tokenObject (readonly)

512 KiB



39
40
41
# File 'lib/async/discord/client.rb', line 39

def token
  @token
end

Instance Method Details

#apiObject

Returns a Gateway that provides method-chained access to every Discord HTTP API endpoint. Chains are validated against the official OpenAPI path tree and terminated by .get(), .post(), .put(), .patch(), or .delete().

client.api.channels("123").messages.post(content: "hello")
client.api.guilds("789").get
client.api.users("@me").get


72
73
74
# File 'lib/async/discord/client.rb', line 72

def api
  Api::Gateway.new(self)
end

#closeObject



90
91
92
93
# File 'lib/async/discord/client.rb', line 90

def close
  @internet&.close
  @internet = nil
end

#get(path, max_retries: nil) ⇒ Object

── Low-level HTTP ────────────────────────────────────────



78
79
80
# File 'lib/async/discord/client.rb', line 78

def get(path, max_retries: nil)
  request("GET", path, nil, max_retries: max_retries)
end

#post(path, body = {}, max_retries: nil) ⇒ Object



82
83
84
# File 'lib/async/discord/client.rb', line 82

def post(path, body = {}, max_retries: nil)
  request("POST", path, body, max_retries: max_retries)
end

#put(path, body = {}, max_retries: nil) ⇒ Object



86
87
88
# File 'lib/async/discord/client.rb', line 86

def put(path, body = {}, max_retries: nil)
  request("PUT", path, body, max_retries: max_retries)
end

#request(method, path, body = nil, max_retries: nil) ⇒ Object

General-purpose request method supporting any HTTP method.



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
# File 'lib/async/discord/client.rb', line 96

def request(method, path, body = nil, max_retries: nil)
  url = "#{@base}#{path}"
  json_body = body ? JSON.generate(body) : nil
  effective_max_retries = max_retries || @max_retries

  Console.debug(self) { "#{method} #{path}" }

  attempt = 0
  loop do
    response = internet.call(method, url, @headers, json_body)
    status   = response.status

    if (200..299).cover?(status)
      payload = read_limited(response, @response_size_limit)
      return payload && !payload.empty? ? JSON.parse(payload) : {}
    end

    attempt += 1

    if attempt <= effective_max_retries && retryable_status?(status)
      delay = compute_retry_delay(status, response, attempt)
      Console.warn(self) {
        "#{method} #{path} returned #{status}, retry #{attempt}/#{effective_max_retries} in #{delay.round(2)}s"
      }
      response.close if response.respond_to?(:close)
      sleep(delay)
      next
    end

    payload = read_limited(response, @error_response_size_limit)
    parsed = begin; JSON.parse(payload || "{}"); rescue; {} end
    discord_code = parsed["code"]
    discord_msg  = parsed["message"] || payload.to_s[0..200]

    Console.error(self) { "Discord API #{status}: #{discord_code}#{discord_msg}" }

    error_class = case status
                  when 401 then AuthError
                  when 429 then RateLimitError
                  when 400..499 then ApiError
                  else ServerError
                  end

    raise error_class.new(
      discord_code.to_s,
      discord_msg,
      status: status
    )
  end
end