onyxcord 2.0.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e5d5e025fe9ad9abb42d0fca6da5fe8bf9f32bfbe527de27a2da742bfe5f262
4
- data.tar.gz: e7b494fd1d4821c2c517f24ceca4a6da27720f552618b71f5a5452c5a3ef915b
3
+ metadata.gz: c794e2e7f5ad51dea52e27c049fb7d08fb7b54c84b452243fe549cd54f46dd5d
4
+ data.tar.gz: 2f7e1a9df9cf56085e31dd7bc13e843f8c5bee2e9083817305e6936117c92dd4
5
5
  SHA512:
6
- metadata.gz: '059ec009fb6fd52704a2906bdfc8fb6a20f0e06dd2ac783e1f466ce4cfabbe71c2fa9cb269a332e69fc3a05cdef7d8e394cfc3a3e1bfb11c94c0c7d43c0dd26d'
7
- data.tar.gz: c4f5fb863ec9a636d4b7591b50afe16d4a0174c28390f5d42aa84f7ff32a2e904cfe71de4104b4425a7661cb82e71a9b114abc6b874888c52d8ad74683f14d58
6
+ metadata.gz: dce8555d60a8bf6662bfc357c64918e75ff803ab30c43c93dab390756ee52e0057f20914367ec4cce89e5a6906c0745f99b89f42953bb9b91648caa3fe296378
7
+ data.tar.gz: 7b25cd2d344182abbdce7fb5f74f8cffb7dea3ad478d6a03e9018ea138635fac13e9203184f656f677c39b85b26f1a8287f24ac6a855b5b9ae64ec12165e8c72
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.6 - 2026-06-28
4
+
5
+ ### Correcoes de empacotamento
6
+
7
+ - Incluidos na gem os arquivos da infraestrutura async que ficaram fora do pacote `2.0.5`: `onyxcord/async/runtime` e `onyxcord/rate_limiter/async_rest`.
8
+ - Incluidos na gem os arquivos da DSL moderna de application commands: `onyxcord/application_commands` e seus componentes internos.
9
+ - Corrige `LoadError: cannot load such file -- onyxcord/async/runtime` ao usar a gem publicada.
10
+
3
11
  ## 2.0.5 - 2026-06-28
4
12
 
