onyxcord 2.0.5 → 2.0.8
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/CHANGELOG.md +17 -3
- data/Rakefile +2 -6
- 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/gateway.rb +12 -3
- data/lib/onyxcord/rate_limiter/async_rest.rb +149 -0
- data/lib/onyxcord/version.rb +1 -1
- data/lib/onyxcord/webhooks/version.rb +3 -3
- data/lib/onyxcord/websocket.rb +11 -2
- metadata +8 -2
- data/onyxcord-webhooks.gemspec +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de906b93116b1c325312db44d7373011c24b4ea6807cea759112af2846a9449a
|
|
4
|
+
data.tar.gz: 3be86bc11a833fd3cb8b88874837d2cc732c916a4b6f4afbad340f1573531f89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7c429540a55956bb417841e610d9e8da946d18c4565c3177a45cb2ec4ba885988e02fade2c910edff015aee77970709198d450ba5719faa1862a182ab3507fb
|
|
7
|
+
data.tar.gz: 8159656f82928ec348522857df842cfe27479c6ba147e090778ac26749533f7d0c32c80390b61df6e4b15ef485fa2e6dcb886355650cc2cb1f2598759f694da9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.8 - 2026-06-28
|
|
4
|
+
|
|
5
|
+
### Correcoes de gateway
|
|
6
|
+
|
|
7
|
+
- Forca WebSocket do Gateway e do cliente generico em HTTP/1.1 para evitar `Async::WebSocket::ConnectionError: Failed to negotiate connection!` ao conectar no Discord.
|
|
8
|
+
- Desativa extensoes WebSocket no handshake para evitar close `Error while decoding payload` no Gateway do Discord.
|
|
9
|
+
- Remove o empacotamento separado `onyxcord-webhooks`; webhooks continuam incluidos diretamente na gem `onyxcord`.
|
|
10
|
+
|
|
11
|
+
## 2.0.6 - 2026-06-28
|
|
12
|
+
|
|
13
|
+
### Correcoes de empacotamento
|
|
14
|
+
|
|
15
|
+
- 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`.
|
|
16
|
+
- Incluidos na gem os arquivos da DSL moderna de application commands: `onyxcord/application_commands` e seus componentes internos.
|
|
17
|
+
- Corrige `LoadError: cannot load such file -- onyxcord/async/runtime` ao usar a gem publicada.
|
|
18
|
+
|
|
3
19
|
## 2.0.5 - 2026-06-28
|
|
4
20
|
|
|
5
21
|
### Async Runtime (Infraestrutura nao-bloqueante)
|
|
@@ -56,7 +72,7 @@ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
|
|
|
56
72
|
- **Gateway via Async-WebSocket**: Substituída a implementação de raw TCP sockets + `websocket-client-simple` por `async-websocket`, proporcionando um event loop de gateway extremamente rápido e escalável.
|
|
57
73
|
- **Parse JSON via Oj**: A gem `oj` foi integrada em modo de compatibilidade (`mode: :compat`), acelerando transparentemente todas as serializações e deserializações de pacotes do Discord na lib inteira.
|
|
58
74
|
- **Cache Inteligente LRU**: Os caches em memória (usuários, canais, servidores, membros) agora utilizam `LruRedux::ThreadSafeCache`. Os tamanhos padrão foram aumentados (`users: 50_000`, `channels: 10_000`, `servers: 1_000`, `members: 100_000`) e podem ser customizados via `OnyxCord.configure { |c| c.cache_sizes.users = 100_000 }`.
|
|
59
|
-
- **Fusão do Webhooks**: A funcionalidade da gem separada `onyxcord-webhooks` foi integrada diretamente no núcleo da gem `onyxcord
|
|
75
|
+
- **Fusão do Webhooks**: A funcionalidade da gem separada `onyxcord-webhooks` foi integrada diretamente no núcleo da gem `onyxcord`; nao ha mais pacote separado para publicar.
|
|
60
76
|
- **Alvo Ruby ≥ 3.4**: Atualizada a versão mínima requerida do Ruby para aproveitar as otimizações modernas do interpretador e fibras.
|
|
61
77
|
|
|
62
78
|
## 1.1.8 - 2026-06-28
|
|
@@ -73,7 +89,6 @@ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
|
|
|
73
89
|
- `ruby -c lib/onyxcord/data/component.rb`: sucesso.
|
|
74
90
|
- `ruby -c spec/components_v2_spec.rb`: sucesso.
|
|
75
91
|
- `gem build onyxcord.gemspec`: sucesso.
|
|
76
|
-
- `gem build onyxcord-webhooks.gemspec`: sucesso.
|
|
77
92
|
|
|
78
93
|
## 1.1.7 - 2026-06-28
|
|
79
94
|
|
|
@@ -85,7 +100,6 @@ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
|
|
|
85
100
|
### Validacao
|
|
86
101
|
|
|
87
102
|
- `ruby -c onyxcord.gemspec`: sucesso.
|
|
88
|
-
- `ruby -c onyxcord-webhooks.gemspec`: sucesso.
|
|
89
103
|
|
|
90
104
|
## 1.1.6 - 2026-06-28
|
|
91
105
|
|
data/Rakefile
CHANGED
|
@@ -6,12 +6,8 @@ namespace :main do
|
|
|
6
6
|
Bundler::GemHelper.install_tasks(name: 'onyxcord')
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
task build: %i[main:build webhooks:build]
|
|
14
|
-
task release: %i[main:release webhooks:release]
|
|
9
|
+
task build: :'main:build'
|
|
10
|
+
task release: :'main:release'
|
|
15
11
|
|
|
16
12
|
# Make "build" the default task
|
|
17
13
|
task default: :build
|
|
@@ -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
|
data/lib/onyxcord/gateway.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'async'
|
|
4
4
|
require 'async/http/endpoint'
|
|
5
|
+
require 'async/http/protocol/http11'
|
|
5
6
|
require 'async/websocket/client'
|
|
6
7
|
require 'onyxcord/async/runtime'
|
|
7
8
|
require 'onyxcord/rate_limiter/gateway'
|
|
@@ -293,9 +294,9 @@ module OnyxCord
|
|
|
293
294
|
@pipe_broken = false
|
|
294
295
|
@closed = false
|
|
295
296
|
|
|
296
|
-
endpoint =
|
|
297
|
+
endpoint = websocket_endpoint(url)
|
|
297
298
|
|
|
298
|
-
Async::WebSocket::Client.connect(endpoint) do |connection|
|
|
299
|
+
Async::WebSocket::Client.connect(endpoint, extensions: nil) do |connection|
|
|
299
300
|
@connection = connection
|
|
300
301
|
LOGGER.debug('WebSocket connected')
|
|
301
302
|
|
|
@@ -313,6 +314,14 @@ module OnyxCord
|
|
|
313
314
|
@connection = nil
|
|
314
315
|
end
|
|
315
316
|
|
|
317
|
+
def websocket_endpoint(url)
|
|
318
|
+
Async::HTTP::Endpoint.parse(
|
|
319
|
+
url,
|
|
320
|
+
protocol: Async::HTTP::Protocol::HTTP11,
|
|
321
|
+
alpn_protocols: ['http/1.1']
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
316
325
|
def handle_open; end
|
|
317
326
|
|
|
318
327
|
def handle_error(e)
|
|
@@ -453,7 +462,7 @@ module OnyxCord
|
|
|
453
462
|
|
|
454
463
|
@send_limiter.wait
|
|
455
464
|
|
|
456
|
-
@connection.write(
|
|
465
|
+
@connection.write(data)
|
|
457
466
|
@connection.flush
|
|
458
467
|
rescue StandardError => e
|
|
459
468
|
@pipe_broken = true
|
|
@@ -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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Webhook support
|
|
3
|
+
# Webhook support bundled in onyxcord.
|
|
4
4
|
module OnyxCord
|
|
5
5
|
module Webhooks
|
|
6
|
-
#
|
|
7
|
-
VERSION = '2.0.
|
|
6
|
+
# Kept for compatibility with code that checks the webhooks module version.
|
|
7
|
+
VERSION = '2.0.8'
|
|
8
8
|
end
|
|
9
9
|
end
|
data/lib/onyxcord/websocket.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'onyxcord/async/runtime'
|
|
4
4
|
require 'async/http/endpoint'
|
|
5
|
+
require 'async/http/protocol/http11'
|
|
5
6
|
require 'async/websocket/client'
|
|
6
7
|
|
|
7
8
|
module OnyxCord
|
|
@@ -43,10 +44,10 @@ module OnyxCord
|
|
|
43
44
|
private
|
|
44
45
|
|
|
45
46
|
def connect
|
|
46
|
-
endpoint =
|
|
47
|
+
endpoint = websocket_endpoint(@host)
|
|
47
48
|
|
|
48
49
|
@task = OnyxCord::AsyncRuntime.async do
|
|
49
|
-
Async::WebSocket::Client.connect(endpoint) do |connection|
|
|
50
|
+
Async::WebSocket::Client.connect(endpoint, extensions: nil) do |connection|
|
|
50
51
|
@connection = connection
|
|
51
52
|
@connected = true
|
|
52
53
|
@open_handler&.call
|
|
@@ -64,5 +65,13 @@ module OnyxCord
|
|
|
64
65
|
rescue StandardError => e
|
|
65
66
|
@error_handler&.call(e)
|
|
66
67
|
end
|
|
68
|
+
|
|
69
|
+
def websocket_endpoint(url)
|
|
70
|
+
Async::HTTP::Endpoint.parse(
|
|
71
|
+
url,
|
|
72
|
+
protocol: Async::HTTP::Protocol::HTTP11,
|
|
73
|
+
alpn_protocols: ['http/1.1']
|
|
74
|
+
)
|
|
75
|
+
end
|
|
67
76
|
end
|
|
68
77
|
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.
|
|
4
|
+
version: 2.0.8
|
|
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
|
|
@@ -499,7 +506,6 @@ files:
|
|
|
499
506
|
- lib/onyxcord/webhooks/version.rb
|
|
500
507
|
- lib/onyxcord/webhooks/view.rb
|
|
501
508
|
- lib/onyxcord/websocket.rb
|
|
502
|
-
- onyxcord-webhooks.gemspec
|
|
503
509
|
- onyxcord.gemspec
|
|
504
510
|
homepage: https://github.com/kruldevb/OnyxCord
|
|
505
511
|
licenses:
|
data/onyxcord-webhooks.gemspec
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'lib/onyxcord/webhooks/version'
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = 'onyxcord-webhooks'
|
|
7
|
-
spec.version = OnyxCord::Webhooks::VERSION
|
|
8
|
-
spec.authors = ['Gustavo Silva']
|
|
9
|
-
spec.email = ['gustavosilva8kt@gmail.com']
|
|
10
|
-
|
|
11
|
-
spec.summary = '[DEPRECATED] Webhook client for onyxcord — now bundled into the onyxcord gem'
|
|
12
|
-
spec.description = "This gem is deprecated. Webhooks are now included in the onyxcord gem. Install 'onyxcord' instead."
|
|
13
|
-
spec.homepage = 'https://github.com/kruldevb/OnyxCord'
|
|
14
|
-
spec.license = 'MIT'
|
|
15
|
-
|
|
16
|
-
spec.files = ['lib/onyxcord/webhooks.rb', 'lib/onyxcord/webhooks/version.rb']
|
|
17
|
-
spec.bindir = 'exe'
|
|
18
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
19
|
-
spec.require_paths = ['lib']
|
|
20
|
-
|
|
21
|
-
# This gem now simply depends on the main onyxcord gem
|
|
22
|
-
spec.add_dependency 'onyxcord', "~> #{OnyxCord::Webhooks::VERSION}"
|
|
23
|
-
|
|
24
|
-
spec.required_ruby_version = '>= 3.4'
|
|
25
|
-
spec.metadata = {
|
|
26
|
-
'bug_tracker_uri' => 'https://github.com/kruldevb/OnyxCord/issues',
|
|
27
|
-
'documentation_uri' => 'https://github.com/kruldevb/OnyxCord#readme',
|
|
28
|
-
'source_code_uri' => 'https://github.com/kruldevb/OnyxCord',
|
|
29
|
-
'rubygems_mfa_required' => 'true'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
spec.post_install_message = <<~MSG
|
|
33
|
-
⚠️ onyxcord-webhooks is DEPRECATED.
|
|
34
|
-
Webhooks are now bundled into the 'onyxcord' gem.
|
|
35
|
-
Please update your Gemfile:
|
|
36
|
-
gem 'onyxcord', '~> 2.0'
|
|
37
|
-
and remove 'onyxcord-webhooks'.
|
|
38
|
-
MSG
|
|
39
|
-
end
|