discord_rda 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +398 -0
- data/lib/discord_rda/bot.rb +842 -0
- data/lib/discord_rda/cache/configurable_cache.rb +283 -0
- data/lib/discord_rda/cache/entity_cache.rb +184 -0
- data/lib/discord_rda/cache/memory_store.rb +143 -0
- data/lib/discord_rda/cache/redis_store.rb +136 -0
- data/lib/discord_rda/cache/store.rb +56 -0
- data/lib/discord_rda/connection/gateway_client.rb +383 -0
- data/lib/discord_rda/connection/invalid_bucket.rb +205 -0
- data/lib/discord_rda/connection/rate_limiter.rb +280 -0
- data/lib/discord_rda/connection/request_queue.rb +340 -0
- data/lib/discord_rda/connection/reshard_manager.rb +328 -0
- data/lib/discord_rda/connection/rest_client.rb +316 -0
- data/lib/discord_rda/connection/rest_proxy.rb +165 -0
- data/lib/discord_rda/connection/scalable_rest_client.rb +526 -0
- data/lib/discord_rda/connection/shard_manager.rb +223 -0
- data/lib/discord_rda/core/async_runtime.rb +108 -0
- data/lib/discord_rda/core/configuration.rb +194 -0
- data/lib/discord_rda/core/logger.rb +188 -0
- data/lib/discord_rda/core/snowflake.rb +121 -0
- data/lib/discord_rda/entity/attachment.rb +88 -0
- data/lib/discord_rda/entity/base.rb +103 -0
- data/lib/discord_rda/entity/channel.rb +446 -0
- data/lib/discord_rda/entity/channel_builder.rb +280 -0
- data/lib/discord_rda/entity/color.rb +253 -0
- data/lib/discord_rda/entity/embed.rb +221 -0
- data/lib/discord_rda/entity/emoji.rb +89 -0
- data/lib/discord_rda/entity/factory.rb +99 -0
- data/lib/discord_rda/entity/guild.rb +619 -0
- data/lib/discord_rda/entity/member.rb +263 -0
- data/lib/discord_rda/entity/message.rb +405 -0
- data/lib/discord_rda/entity/message_builder.rb +369 -0
- data/lib/discord_rda/entity/role.rb +157 -0
- data/lib/discord_rda/entity/support.rb +294 -0
- data/lib/discord_rda/entity/user.rb +231 -0
- data/lib/discord_rda/entity/value_objects.rb +263 -0
- data/lib/discord_rda/event/auto_moderation.rb +294 -0
- data/lib/discord_rda/event/base.rb +986 -0
- data/lib/discord_rda/event/bus.rb +225 -0
- data/lib/discord_rda/event/scheduled_event.rb +257 -0
- data/lib/discord_rda/hot_reload_manager.rb +303 -0
- data/lib/discord_rda/interactions/application_command.rb +436 -0
- data/lib/discord_rda/interactions/command_system.rb +484 -0
- data/lib/discord_rda/interactions/components.rb +464 -0
- data/lib/discord_rda/interactions/interaction.rb +553 -0
- data/lib/discord_rda/plugin/analytics_plugin.rb +528 -0
- data/lib/discord_rda/plugin/base.rb +190 -0
- data/lib/discord_rda/plugin/registry.rb +126 -0
- data/lib/discord_rda/version.rb +5 -0
- data/lib/discord_rda.rb +70 -0
- metadata +302 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DiscordRDA
|
|
4
|
+
# Enhanced scalable REST client inspired by Discordeno.
|
|
5
|
+
# Features: request queues, invalid request bucket, URL simplification, proxy support.
|
|
6
|
+
#
|
|
7
|
+
class ScalableRestClient
|
|
8
|
+
# Discord API base URL
|
|
9
|
+
API_BASE = 'https://discord.com/api/v10'
|
|
10
|
+
|
|
11
|
+
# Rate limit headers
|
|
12
|
+
RATE_LIMIT_REMAINING_HEADER = 'x-ratelimit-remaining'
|
|
13
|
+
RATE_LIMIT_RESET_AFTER_HEADER = 'x-ratelimit-reset-after'
|
|
14
|
+
RATE_LIMIT_GLOBAL_HEADER = 'x-ratelimit-global'
|
|
15
|
+
RATE_LIMIT_BUCKET_HEADER = 'x-ratelimit-bucket'
|
|
16
|
+
RATE_LIMIT_LIMIT_HEADER = 'x-ratelimit-limit'
|
|
17
|
+
|
|
18
|
+
# Major parameters that affect rate limit buckets
|
|
19
|
+
MAJOR_PARAMS = %w[channels guilds webhooks].freeze
|
|
20
|
+
|
|
21
|
+
# @return [Configuration] Configuration instance
|
|
22
|
+
attr_reader :config
|
|
23
|
+
|
|
24
|
+
# @return [Logger] Logger instance
|
|
25
|
+
attr_reader :logger
|
|
26
|
+
|
|
27
|
+
# @return [InvalidRequestBucket] Invalid request bucket
|
|
28
|
+
attr_reader :invalid_bucket
|
|
29
|
+
|
|
30
|
+
# @return [Hash<String, RequestQueue>] Request queues
|
|
31
|
+
attr_reader :queues
|
|
32
|
+
|
|
33
|
+
# @return [Hash<String, Hash>] Rate limited paths
|
|
34
|
+
attr_reader :rate_limited_paths
|
|
35
|
+
|
|
36
|
+
# @return [Boolean] Whether globally rate limited
|
|
37
|
+
attr_accessor :globally_rate_limited
|
|
38
|
+
|
|
39
|
+
# @return [Boolean] Whether processing rate limited paths
|
|
40
|
+
attr_accessor :processing_rate_limited_paths
|
|
41
|
+
|
|
42
|
+
# @return [Integer] Delay before deleting empty queue (ms)
|
|
43
|
+
attr_reader :delete_queue_delay
|
|
44
|
+
|
|
45
|
+
# @return [Integer] Maximum retry count
|
|
46
|
+
attr_reader :max_retry_count
|
|
47
|
+
|
|
48
|
+
# @return [Boolean] Whether using proxy
|
|
49
|
+
attr_reader :is_proxied
|
|
50
|
+
|
|
51
|
+
# @return [String] Proxy base URL
|
|
52
|
+
attr_reader :proxy_base_url
|
|
53
|
+
|
|
54
|
+
# @return [String] Proxy authorization
|
|
55
|
+
attr_reader :proxy_authorization
|
|
56
|
+
|
|
57
|
+
# Initialize the scalable REST client
|
|
58
|
+
# @param config [Configuration] Bot configuration
|
|
59
|
+
# @param logger [Logger] Logger instance
|
|
60
|
+
# @param proxy [Hash] Proxy configuration (base_url, authorization)
|
|
61
|
+
def initialize(config, logger, proxy: nil)
|
|
62
|
+
@config = config
|
|
63
|
+
@logger = logger
|
|
64
|
+
@invalid_bucket = InvalidRequestBucket.new(logger: logger)
|
|
65
|
+
@queues = {}
|
|
66
|
+
@rate_limited_paths = {}
|
|
67
|
+
@globally_rate_limited = false
|
|
68
|
+
@processing_rate_limited_paths = false
|
|
69
|
+
@delete_queue_delay = 60_000
|
|
70
|
+
@max_retry_count = Float::INFINITY
|
|
71
|
+
@mutex = Mutex.new
|
|
72
|
+
@internet = nil
|
|
73
|
+
|
|
74
|
+
# Proxy configuration for horizontal scaling
|
|
75
|
+
if proxy
|
|
76
|
+
@is_proxied = true
|
|
77
|
+
@proxy_base_url = proxy[:base_url]
|
|
78
|
+
@proxy_authorization = proxy[:authorization]
|
|
79
|
+
else
|
|
80
|
+
@is_proxied = false
|
|
81
|
+
@proxy_base_url = API_BASE
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Start the REST client
|
|
86
|
+
# @return [void]
|
|
87
|
+
def start
|
|
88
|
+
@internet = Async::HTTP::Internet.new
|
|
89
|
+
process_rate_limited_paths
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Stop the REST client
|
|
93
|
+
# @return [void]
|
|
94
|
+
def stop
|
|
95
|
+
@internet&.close
|
|
96
|
+
@internet = nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Make a GET request
|
|
100
|
+
# @param route [String] API route
|
|
101
|
+
# @param options [Hash] Request options
|
|
102
|
+
# @return [Hash] Response data
|
|
103
|
+
def get(route, options = {})
|
|
104
|
+
make_request(:get, route, options)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Make a POST request
|
|
108
|
+
# @param route [String] API route
|
|
109
|
+
# @param options [Hash] Request options
|
|
110
|
+
# @return [Hash] Response data
|
|
111
|
+
def post(route, options = {})
|
|
112
|
+
make_request(:post, route, options)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Make a PUT request
|
|
116
|
+
# @param route [String] API route
|
|
117
|
+
# @param options [Hash] Request options
|
|
118
|
+
# @return [Hash] Response data
|
|
119
|
+
def put(route, options = {})
|
|
120
|
+
make_request(:put, route, options)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Make a PATCH request
|
|
124
|
+
# @param route [String] API route
|
|
125
|
+
# @param options [Hash] Request options
|
|
126
|
+
# @return [Hash] Response data
|
|
127
|
+
def patch(route, options = {})
|
|
128
|
+
make_request(:patch, route, options)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Make a DELETE request
|
|
132
|
+
# @param route [String] API route
|
|
133
|
+
# @param options [Hash] Request options
|
|
134
|
+
# @return [Hash] Response data
|
|
135
|
+
def delete(route, options = {})
|
|
136
|
+
make_request(:delete, route, options)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Simplify URL for rate limit bucket identification
|
|
140
|
+
# @param url [String] Full URL
|
|
141
|
+
# @param method [Symbol] HTTP method
|
|
142
|
+
# @return [String] Simplified URL for bucket
|
|
143
|
+
def simplify_url(url, method)
|
|
144
|
+
# Split URL into parts
|
|
145
|
+
parts = url.split('/').reject(&:empty?)
|
|
146
|
+
|
|
147
|
+
# Build simplified URL
|
|
148
|
+
simplified = [method.to_s.upcase]
|
|
149
|
+
|
|
150
|
+
parts.each_with_index do |part, index|
|
|
151
|
+
# Check if this is a major parameter (channels, guilds, webhooks)
|
|
152
|
+
if MAJOR_PARAMS.include?(part)
|
|
153
|
+
simplified << part
|
|
154
|
+
# Keep the ID after major params
|
|
155
|
+
if parts[index + 1] && parts[index + 1] =~ /^\d+$/
|
|
156
|
+
simplified << parts[index + 1]
|
|
157
|
+
end
|
|
158
|
+
elsif part =~ /^\d+$/
|
|
159
|
+
# Replace numeric IDs with 'x' unless after major param
|
|
160
|
+
prev = parts[index - 1]
|
|
161
|
+
simplified << 'x' unless MAJOR_PARAMS.include?(prev)
|
|
162
|
+
else
|
|
163
|
+
simplified << part
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Special handling for reactions
|
|
168
|
+
if url.include?('/reactions/')
|
|
169
|
+
# Simplify reactions path: /reactions/emoji/@me or /reactions/emoji/user_id
|
|
170
|
+
simplified = simplify_reactions_url(simplified)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Special handling for messages
|
|
174
|
+
if url.include?('/messages/')
|
|
175
|
+
# Keep method in front for messages
|
|
176
|
+
simplified = simplify_messages_url(method, parts)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
simplified.join('/')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Check rate limits for a URL or bucket
|
|
183
|
+
# @param url [String] URL or bucket ID
|
|
184
|
+
# @param identifier [String] Queue identifier
|
|
185
|
+
# @return [Integer, false] Milliseconds until reset, or false if not limited
|
|
186
|
+
def check_rate_limits(url, identifier)
|
|
187
|
+
@mutex.synchronize do
|
|
188
|
+
# Check specific URL rate limit
|
|
189
|
+
limited = @rate_limited_paths["#{identifier}#{url}"]
|
|
190
|
+
global = @rate_limited_paths['global']
|
|
191
|
+
now = Time.now.to_f * 1000
|
|
192
|
+
|
|
193
|
+
if limited && now < limited[:reset_timestamp]
|
|
194
|
+
return limited[:reset_timestamp] - now
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if global && now < global[:reset_timestamp]
|
|
198
|
+
return global[:reset_timestamp] - now
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
false
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Process rate limited paths (cleanup loop)
|
|
206
|
+
# @return [void]
|
|
207
|
+
def process_rate_limited_paths
|
|
208
|
+
@mutex.synchronize do
|
|
209
|
+
now = Time.now.to_f * 1000
|
|
210
|
+
|
|
211
|
+
@rate_limited_paths.delete_if do |key, value|
|
|
212
|
+
if value[:reset_timestamp] <= now
|
|
213
|
+
# If it was global, mark as not globally rate limited
|
|
214
|
+
@globally_rate_limited = false if key == 'global'
|
|
215
|
+
true # Delete this entry
|
|
216
|
+
else
|
|
217
|
+
false # Keep this entry
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# If all paths are cleared, stop processing
|
|
222
|
+
if @rate_limited_paths.empty?
|
|
223
|
+
@processing_rate_limited_paths = false
|
|
224
|
+
else
|
|
225
|
+
@processing_rate_limited_paths = true
|
|
226
|
+
# Recheck in 1 second
|
|
227
|
+
Async { sleep(1); process_rate_limited_paths }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Update token in all queues (for token refresh)
|
|
233
|
+
# @param old_token [String] Old token
|
|
234
|
+
# @param new_token [String] New token
|
|
235
|
+
# @return [void]
|
|
236
|
+
def update_token_queues(old_token, new_token)
|
|
237
|
+
@mutex.synchronize do
|
|
238
|
+
old_identifier = "Bearer #{old_token}"
|
|
239
|
+
new_identifier = "Bearer #{new_token}"
|
|
240
|
+
|
|
241
|
+
# Update queues
|
|
242
|
+
@queues.delete_if do |key, queue|
|
|
243
|
+
next false unless key.start_with?(old_identifier)
|
|
244
|
+
|
|
245
|
+
@queues.delete(key)
|
|
246
|
+
queue.identifier = new_identifier
|
|
247
|
+
|
|
248
|
+
new_key = "#{new_identifier}#{queue.url}"
|
|
249
|
+
existing = @queues[new_key]
|
|
250
|
+
|
|
251
|
+
if existing
|
|
252
|
+
# Merge queues
|
|
253
|
+
existing.pending.concat(queue.pending)
|
|
254
|
+
queue.pending.clear
|
|
255
|
+
queue.cleanup
|
|
256
|
+
true # Delete old queue
|
|
257
|
+
else
|
|
258
|
+
@queues[new_key] = queue
|
|
259
|
+
false # Don't delete, we moved it
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Update rate limited paths
|
|
264
|
+
@rate_limited_paths.delete_if do |key, path|
|
|
265
|
+
next false unless key.start_with?(old_identifier)
|
|
266
|
+
|
|
267
|
+
@rate_limited_paths["#{new_identifier}#{path[:url]}"] = path
|
|
268
|
+
|
|
269
|
+
if path[:bucket_id]
|
|
270
|
+
@rate_limited_paths["#{new_identifier}#{path[:bucket_id]}"] = path
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
true # Delete old entry
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
def make_request(method, route, options = {})
|
|
281
|
+
url = simplify_url(route, method)
|
|
282
|
+
identifier = options[:authorization] || "Bot #{@config.token}"
|
|
283
|
+
|
|
284
|
+
# Create queue if doesn't exist
|
|
285
|
+
queue = @mutex.synchronize do
|
|
286
|
+
@queues["#{identifier}#{url}"] ||= RequestQueue.new(
|
|
287
|
+
self,
|
|
288
|
+
url: url,
|
|
289
|
+
identifier: identifier,
|
|
290
|
+
delete_queue_delay: @delete_queue_delay
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Add request to queue
|
|
295
|
+
promise = Async::Condition.new
|
|
296
|
+
|
|
297
|
+
request = {
|
|
298
|
+
method: method,
|
|
299
|
+
route: route,
|
|
300
|
+
body: options[:body],
|
|
301
|
+
headers: options[:headers],
|
|
302
|
+
bucket_id: options[:bucket_id],
|
|
303
|
+
resolve: ->(result) { promise.signal(result) },
|
|
304
|
+
reject: ->(error) { promise.signal(error) }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
queue.make_request(request)
|
|
308
|
+
|
|
309
|
+
# Wait for result
|
|
310
|
+
result = promise.wait
|
|
311
|
+
|
|
312
|
+
# Check if it's an error
|
|
313
|
+
raise result[:error] if result.is_a?(Hash) && result[:error]
|
|
314
|
+
|
|
315
|
+
result
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def send_request(request)
|
|
319
|
+
full_url = "#{@proxy_base_url}/v#{@config.api_version}#{request[:route]}"
|
|
320
|
+
|
|
321
|
+
# Build headers
|
|
322
|
+
headers = build_headers(request[:headers])
|
|
323
|
+
|
|
324
|
+
# Make request
|
|
325
|
+
response = make_http_request(request[:method], full_url, request[:body], headers)
|
|
326
|
+
|
|
327
|
+
# Process response
|
|
328
|
+
process_response(response, request)
|
|
329
|
+
rescue => e
|
|
330
|
+
@logger.error('Request failed', error: e, route: request[:route])
|
|
331
|
+
raise
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def make_http_request(method, url, body, headers)
|
|
335
|
+
body_json = body ? Oj.dump(body, mode: :compat) : nil
|
|
336
|
+
|
|
337
|
+
case method
|
|
338
|
+
when :get
|
|
339
|
+
@internet.get(url, headers)
|
|
340
|
+
when :post
|
|
341
|
+
@internet.post(url, headers, body_json)
|
|
342
|
+
when :put
|
|
343
|
+
@internet.put(url, headers, body_json)
|
|
344
|
+
when :patch
|
|
345
|
+
@internet.patch(url, headers, body_json)
|
|
346
|
+
when :delete
|
|
347
|
+
@internet.delete(url, headers)
|
|
348
|
+
else
|
|
349
|
+
raise ArgumentError, "Unknown HTTP method: #{method}"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def process_response(response, request)
|
|
354
|
+
status = response.status
|
|
355
|
+
body = response.read
|
|
356
|
+
data = body ? Oj.load(body) : nil
|
|
357
|
+
|
|
358
|
+
# Handle invalid request tracking
|
|
359
|
+
@invalid_bucket.handle_request(status)
|
|
360
|
+
|
|
361
|
+
# Process rate limit headers
|
|
362
|
+
bucket_id = process_headers(request[:route], response.headers, request[:identifier])
|
|
363
|
+
|
|
364
|
+
# Update queue with rate limit info
|
|
365
|
+
url = simplify_url(request[:route], request[:method])
|
|
366
|
+
queue = @queues["#{request[:identifier]}#{url}"]
|
|
367
|
+
|
|
368
|
+
if queue
|
|
369
|
+
queue.handle_completed_request(
|
|
370
|
+
max: response.headers[RATE_LIMIT_LIMIT_HEADER]&.to_i,
|
|
371
|
+
remaining: response.headers[RATE_LIMIT_REMAINING_HEADER]&.to_i,
|
|
372
|
+
interval: response.headers[RATE_LIMIT_RESET_AFTER_HEADER]&.to_f&.*(1000)
|
|
373
|
+
)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
case status
|
|
377
|
+
when 200..299
|
|
378
|
+
data
|
|
379
|
+
when 400
|
|
380
|
+
raise BadRequestError.new(status, data)
|
|
381
|
+
when 401
|
|
382
|
+
raise UnauthorizedError.new(status, data)
|
|
383
|
+
when 403
|
|
384
|
+
raise ForbiddenError.new(status, data)
|
|
385
|
+
when 404
|
|
386
|
+
raise NotFoundError.new(status, data)
|
|
387
|
+
when 429
|
|
388
|
+
retry_after = data['retry_after'] || response.headers['retry-after']&.to_f
|
|
389
|
+
raise RateLimitedError.new(status, data, retry_after: retry_after)
|
|
390
|
+
when 500..599
|
|
391
|
+
raise ServerError.new(status, data)
|
|
392
|
+
else
|
|
393
|
+
raise APIError.new(status, data)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def process_headers(url, headers, identifier)
|
|
398
|
+
remaining = headers[RATE_LIMIT_REMAINING_HEADER]
|
|
399
|
+
retry_after = headers['Retry-After'] || headers[RATE_LIMIT_RESET_AFTER_HEADER]
|
|
400
|
+
reset = Time.now.to_f * 1000 + retry_after.to_f * 1000 if retry_after
|
|
401
|
+
global = headers[RATE_LIMIT_GLOBAL_HEADER]
|
|
402
|
+
bucket_id = headers[RATE_LIMIT_BUCKET_HEADER]
|
|
403
|
+
identifier ||= "Bot #{@config.token}"
|
|
404
|
+
|
|
405
|
+
rate_limited = false
|
|
406
|
+
|
|
407
|
+
# If no remaining, mark as rate limited
|
|
408
|
+
if remaining == '0'
|
|
409
|
+
rate_limited = true
|
|
410
|
+
|
|
411
|
+
@mutex.synchronize do
|
|
412
|
+
@rate_limited_paths["#{identifier}#{url}"] = {
|
|
413
|
+
url: url,
|
|
414
|
+
reset_timestamp: reset,
|
|
415
|
+
bucket_id: bucket_id
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if bucket_id
|
|
419
|
+
@rate_limited_paths["#{identifier}#{bucket_id}"] = {
|
|
420
|
+
url: url,
|
|
421
|
+
reset_timestamp: reset,
|
|
422
|
+
bucket_id: bucket_id
|
|
423
|
+
}
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Handle global rate limit
|
|
429
|
+
if global
|
|
430
|
+
retry_ms = headers['retry-after'].to_f * 1000
|
|
431
|
+
global_reset = Time.now.to_f * 1000 + retry_ms
|
|
432
|
+
|
|
433
|
+
@globally_rate_limited = true
|
|
434
|
+
rate_limited = true
|
|
435
|
+
|
|
436
|
+
@mutex.synchronize do
|
|
437
|
+
@rate_limited_paths['global'] = {
|
|
438
|
+
url: 'global',
|
|
439
|
+
reset_timestamp: global_reset,
|
|
440
|
+
bucket_id: bucket_id
|
|
441
|
+
}
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
Async { sleep(retry_ms / 1000.0); @globally_rate_limited = false }
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Start processing rate limited paths if needed
|
|
448
|
+
if rate_limited && !@processing_rate_limited_paths
|
|
449
|
+
process_rate_limited_paths
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
bucket_id if rate_limited
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def build_headers(additional = {})
|
|
456
|
+
base = {
|
|
457
|
+
'User-Agent' => "DiscordRDA (https://github.com/juliaklee/discord_rda, #{VERSION})",
|
|
458
|
+
'Content-Type' => 'application/json',
|
|
459
|
+
'Accept' => 'application/json'
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
# Add authorization
|
|
463
|
+
if @is_proxied && @proxy_authorization
|
|
464
|
+
base['Authorization'] = @proxy_authorization
|
|
465
|
+
else
|
|
466
|
+
base['Authorization'] = "Bot #{@config.token}"
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
base.merge(additional || {})
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def simplify_reactions_url(parts)
|
|
473
|
+
# Convert reactions/emoji to reactions/x
|
|
474
|
+
parts.map { |p| p =~ /^[\w-]+$/ && p.length > 10 ? 'x' : p }
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def simplify_messages_url(method, parts)
|
|
478
|
+
result = [method.to_s.upcase]
|
|
479
|
+
|
|
480
|
+
parts.each_with_index do |part, index|
|
|
481
|
+
if MAJOR_PARAMS.include?(part)
|
|
482
|
+
result << part
|
|
483
|
+
# Keep ID after major param
|
|
484
|
+
result << parts[index + 1] if parts[index + 1] && parts[index + 1] =~ /^\d+$/
|
|
485
|
+
elsif part == 'messages' && parts[index + 1] =~ /^\d+$/
|
|
486
|
+
result << part << 'x'
|
|
487
|
+
elsif part =~ /^\d+$/
|
|
488
|
+
prev = parts[index - 1]
|
|
489
|
+
result << 'x' unless MAJOR_PARAMS.include?(prev)
|
|
490
|
+
else
|
|
491
|
+
result << part
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
result.uniq
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Error classes
|
|
499
|
+
class APIError < StandardError
|
|
500
|
+
attr_reader :status, :data
|
|
501
|
+
|
|
502
|
+
def initialize(status, data)
|
|
503
|
+
@status = status
|
|
504
|
+
@data = data || {}
|
|
505
|
+
message = @data['message'] || 'Unknown error'
|
|
506
|
+
super("API Error #{status}: #{message}")
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
class BadRequestError < APIError; end
|
|
511
|
+
class UnauthorizedError < APIError; end
|
|
512
|
+
class ForbiddenError < APIError; end
|
|
513
|
+
class NotFoundError < APIError; end
|
|
514
|
+
|
|
515
|
+
class RateLimitedError < APIError
|
|
516
|
+
attr_reader :retry_after
|
|
517
|
+
|
|
518
|
+
def initialize(status, data, retry_after: nil)
|
|
519
|
+
super(status, data)
|
|
520
|
+
@retry_after = retry_after || data['retry_after'] || 1.0
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
class ServerError < APIError; end
|
|
525
|
+
end
|
|
526
|
+
end
|