5
13
  ### Async Runtime (Infraestrutura nao-bloqueante)
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module ApplicationCommands
5
+ class Command
6
+ attr_reader :name, :description, :type, :attributes, :options, :block
7
+
8
+ TYPES = {
9
+ chat_input: 1,
10
+ user: 2,
11
+ message: 3
12
+ }.freeze
13
+
14
+ def self.chat_input(name, description:, **attributes, &block)
15
+ new(name, description: description, type: :chat_input, **attributes, &block)
16
+ end
17
+
18
+ def self.user(name, **attributes, &block)
19
+ new(name, description: '', type: :user, **attributes, &block)
20
+ end
21
+
22
+ def self.message(name, **attributes, &block)
23
+ new(name, description: '', type: :message, **attributes, &block)
24
+ end
25
+
26
+ def initialize(name, description: '', type: :chat_input, **attributes, &block)
27
+ @name = name.to_s
28
+ @description = description
29
+ @type = type
30
+ @attributes = attributes
31
+ @options = []
32
+ @block = block
33
+ @executor = nil
34
+ @default_member_permissions = attributes[:default_member_permissions]
35
+ @nsfw = attributes[:nsfw]
36
+ @contexts = attributes[:contexts]
37
+ end
38
+
39
+ def parse(&block)
40
+ instance_eval(&block) if block
41
+ self
42
+ end
43
+
44
+ def execute(&block)
45
+ @executor = block
46
+ end
47
+
48
+ def call(context)
49
+ return unless @executor
50
+
51
+ @executor.call(context)
52
+ end
53
+
54
+ def to_h
55
+ data = {
56
+ name: @name,
57
+ type: TYPES[@type] || @type
58
+ }
59
+
60
+ data[:description] = @description if @type == :chat_input
61
+ data[:options] = @options.map(&:to_h) unless @options.empty?
62
+ data[:default_member_permissions] = @default_member_permissions if @default_member_permissions
63
+ data[:nsfw] = @nsfw if @nsfw
64
+ data[:contexts] = @contexts if @contexts
65
+
66
+ data
67
+ end
68
+
69
+ Option::OPTION_METHODS.each do |method_name, option_type|
70
+ define_method(method_name) do |name, description = '', **attrs, &blk|
71
+ opt = Option.new(name, description, option_type, **attrs, &blk)
72
+ @options << opt
73
+ opt
74
+ end
75
+ end
76
+
77
+ def subcommand(name, description, &block)
78
+ sub = Option.new(name, description, :subcommand, &block)
79
+ @options << sub
80
+ sub
81
+ end
82
+
83
+ def subcommand_group(name, description, &block)
84
+ group = Option.new(name, description, :subcommand_group, &block)
85
+ @options << group
86
+ group
87
+ end
88
+
89
+ def method_missing(method_name, *args, **kwargs, &block)
90
+ if @block && @block.arity.positive?
91
+ @block.call(Context::Proxy.new(self, method_name, args, kwargs, block))
92
+ else
93
+ super
94
+ end
95
+ end
96
+
97
+ def respond_to_missing?(method_name, include_private = false)
98
+ true
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module ApplicationCommands
5
+ class Context
6
+ attr_reader :event, :command
7
+
8
+ def initialize(event, command)
9
+ @event = event
10
+ @command = command
11
+ end
12
+
13
+ def bot
14
+ event.bot
15
+ end
16
+
17
+ def user
18
+ event.user
19
+ end
20
+
21
+ def member
22
+ event.user
23
+ end
24
+
25
+ def guild
26
+ event.server
27
+ end
28
+
29
+ def guild_id
30
+ event.server_id
31
+ end
32
+
33
+ def channel
34
+ event.channel
35
+ end
36
+
37
+ def channel_id
38
+ event.channel_id
39
+ end
40
+
41
+ def server
42
+ event.server
43
+ end
44
+
45
+ def server_id
46
+ event.server_id
47
+ end
48
+
49
+ def options
50
+ return {} unless event.data
51
+
52
+ if event.data['options']
53
+ result = {}
54
+ event.data['options'].each do |opt|
55
+ key = opt['name'].to_sym
56
+ result[key] = opt['value']
57
+ end
58
+ result
59
+ else
60
+ {}
61
+ end
62
+ end
63
+
64
+ def respond(...)
65
+ event.respond(...)
66
+ end
67
+
68
+ def defer(...)
69
+ event.defer(...)
70
+ end
71
+
72
+ def edit_original(...)
73
+ event.edit_response(...)
74
+ end
75
+
76
+ def delete_original
77
+ event.delete_response
78
+ end
79
+
80
+ def followup(...)
81
+ event.send_message(...)
82
+ end
83
+
84
+ class Proxy
85
+ def initialize(command, method_name, args, kwargs, block)
86
+ @command = command
87
+ @method_name = method_name
88
+ @args = args
89
+ @kwargs = kwargs
90
+ @block = block
91
+ end
92
+
93
+ def to_h
94
+ {}
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module ApplicationCommands
5
+ class Option
6
+ attr_reader :name, :description, :type, :attributes, :options
7
+
8
+ OPTION_TYPES = {
9
+ subcommand: 1,
10
+ subcommand_group: 2,
11
+ string: 3,
12
+ integer: 4,
13
+ boolean: 5,
14
+ user: 6,
15
+ channel: 7,
16
+ role: 8,
17
+ mentionable: 9,
18
+ number: 10,
19
+ attachment: 11
20
+ }.freeze
21
+
22
+ OPTION_METHODS = OPTION_TYPES.each_with_object({}) do |(name, value), hash|
23
+ next if %i[subcommand subcommand_group].include?(name)
24
+
25
+ hash[name] = value
26
+ end.freeze
27
+
28
+ def initialize(name, description, type, **attributes, &block)
29
+ @name = name.to_s
30
+ @description = description
31
+ @type = type
32
+ @attributes = attributes
33
+ @options = []
34
+ @block = block
35
+
36
+ instance_eval(&@block) if @block && type == :subcommand
37
+ end
38
+
39
+ def to_h
40
+ data = {
41
+ name: @name,
42
+ description: @description,
43
+ type: OPTION_TYPES[@type] || @type
44
+ }
45
+
46
+ data[:required] = @attributes[:required] unless @attributes[:required].nil?
47
+ data[:min_length] = @attributes[:min_length] if @attributes[:min_length]
48
+ data[:max_length] = @attributes[:max_length] if @attributes[:max_length]
49
+ data[:min_value] = @attributes[:min_value] if @attributes[:min_value]
50
+ data[:max_value] = @attributes[:max_value] if @attributes[:max_value]
51
+ data[:autocomplete] = @attributes[:autocomplete] unless @attributes[:autocomplete].nil?
52
+ data[:channel_types] = @attributes[:channel_types] if @attributes[:channel_types]
53
+
54
+ if @attributes[:choices]
55
+ data[:choices] = @attributes[:choices].map do |name, value|
56
+ { name: name.to_s, value: value }
57
+ end
58
+ end
59
+
60
+ data[:options] = @options.map(&:to_h) unless @options.empty?
61
+
62
+ data
63
+ end
64
+
65
+ def subcommand(name, description, **attrs, &block)
66
+ sub = Option.new(name, description, :subcommand, **attrs, &block)
67
+ @options << sub
68
+ sub
69
+ end
70
+
71
+ OPTION_METHODS.each do |method_name, option_type|
72
+ define_method(method_name) do |name, description = '', **attrs, &blk|
73
+ opt = Option.new(name, description, option_type, **attrs, &blk)
74
+ @options << opt
75
+ opt
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnyxCord
4
+ module ApplicationCommands
5
+ class Registry
6
+ attr_reader :bot, :commands
7
+
8
+ def initialize(bot)
9
+ @bot = bot
10
+ @commands = {}
11
+ end
12
+
13
+ def slash(name, description:, **attributes, &block)
14
+ register(Command.chat_input(name, description: description, **attributes, &block))
15
+ end
16
+
17
+ def user(name, **attributes, &block)
18
+ register(Command.user(name, **attributes, &block))
19
+ end
20
+
21
+ def message(name, **attributes, &block)
22
+ register(Command.message(name, **attributes, &block))
23
+ end
24
+
25
+ def register(command)
26
+ @commands[command.name] = command
27
+ wire_handler(command)
28
+ command
29
+ end
30
+
31
+ def sync!(server_id: nil, delete_unknown: false)
32
+ payload = @commands.values.map(&:to_h)
33
+
34
+ if server_id
35
+ @bot.bulk_overwrite_guild_application_commands(server_id, payload)
36
+ else
37
+ @bot.bulk_overwrite_global_application_commands(payload)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def wire_handler(command)
44
+ @bot.application_command(command.name) do |event|
45
+ command.call(Context.new(event, command))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'onyxcord/application_commands/option'
4
+ require 'onyxcord/application_commands/command'
5
+ require 'onyxcord/application_commands/context'
6
+ require 'onyxcord/application_commands/registry'
7
+
8
+ module OnyxCord
9
+ module ApplicationCommands
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'async'
4
+
5
+ module OnyxCord
6
+ module AsyncRuntime
7
+ module_function
8
+
9
+ def run(&block)
10
+ current = Async::Task.current?
11
+ return yield current if current
12
+
13
+ Async(&block).wait
14
+ end
15
+
16
+ def async(&block)
17
+ current = Async::Task.current?
18
+ return current.async(&block) if current
19
+
20
+ Async(&block)
21
+ end
22
+
23
+ def sleep(duration)
24
+ task = Async::Task.current?
25
+ return task.sleep(duration) if task.respond_to?(:sleep)
26
+
27
+ Kernel.sleep(duration)
28
+ end
29
+ end
30
+ end
@@ -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.5'
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
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.5
4
+ version: 2.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gustavo Silva
@@ -396,6 +396,12 @@ files:
396
396
  - lib/onyxcord/api/server.rb
397
397
  - lib/onyxcord/api/user.rb
398
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
399
405
  - lib/onyxcord/await.rb
400
406
  - lib/onyxcord/bot.rb
401
407
  - lib/onyxcord/cache.rb
@@ -482,6 +488,7 @@ files:
482
488
  - lib/onyxcord/message_components.rb
483
489
  - lib/onyxcord/paginator.rb
484
490
  - lib/onyxcord/permissions.rb
491
+ - lib/onyxcord/rate_limiter/async_rest.rb
485
492
  - lib/onyxcord/rate_limiter/gateway.rb
486
493
  - lib/onyxcord/rate_limiter/rest.rb
487
494
  - lib/onyxcord/version.rb