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.



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

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



41
42
43
# File 'lib/async/discord/client.rb', line 41

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


74
75
76
# File 'lib/async/discord/client.rb', line 74

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

#closeObject



92
93
94
95
# File 'lib/async/discord/client.rb', line 92

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

#get(path, max_retries: nil) ⇒ Object

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



80
81
82
# File 'lib/async/discord/client.rb', line 80

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

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



84
85
86
# File 'lib/async/discord/client.rb', line 84

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

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



88
89
90
# File 'lib/async/discord/client.rb', line 88

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.



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

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