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.
@@ -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
@@ -3,5 +3,5 @@
3
3
  # OnyxCord and all its functionality, in this case only the version.
4
4
  module OnyxCord
5
5
  # The current version of onyxcord.
6
- VERSION = '2.0.0'
6
+ VERSION = '2.0.6'
7
7
  end
@@ -4,6 +4,6 @@
4
4
  module OnyxCord
5
5
  module Webhooks
6
6
  # The current version of onyxcord-webhooks.
7
- VERSION = '2.0.0'
7
+ VERSION = '2.0.6'
8
8
  end
9
9
  end
@@ -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
- Async do
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.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
@@ -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
- }
@@ -1,4 +0,0 @@
1
- #!/bin/bash
2
-
3
- bundle config set path vendor/bundle
4
- bundle install --jobs=1