onyxcord 2.0.0 → 2.0.6
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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +55 -0
- data/README.md +64 -1
- data/lib/onyxcord/api.rb +58 -55
- data/lib/onyxcord/application_commands/command.rb +102 -0
- data/lib/onyxcord/application_commands/context.rb +99 -0
- data/lib/onyxcord/application_commands/option.rb +80 -0
- data/lib/onyxcord/application_commands/registry.rb +50 -0
- data/lib/onyxcord/application_commands.rb +11 -0
- data/lib/onyxcord/async/runtime.rb +30 -0
- data/lib/onyxcord/bot.rb +47 -15
- data/lib/onyxcord/data/interaction.rb +4 -0
- data/lib/onyxcord/event_executor.rb +56 -6
- data/lib/onyxcord/gateway.rb +15 -15
- data/lib/onyxcord/http.rb +115 -0
- data/lib/onyxcord/json.rb +49 -0
- data/lib/onyxcord/rate_limiter/async_rest.rb +149 -0
- data/lib/onyxcord/version.rb +1 -1
- data/lib/onyxcord/webhooks/version.rb +1 -1
- data/lib/onyxcord/websocket.rb +2 -15
- metadata +10 -4
- data/.devcontainer/Dockerfile +0 -13
- data/.devcontainer/devcontainer.json +0 -29
- data/.devcontainer/postcreate.sh +0 -4
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'onyxcord/async/runtime'
|
|
5
|
+
|
|
6
|
+
module OnyxCord
|
|
7
|
+
module RateLimiter
|
|
8
|
+
class AsyncRest
|
|
9
|
+
DEFAULT_ENTRY_TTL = 3600
|
|
10
|
+
DEFAULT_PRUNE_INTERVAL = 100
|
|
11
|
+
|
|
12
|
+
def initialize(clock: -> { Time.now }, entry_ttl: DEFAULT_ENTRY_TTL, prune_interval: DEFAULT_PRUNE_INTERVAL)
|
|
13
|
+
@route_buckets = {}
|
|
14
|
+
@bucket_locks = {}
|
|
15
|
+
@bucket_last_used = {}
|
|
16
|
+
@global_lock = Mutex.new
|
|
17
|
+
@clock = clock
|
|
18
|
+
@entry_ttl = entry_ttl
|
|
19
|
+
@prune_interval = prune_interval
|
|
20
|
+
@requests_since_prune = 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def before_request(route, major_parameter)
|
|
24
|
+
wait_for(mutex_for(route, major_parameter))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def record_response(route, major_parameter, headers)
|
|
28
|
+
headers = normalize_headers(headers)
|
|
29
|
+
bucket = headers[:x_ratelimit_bucket]
|
|
30
|
+
key = route_key(route, major_parameter)
|
|
31
|
+
touch(key)
|
|
32
|
+
|
|
33
|
+
if bucket
|
|
34
|
+
bucket = bucket_key(bucket, major_parameter)
|
|
35
|
+
@route_buckets[key] = bucket
|
|
36
|
+
touch(bucket)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
return unless headers[:x_ratelimit_remaining] == '0'
|
|
40
|
+
|
|
41
|
+
wait_seconds = headers[:x_ratelimit_reset_after].to_f
|
|
42
|
+
return unless wait_seconds.positive?
|
|
43
|
+
|
|
44
|
+
async_wait(wait_seconds, mutex_for(route, major_parameter))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_rate_limit(route, major_parameter, response)
|
|
48
|
+
headers = normalize_headers(response.headers)
|
|
49
|
+
wait_seconds = retry_after(response, headers)
|
|
50
|
+
|
|
51
|
+
return unless wait_seconds.positive?
|
|
52
|
+
|
|
53
|
+
if headers[:x_ratelimit_global] == 'true' || headers[:x_ratelimit_scope] == 'global'
|
|
54
|
+
global_wait(wait_seconds)
|
|
55
|
+
else
|
|
56
|
+
async_wait(wait_seconds, mutex_for(route, major_parameter))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def stats
|
|
61
|
+
{
|
|
62
|
+
route_buckets: @route_buckets.size,
|
|
63
|
+
bucket_locks: @bucket_locks.size,
|
|
64
|
+
tracked_keys: @bucket_last_used.size
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def prune!
|
|
69
|
+
return 0 unless @entry_ttl
|
|
70
|
+
|
|
71
|
+
cutoff = @clock.call - @entry_ttl
|
|
72
|
+
stale_keys = @bucket_last_used.select { |_, last_used| last_used < cutoff }.keys
|
|
73
|
+
|
|
74
|
+
stale_keys.each do |key|
|
|
75
|
+
@bucket_locks.delete(key)
|
|
76
|
+
@bucket_last_used.delete(key)
|
|
77
|
+
@route_buckets.delete(key)
|
|
78
|
+
@route_buckets.delete_if { |_, bucket_key| bucket_key == key }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@requests_since_prune = 0
|
|
82
|
+
stale_keys.length
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def mutex_for(route, major_parameter)
|
|
88
|
+
key = resolved_key(route, major_parameter)
|
|
89
|
+
touch(key)
|
|
90
|
+
@bucket_locks[key] ||= Mutex.new
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def resolved_key(route, major_parameter)
|
|
94
|
+
@route_buckets[route_key(route, major_parameter)] || route_key(route, major_parameter)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def route_key(route, major_parameter)
|
|
98
|
+
[route, major_parameter].freeze
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def bucket_key(bucket, major_parameter)
|
|
102
|
+
[:bucket, bucket, major_parameter].freeze
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def retry_after(response, headers)
|
|
106
|
+
body = response.respond_to?(:body) ? response.body : response.to_s
|
|
107
|
+
if body && !body.empty?
|
|
108
|
+
data = JSON.parse(body)
|
|
109
|
+
return data['retry_after'].to_f if data['retry_after']
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
(headers[:retry_after] || 0).to_f
|
|
113
|
+
rescue JSON::ParserError
|
|
114
|
+
(headers[:retry_after] || 0).to_f
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def normalize_headers(headers)
|
|
118
|
+
headers.each_with_object({}) do |(key, value), memo|
|
|
119
|
+
memo[key.to_s.tr('-', '_').downcase.to_sym] = value.to_s
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def touch(key)
|
|
124
|
+
@bucket_last_used[key] = @clock.call
|
|
125
|
+
prune_if_needed
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def prune_if_needed
|
|
129
|
+
return unless @prune_interval
|
|
130
|
+
|
|
131
|
+
@requests_since_prune += 1
|
|
132
|
+
prune! if @requests_since_prune >= @prune_interval
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def wait_for(mutex)
|
|
136
|
+
mutex.lock
|
|
137
|
+
mutex.unlock
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def async_wait(time, mutex)
|
|
141
|
+
mutex.synchronize { OnyxCord::AsyncRuntime.sleep(time) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def global_wait(time)
|
|
145
|
+
OnyxCord::AsyncRuntime.sleep(time)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/onyxcord/version.rb
CHANGED
data/lib/onyxcord/websocket.rb
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'async'
|
|
3
|
+
require 'onyxcord/async/runtime'
|
|
4
4
|
require 'async/http/endpoint'
|
|
5
5
|
require 'async/websocket/client'
|
|
6
6
|
|
|
7
7
|
module OnyxCord
|
|
8
|
-
# Wrapper around async-websocket that provides the same callback-based
|
|
9
|
-
# interface used by the Gateway and Voice subsystems. This replaces the
|
|
10
|
-
# previous websocket-client-simple implementation.
|
|
11
8
|
class WebSocket
|
|
12
|
-
# @return [Boolean] whether the connection is currently open.
|
|
13
9
|
attr_reader :connected
|
|
14
10
|
|
|
15
11
|
alias_method :connected?, :connected
|
|
16
12
|
|
|
17
|
-
# Creates a new WebSocket wrapper.
|
|
18
|
-
# @param host [String] The `wss://` endpoint URL to connect to.
|
|
19
|
-
# @param open_handler [Proc] Called once the connection is established.
|
|
20
|
-
# @param message_handler [Proc] Called for every text frame received.
|
|
21
|
-
# @param error_handler [Proc] Called when an error occurs.
|
|
22
|
-
# @param close_handler [Proc] Called when the connection closes.
|
|
23
13
|
def initialize(host, open_handler, message_handler, error_handler, close_handler)
|
|
24
14
|
@host = host
|
|
25
15
|
@open_handler = open_handler
|
|
@@ -33,8 +23,6 @@ module OnyxCord
|
|
|
33
23
|
connect
|
|
34
24
|
end
|
|
35
25
|
|
|
36
|
-
# Send a text message over the WebSocket.
|
|
37
|
-
# @param data [String, Hash] Data to send. Hashes are JSON-encoded automatically.
|
|
38
26
|
def send(data)
|
|
39
27
|
return unless @connection
|
|
40
28
|
|
|
@@ -45,7 +33,6 @@ module OnyxCord
|
|
|
45
33
|
@error_handler&.call(e)
|
|
46
34
|
end
|
|
47
35
|
|
|
48
|
-
# Cleanly close the connection.
|
|
49
36
|
def close
|
|
50
37
|
@connected = false
|
|
51
38
|
@connection&.close
|
|
@@ -58,7 +45,7 @@ module OnyxCord
|
|
|
58
45
|
def connect
|
|
59
46
|
endpoint = Async::HTTP::Endpoint.parse(@host)
|
|
60
47
|
|
|
61
|
-
|
|
48
|
+
@task = OnyxCord::AsyncRuntime.async do
|
|
62
49
|
Async::WebSocket::Client.connect(endpoint) do |connection|
|
|
63
50
|
@connection = connection
|
|
64
51
|
@connected = true
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: onyxcord
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gustavo Silva
|
|
@@ -365,9 +365,6 @@ executables: []
|
|
|
365
365
|
extensions: []
|
|
366
366
|
extra_rdoc_files: []
|
|
367
367
|
files:
|
|
368
|
-
- ".devcontainer/Dockerfile"
|
|
369
|
-
- ".devcontainer/devcontainer.json"
|
|
370
|
-
- ".devcontainer/postcreate.sh"
|
|
371
368
|
- ".github/CONTRIBUTING.md"
|
|
372
369
|
- ".github/ISSUE_TEMPLATE/bug_report.md"
|
|
373
370
|
- ".github/ISSUE_TEMPLATE/feature_request.md"
|
|
@@ -399,6 +396,12 @@ files:
|
|
|
399
396
|
- lib/onyxcord/api/server.rb
|
|
400
397
|
- lib/onyxcord/api/user.rb
|
|
401
398
|
- lib/onyxcord/api/webhook.rb
|
|
399
|
+
- lib/onyxcord/application_commands.rb
|
|
400
|
+
- lib/onyxcord/application_commands/command.rb
|
|
401
|
+
- lib/onyxcord/application_commands/context.rb
|
|
402
|
+
- lib/onyxcord/application_commands/option.rb
|
|
403
|
+
- lib/onyxcord/application_commands/registry.rb
|
|
404
|
+
- lib/onyxcord/async/runtime.rb
|
|
402
405
|
- lib/onyxcord/await.rb
|
|
403
406
|
- lib/onyxcord/bot.rb
|
|
404
407
|
- lib/onyxcord/cache.rb
|
|
@@ -474,7 +477,9 @@ files:
|
|
|
474
477
|
- lib/onyxcord/events/voice_state_update.rb
|
|
475
478
|
- lib/onyxcord/events/webhooks.rb
|
|
476
479
|
- lib/onyxcord/gateway.rb
|
|
480
|
+
- lib/onyxcord/http.rb
|
|
477
481
|
- lib/onyxcord/id_object.rb
|
|
482
|
+
- lib/onyxcord/json.rb
|
|
478
483
|
- lib/onyxcord/light.rb
|
|
479
484
|
- lib/onyxcord/light/data.rb
|
|
480
485
|
- lib/onyxcord/light/integrations.rb
|
|
@@ -483,6 +488,7 @@ files:
|
|
|
483
488
|
- lib/onyxcord/message_components.rb
|
|
484
489
|
- lib/onyxcord/paginator.rb
|
|
485
490
|
- lib/onyxcord/permissions.rb
|
|
491
|
+
- lib/onyxcord/rate_limiter/async_rest.rb
|
|
486
492
|
- lib/onyxcord/rate_limiter/gateway.rb
|
|
487
493
|
- lib/onyxcord/rate_limiter/rest.rb
|
|
488
494
|
- lib/onyxcord/version.rb
|
data/.devcontainer/Dockerfile
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Which Ruby version to use. You may need to use a more restrictive version,
|
|
2
|
-
# e.g. `3.0`
|
|
3
|
-
ARG VARIANT=3.3
|
|
4
|
-
|
|
5
|
-
# Pull Microsoft's ruby devcontainer base image
|
|
6
|
-
FROM mcr.microsoft.com/devcontainers/ruby:${VARIANT}
|
|
7
|
-
|
|
8
|
-
# Install libsodium dependency for voice channel interactions
|
|
9
|
-
RUN apt update -yq && apt install -y libsodium-dev
|
|
10
|
-
|
|
11
|
-
# Ensure we're running the latest bundler, as what ships with the Ruby image may
|
|
12
|
-
# not be current, and bundler will auto-downgrade to match the Gemfile.lock
|
|
13
|
-
RUN gem install bundler
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Ruby",
|
|
3
|
-
"build": {
|
|
4
|
-
"dockerfile": "Dockerfile"
|
|
5
|
-
},
|
|
6
|
-
|
|
7
|
-
// Configure tool-specific properties.
|
|
8
|
-
"customizations": {
|
|
9
|
-
// Configure properties specific to VS Code.
|
|
10
|
-
"vscode": {
|
|
11
|
-
// Add the IDs of extensions you want installed when the container is created.
|
|
12
|
-
"extensions": [
|
|
13
|
-
"shopify.ruby-lsp"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
// Set the environment variables
|
|
19
|
-
// "runArgs": ["--env-file",".env"],
|
|
20
|
-
|
|
21
|
-
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
22
|
-
// "forwardPorts": [],
|
|
23
|
-
|
|
24
|
-
// Use 'postCreateCommand' to run commands after the container is created.
|
|
25
|
-
"postCreateCommand": "bash .devcontainer/postcreate.sh",
|
|
26
|
-
|
|
27
|
-
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
|
28
|
-
"remoteUser": "vscode"
|
|
29
|
-
}
|
data/.devcontainer/postcreate.sh
DELETED