onyxcord 1.1.8 → 2.0.5

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: 07ea78b3a647f1f03a640e750eef4e5a99595f6afa66ce9be842d012c5e8e2c1
4
- data.tar.gz: '08f1edd1b05510983d80766f627db7372edb8f3bce6234ba9cb4d669e9cf2070'
3
+ metadata.gz: 2e5d5e025fe9ad9abb42d0fca6da5fe8bf9f32bfbe527de27a2da742bfe5f262
4
+ data.tar.gz: e7b494fd1d4821c2c517f24ceca4a6da27720f552618b71f5a5452c5a3ef915b
5
5
  SHA512:
6
- metadata.gz: '092c91e1b949b091079169f2977c44745fea551a93e2cc3d065df2b9c51b534cbb027a01519d781dfc0fe2f783c360eecccee29d46b4f0bb28cd99350bde56cf'
7
- data.tar.gz: 076604ea2c7a9a6a0c9303fedfbd5e1cac58cb11cbc741d88afff80a838f80da5637a292b494a6d84ae63da4ce5fc435c8eb3548dcb833961dff588ff6b24974
6
+ metadata.gz: '059ec009fb6fd52704a2906bdfc8fb6a20f0e06dd2ac783e1f466ce4cfabbe71c2fa9cb269a332e69fc3a05cdef7d8e394cfc3a3e1bfb11c94c0c7d43c0dd26d'
7
+ data.tar.gz: c4f5fb863ec9a636d4b7591b50afe16d4a0174c28390f5d42aa84f7ff32a2e904cfe71de4104b4425a7661cb82e71a9b114abc6b874888c52d8ad74683f14d58
data/.gitignore CHANGED
@@ -9,6 +9,7 @@
9
9
  /profiles/
10
10
  /tmp/
11
11
  /.idea/
12
+ /obsidian/
12
13
  .DS_Store
13
14
  *.gem
14
15
  **.sw*
data/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.5 - 2026-06-28
4
+
5
+ ### Async Runtime (Infraestrutura nao-bloqueante)
6
+
7
+ - **`OnyxCord::AsyncRuntime`**: modulo central que gerencia o reactor `async` com `run`, `async` e `sleep`, reaproveitando reactor existente quando disponivel.
8
+ - **`EventExecutor::AsyncPool`**: novo pool de workers baseado em `Async::Queue` e fibers, sem threads.
9
+ - **Gateway**: `run_async` nao cria mais `Thread.new` — usa `@task = AsyncRuntime.async { run }`. Todos os `sleep` trocados por `AsyncRuntime.sleep`.
10
+ - **WebSocket**: usa `AsyncRuntime.async` em vez de `Async do` solto na classe.
11
+ - **API REST**: `request` agora delega para `request_async` automaticamente quando dentro de um reactor. `request_async` usa rate limiter async, `AsyncRuntime.sleep`, e retry com limite em 502.
12
+ - **Rate Limiter Async**: novo `OnyxCord::RateLimiter::AsyncRest` que evita `mutex.synchronize { sleep }` bloqueante.
13
+ - **Bot**: `run`/`stop`/`join` refatorados para o runtime async. `send_temporary_message` e `voice_connect` usam sleeps async.
14
+ - Compatibilidade sync mantida: a API publica continua funcionando de forma sincrona quando chamada fora de um reactor.
15
+
16
+ ### Modern Application Commands DSL
17
+
18
+ - **`bot.slash`, `bot.user_command`, `bot.message_command`**: nova DSL para comandos modernos com definicao e handler unificados.
19
+ - **`bot.sync_application_commands!`**: sincroniza todos os commands registrados com a API do Discord de uma vez.
20
+ - **`bot.bulk_overwrite_global_application_commands`** e **`bot.bulk_overwrite_guild_application_commands`**: wrappers para bulk overwrite.
21
+ - **`ApplicationCommands::Context`**: wrapper com `respond`, `defer`, `edit_original`, `delete_original`, `followup` e acesso a `options`, `guild`, `channel`, `user`.
22
+ - **`Interaction#edit_original`**, **`Interaction#delete_original`**, **`Interaction#followup`**: novos aliases dos metodos originais.
23
+ - API legacy (`register_application_command` + `application_command`) mantida com compatibilidade total.
24
+
25
+ ### Exemplo da nova DSL
26
+
27
+ ```ruby
28
+ bot.slash :ban, description: "Bane um membro", default_member_permissions: [:ban_members] do
29
+ user :member, "Membro que sera banido", required: true
30
+ string :reason, "Motivo do banimento", max_length: 512
31
+
32
+ execute do |ctx|
33
+ ctx.defer(ephemeral: true)
34
+ member = ctx.options[:member]
35
+ reason = ctx.options[:reason] || "Sem motivo informado"
36
+ ctx.guild.ban(member, reason: reason)
37
+ ctx.edit_original(content: "Membro banido com sucesso.")
38
+ end
39
+ end
40
+
41
+ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
42
+ ```
43
+
44
+ ### Validacao
45
+
46
+ - `bundle exec rspec`: 460 exemplos, 0 falhas, 3 pendentes.
47
+ - `ruby -c lib/onyxcord/**/*.rb`: todos os arquivos com sintaxe OK.
48
+ - `gem build onyxcord.gemspec`: sucesso.
49
+
50
+ ## 2.0.0 - 2026-06-28
51
+
52
+ ### Arquitetura & Performance (Major Refactoring)
53
+
54
+ - **Runtime 100% Async**: A infraestrutura de threads (`Thread.new`) foi substituída pelo modelo de fibers do Ruby usando a gem `async`. Gateway, heartbeats, fila REST, worker de eventos e comandos agora executam de forma não-bloqueante no reator Async.
55
+ - **REST Moderno via HTTPX**: Substituída a gem legada `rest-client` pela `httpx`, trazendo suporte nativo a conexões persistentes (Keep-Alive), HTTP/2, multipart uploads nativos e retries automáticos em erros 502.
56
+ - **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
+ - **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
+ - **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`. A gem `onyxcord-webhooks` agora atua apenas como um shim de transição deprecado.
60
+ - **Alvo Ruby ≥ 3.4**: Atualizada a versão mínima requerida do Ruby para aproveitar as otimizações modernas do interpretador e fibras.
61
+
3
62
  ## 1.1.8 - 2026-06-28
4
63
 
5
64
  ### Correcoes
@@ -10,6 +69,7 @@
10
69
  ### Validacao
11
70
 
12
71
  - `bundle exec rspec spec/components_v2_spec.rb`: sucesso.
72
+ - `bundle exec rspec`: 460 exemplos, 0 falhas, 3 pendentes.
13
73
  - `ruby -c lib/onyxcord/data/component.rb`: sucesso.
14
74
  - `ruby -c spec/components_v2_spec.rb`: sucesso.
15
75
  - `gem build onyxcord.gemspec`: sucesso.
data/Gemfile CHANGED
@@ -4,4 +4,3 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in onyxcord.gemspec
6
6
  gemspec name: 'onyxcord'
7
- gemspec name: 'onyxcord-webhooks', development_group: 'webhooks'
data/README.md CHANGED
@@ -22,6 +22,8 @@ Simple to start, deep enough to control.
22
22
  - Friendly Ruby API for Discord bots.
23
23
  - Traditional object events for productivity.
24
24
  - Raw gateway events for performance and lower allocation.
25
+ - **Modern async runtime** built on `async` gem with non-blocking gateway, REST and event dispatch.
26
+ - **New modern slash command DSL** with `bot.slash`, `execute`, and `bot.sync_application_commands!`.
25
27
  - Components V2 support with `Text Display`, `Container`, `Section`, `Media Gallery`, `File`, `Separator`, and `Thumbnail`.
26
28
  - Modern modal components, including `Label`, `Text Display`, modal selects, file upload, radio group, checkbox group, and checkbox.
27
29
  - Webhooks with embeds, files, and components.
@@ -112,6 +114,25 @@ bot.application_command(:feedback) do |event|
112
114
  end
113
115
  ```
114
116
 
117
+ ### Modern Command DSL
118
+
119
+ ```ruby
120
+ bot.slash :ban, description: 'Ban a member', default_member_permissions: [:ban_members] do
121
+ user :member, 'Member to ban', required: true
122
+ string :reason, 'Ban reason', max_length: 512
123
+
124
+ execute do |ctx|
125
+ ctx.defer(ephemeral: true)
126
+ member = ctx.options[:member]
127
+ reason = ctx.options[:reason] || 'No reason provided'
128
+ ctx.guild.ban(member, reason: reason)
129
+ ctx.edit_original(content: 'Member banned.')
130
+ end
131
+ end
132
+
133
+ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
134
+ ```
135
+
115
136
  ### Community
116
137
 
117
138
  Join the Discord server for support, updates, examples, and feedback: https://discord.gg/Jy2tpCUtzM
@@ -131,6 +152,8 @@ Simples para comecar, profundo para controlar.
131
152
  - API Ruby amigavel para bots do Discord.
132
153
  - Eventos tradicionais com objetos para quem quer produtividade.
133
154
  - Eventos raw para quem quer performance e menos alocacao.
155
+ - **Runtime async moderno** baseado na gem `async`: gateway, REST e dispatch de eventos nao-bloqueantes.
156
+ - **Nova DSL moderna de slash commands** com `bot.slash`, `execute` e `bot.sync_application_commands!`.
134
157
  - Components V2 com `Text Display`, `Container`, `Section`, `Media Gallery`, `File`, `Separator` e `Thumbnail`.
135
158
  - Novos componentes de modal, incluindo `Label`, `Text Display`, selects em modal, upload, radio group, checkbox group e checkbox.
136
159
  - Webhooks com embeds, arquivos e componentes.
@@ -221,6 +244,25 @@ bot.application_command(:feedback) do |event|
221
244
  end
222
245
  ```
223
246
 
247
+ ### DSL Moderna de Comandos
248
+
249
+ ```ruby
250
+ bot.slash :ban, description: 'Bane um membro', default_member_permissions: [:ban_members] do
251
+ user :member, 'Membro que sera banido', required: true
252
+ string :reason, 'Motivo do banimento', max_length: 512
253
+
254
+ execute do |ctx|
255
+ ctx.defer(ephemeral: true)
256
+ member = ctx.options[:member]
257
+ reason = ctx.options[:reason] || 'Sem motivo informado'
258
+ ctx.guild.ban(member, reason: reason)
259
+ ctx.edit_original(content: 'Membro banido com sucesso.')
260
+ end
261
+ end
262
+
263
+ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
264
+ ```
265
+
224
266
  ### Comunidade
225
267
 
226
268
  Entre no servidor do Discord para suporte, atualizacoes, exemplos e feedback: https://discord.gg/Jy2tpCUtzM
@@ -237,9 +279,11 @@ Simple para empezar, profundo para controlar.
237
279
 
238
280
  ### Caracteristicas
239
281
 
240
- - API Ruby amigable para bots de Discord.
282
+ - API Ruby amigavel para bots de Discord.
241
283
  - Eventos tradicionales con objetos para productividad.
242
284
  - Eventos raw para rendimiento y menos asignaciones.
285
+ - **Runtime async moderno** basado en la gem `async`: gateway, REST y dispatch de eventos no bloqueantes.
286
+ - **Nueva DSL moderna de slash commands** con `bot.slash`, `execute` y `bot.sync_application_commands!`.
243
287
  - Components V2 con `Text Display`, `Container`, `Section`, `Media Gallery`, `File`, `Separator` y `Thumbnail`.
244
288
  - Componentes modernos de modal, incluyendo `Label`, `Text Display`, selects en modal, subida de archivos, radio group, checkbox group y checkbox.
245
289
  - Webhooks con embeds, archivos y componentes.
@@ -330,6 +374,25 @@ bot.application_command(:feedback) do |event|
330
374
  end
331
375
  ```
332
376
 
377
+ ### DSL Moderna de Comandos
378
+
379
+ ```ruby
380
+ bot.slash :ban, description: 'Banear a un miembro', default_member_permissions: [:ban_members] do
381
+ user :member, 'Miembro a banear', required: true
382
+ string :reason, 'Motivo del baneo', max_length: 512
383
+
384
+ execute do |ctx|
385
+ ctx.defer(ephemeral: true)
386
+ member = ctx.options[:member]
387
+ reason = ctx.options[:reason] || 'Sin motivo'
388
+ ctx.guild.ban(member, reason: reason)
389
+ ctx.edit_original(content: 'Miembro baneado.')
390
+ end
391
+ end
392
+
393
+ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
394
+ ```
395
+
333
396
  ### Comunidad
334
397
 
335
398
  Unete al servidor de Discord para soporte, actualizaciones, ejemplos y feedback: https://discord.gg/Jy2tpCUtzM
@@ -23,7 +23,6 @@ module OnyxCord::API::Channel
23
23
  { **files, payload_json: body.to_json }
24
24
  end
25
25
 
26
-
27
26
  # Get a channel's data
28
27
  # https://discord.com/developers/docs/resources/channel#get-channel
29
28
  def resolve(token, channel_id)
@@ -111,7 +110,7 @@ module OnyxCord::API::Channel
111
110
  # @param attachments [Array<File>, nil] Attachments to use with `attachment://` in embeds. See
112
111
  # https://discord.com/developers/docs/resources/channel#create-message-using-attachments-within-embeds
113
112
  def create_message(token, channel_id, message, tts = false, embeds = nil, nonce = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = nil, enforce_nonce = false, poll = nil)
114
- tts = false unless tts == true || tts == false
113
+ tts = false unless [true, false].include?(tts)
115
114
  components = OnyxCord::MessageComponents.payload(components) unless components.nil?
116
115
  flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
117
116
  body = { content: message, tts: tts == true, embeds: embeds, nonce: nonce, allowed_mentions: allowed_mentions, message_reference: message_reference, components: components, attachments: attachments ? attachment_payload(attachments) : nil, flags: flags, enforce_nonce: enforce_nonce, poll: poll }.compact
@@ -132,11 +131,6 @@ module OnyxCord::API::Channel
132
131
  body,
133
132
  **headers
134
133
  )
135
- rescue RestClient::BadRequest => e
136
- parsed = JSON.parse(e.response.body)
137
- raise OnyxCord::Errors::MessageTooLong, "Message over the character limit (#{message.length} > 2000)" if parsed['content'].is_a?(Array) && parsed['content'].first == 'Must be 2000 or fewer in length.'
138
-
139
- raise
140
134
  end
141
135
 
142
136
  # Send a file as a message to a channel
@@ -425,12 +419,6 @@ module OnyxCord::API::Channel
425
419
  Authorization: token,
426
420
  content_type: :json
427
421
  )
428
- rescue RestClient::InternalServerError
429
- raise 'Attempted to add self as a new group channel recipient!'
430
- rescue RestClient::NoContent
431
- raise 'Attempted to create a group channel with the PM channel recipient!'
432
- rescue RestClient::Forbidden
433
- raise 'Attempted to add a user to group channel without permission!'
434
422
  end
435
423
 
436
424
  # Add a user to a group channel.
@@ -62,7 +62,7 @@ module OnyxCord::API::Webhook
62
62
  end
63
63
 
64
64
  headers = { content_type: :json } unless file || attachments
65
- with_components = components&.any? ? true : nil
65
+ with_components = components&.any? || nil
66
66
  query = URI.encode_www_form({ wait: wait, with_components: with_components }.compact)
67
67
 
68
68
  OnyxCord::API.request(
data/lib/onyxcord/api.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rest-client'
4
- require 'json'
3
+ require 'onyxcord/http'
4
+ require 'onyxcord/json'
5
+ require 'onyxcord/async/runtime'
5
6
  require 'time'
6
7
 
7
8
  require 'onyxcord/errors'
8
9
  require 'onyxcord/rate_limiter/rest'
10
+ require 'onyxcord/rate_limiter/async_rest'
9
11
 
10
12
  # List of methods representing endpoints in Discord's API
11
13
  module OnyxCord::API
@@ -55,22 +57,31 @@ module OnyxCord::API
55
57
  required = "DiscordBot (https://github.com/kruldevb/OnyxCord, v#{OnyxCord::VERSION})"
56
58
  @bot_name ||= ''
57
59
 
58
- "#{required} rest-client/#{RestClient::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} onyxcord/#{OnyxCord::VERSION} #{@bot_name}"
60
+ "#{required} httpx/#{HTTPX::VERSION} #{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL} onyxcord/#{OnyxCord::VERSION} #{@bot_name}"
59
61
  end
60
62
 
61
63
  # Resets all rate limit mutexes
62
64
  def reset_mutexes
63
65
  @rate_limiter = OnyxCord::RateLimiter::Rest.new
66
+ @async_rate_limiter = OnyxCord::RateLimiter::AsyncRest.new
64
67
  end
65
68
 
66
69
  def rate_limiter
67
70
  @rate_limiter ||= OnyxCord::RateLimiter::Rest.new
68
71
  end
69
72
 
73
+ def async_rate_limiter
74
+ @async_rate_limiter ||= OnyxCord::RateLimiter::AsyncRest.new
75
+ end
76
+
70
77
  def rate_limiter_stats
71
78
  rate_limiter.stats
72
79
  end
73
80
 
81
+ def async_rate_limiter_stats
82
+ async_rate_limiter.stats
83
+ end
84
+
74
85
  # Wait a specified amount of time synchronised with the specified mutex.
75
86
  def sync_wait(time, mutex)
76
87
  mutex.synchronize { sleep time }
@@ -82,80 +93,125 @@ module OnyxCord::API
82
93
  mutex.unlock
83
94
  end
84
95
 
85
- # Performs a RestClient request.
96
+ # Performs a raw HTTP request using HTTPX.
86
97
  # @param type [Symbol] The type of HTTP request to use.
87
- # @param attributes [Array] The attributes for the request.
88
- def raw_request(type, attributes)
89
- RestClient.send(type, *attributes)
90
- rescue RestClient::Forbidden => e
91
- # HACK: for #request, dynamically inject restclient's response into NoPermission - this allows us to rate limit
92
- noprm = OnyxCord::Errors::NoPermission.new
93
- noprm.define_singleton_method(:_rc_response) { e.response }
94
- raise noprm, "The bot doesn't have the required permission to do this!"
95
- rescue RestClient::BadGateway
96
- OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
97
- retry
98
+ # @param url [String] The URL to request.
99
+ # @param body [String, Hash, nil] The request body.
100
+ # @param headers [Hash] Additional headers.
101
+ # @return [OnyxCord::HTTP::Response]
102
+ def raw_request(type, url, body = nil, **headers)
103
+ headers[:user_agent] = user_agent
104
+
105
+ response = OnyxCord::HTTP.request(type, url, body, **headers)
106
+
107
+ if response.code == 403
108
+ noprm = OnyxCord::Errors::NoPermission.new
109
+ noprm.define_singleton_method(:_response) { response }
110
+ raise noprm, "The bot doesn't have the required permission to do this!"
111
+ end
112
+
113
+ # Retry on 502 Bad Gateway
114
+ if response.code == 502
115
+ OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
116
+ return raw_request(type, url, body, **headers)
117
+ end
118
+
119
+ response
98
120
  end
99
121
 
100
122
  # Make an API request, including rate limit handling.
101
123
  def request(key, major_parameter, type, *attributes)
102
- # Add a custom user agent
103
- attributes.last[:user_agent] = user_agent if attributes.last.is_a? Hash
124
+ if Async::Task.current?
125
+ request_async(key, major_parameter, type, *attributes)
126
+ else
127
+ OnyxCord::AsyncRuntime.run { request_async(key, major_parameter, type, *attributes) }
128
+ end
129
+ end
130
+
131
+ # Async version of request.
132
+ def request_async(key, major_parameter, type, *attributes)
133
+ url = attributes.shift
134
+ headers_or_body = attributes
135
+
136
+ body = nil
137
+ headers = {}
138
+
139
+ headers_or_body.each do |arg|
140
+ if arg.is_a?(Hash)
141
+ headers.merge!(arg)
142
+ elsif body.nil?
143
+ body = arg
144
+ end
145
+ end
146
+
147
+ content_type = headers.delete(:content_type)
148
+ headers['content-type'] = 'application/json' if content_type == :json
149
+
150
+ headers['user-agent'] = user_agent
151
+
152
+ retries = 0
153
+ max_retries = key == :gateway ? 0 : 3
104
154
 
105
155
  begin
106
- rate_limiter.before_request(key, major_parameter)
156
+ async_rate_limiter.before_request(key, major_parameter)
107
157
 
108
158
  response = nil
109
- begin
110
- response = raw_request(type, attributes)
111
- rescue RestClient::Exception => e
112
- response = e.response
159
+ loop do
160
+ response = OnyxCord::HTTP.request(type, url, body, **headers)
161
+ break unless response.code == 502
113
162
 
114
- if response.body && !e.is_a?(RestClient::TooManyRequests)
115
- data = JSON.parse(response.body)
116
- err_klass = OnyxCord::Errors.error_class_for(data['code'] || 0)
117
- e = err_klass.new(data['message'], data['errors'])
163
+ retries += 1
164
+ break unless retries < max_retries
118
165
 
119
- OnyxCord::LOGGER.error(e.full_message)
120
- end
166
+ OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
167
+ OnyxCord::AsyncRuntime.sleep(retries * 0.5)
168
+ end
121
169
 
122
- raise e
123
- rescue OnyxCord::Errors::NoPermission => e
124
- if e.respond_to?(:_rc_response)
125
- response = e._rc_response
126
- else
127
- OnyxCord::LOGGER.warn("NoPermission doesn't respond_to? _rc_response!")
170
+ if response.code == 403
171
+ noprm = OnyxCord::Errors::NoPermission.new
172
+ noprm.define_singleton_method(:_response) { response }
173
+ raise noprm, "The bot doesn't have the required permission to do this!"
174
+ end
175
+
176
+ if response.code >= 400 && response.code != 429
177
+ data = begin
178
+ JSON.parse(response.body)
179
+ rescue StandardError
180
+ nil
128
181
  end
129
182
 
183
+ raise "HTTP #{response.code}: #{response.body}" unless data
184
+
185
+ err_klass = OnyxCord::Errors.error_class_for(data['code'] || 0)
186
+ e = err_klass.new(data['message'], data['errors'])
187
+ OnyxCord::LOGGER.error(e.full_message)
130
188
  raise e
131
- ensure
132
- if response
133
- rate_limiter.record_response(key, major_parameter, response.headers)
134
- else
135
- OnyxCord::LOGGER.ratelimit('Response was nil before trying to preemptively rate limit!')
136
- end
137
189
  end
138
- rescue RestClient::TooManyRequests => e
139
- trace("429 #{key} #{major_parameter}")
140
- rate_limiter.handle_rate_limit(key, major_parameter, e.response)
190
+ rescue OnyxCord::Errors::NoPermission => e
191
+ async_rate_limiter.record_response(key, major_parameter, e._response.headers) if e.respond_to?(:_response)
192
+ raise e
193
+ else
194
+ async_rate_limiter.record_response(key, major_parameter, response.headers)
195
+ end
141
196
 
142
- retry
197
+ if response.code == 429
198
+ trace("429 #{key} #{major_parameter}")
199
+ async_rate_limiter.handle_rate_limit(key, major_parameter, response)
200
+ return request_async(key, major_parameter, type, url, body, headers)
143
201
  end
144
202
 
145
- # Endpoints that use Elasticsearch can return a 202 when the index isn't ready yet. Wait the
146
- # amount of time indicated by the response body, and then recursively retry and return the request.
147
- if response&.code == 202 && response&.body
148
- body = JSON.parse(response.body)
203
+ if response.code == 202 && response.body
204
+ body_data = JSON.parse(response.body)
149
205
 
150
- if body['code'] == 110_000
151
- case body['retry_after']
206
+ if body_data['code'] == 110_000
207
+ case body_data['retry_after']
152
208
  when 0, 1, nil
153
- sleep(rand(4.5..5.0))
209
+ OnyxCord::AsyncRuntime.sleep(rand(4.5..5.0))
154
210
  else
155
- sleep(body['retry_after'])
211
+ OnyxCord::AsyncRuntime.sleep(body_data['retry_after'])
156
212
  end
157
213
 
158
- return request(key, major_parameter, type, *attributes)
214
+ return request_async(key, major_parameter, type, url, body, headers)
159
215
  end
160
216
  end
161
217
 
data/lib/onyxcord/bot.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rest-client'
3
+ require 'onyxcord/http'
4
4
  require 'zlib'
5
5
 
6
6
  require 'onyxcord/configuration'
@@ -33,6 +33,7 @@ require 'onyxcord/api/invite'
33
33
  require 'onyxcord/api/interaction'
34
34
  require 'onyxcord/api/application'
35
35
 
36
+ require 'onyxcord/async/runtime'
36
37
  require 'onyxcord/errors'
37
38
  require 'onyxcord/message_components'
38
39
  require 'onyxcord/data'
@@ -42,6 +43,7 @@ require 'onyxcord/websocket'
42
43
  require 'onyxcord/cache'
43
44
  require 'onyxcord/gateway'
44
45
 
46
+ require 'onyxcord/application_commands'
45
47
  require 'onyxcord/voice/voice_bot'
46
48
 
47
49
  module OnyxCord
@@ -309,28 +311,27 @@ module OnyxCord
309
311
  # this. If you need a way to safely run code after the bot is fully
310
312
  # connected, use a {#ready} event handler instead.
311
313
  def run(background = false)
312
- @gateway.run_async
313
- return if background
314
+ if background
315
+ @run_task = OnyxCord::AsyncRuntime.async { run_forever }
316
+ return @run_task
317
+ end
318
+
319
+ OnyxCord::AsyncRuntime.run { run_forever }
320
+ end
314
321
 
315
- debug('Oh wait! Not exiting yet as run was run synchronously.')
316
- @gateway.sync
322
+ def run_forever
323
+ @gateway.run
317
324
  end
318
325
 
319
- # Joins the bot's connection thread with the current thread.
320
- # This blocks execution until the websocket stops, which should only happen
321
- # manually triggered. or due to an error. This is necessary to have a
322
- # continuously running bot.
323
326
  def join
324
- @gateway.sync
327
+ @run_task&.wait
325
328
  end
326
329
  alias_method :sync, :join
327
330
 
328
- # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
329
- # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
330
- # @note This method no longer takes an argument as of 3.4.0
331
331
  def stop(_no_sync = nil)
332
332
  @gateway.stop
333
333
  @event_executor.shutdown
334
+ @run_task&.stop if @run_task.respond_to?(:stop)
334
335
  nil
335
336
  end
336
337
 
@@ -411,7 +412,7 @@ module OnyxCord
411
412
 
412
413
  debug('Voice channel init packet sent! Now waiting.')
413
414
 
414
- sleep(0.05) until @voices[server_id]
415
+ OnyxCord::AsyncRuntime.sleep(0.05) until @voices[server_id]
415
416
  debug('Voice connect succeeded!')
416
417
  @voices[server_id]
417
418
  end
@@ -477,11 +478,9 @@ module OnyxCord
477
478
  # @param enforce_nonce [true, false] Whether the nonce should be enforced and used for message de-duplication.
478
479
  # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to this message.
479
480
  def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil)
480
- Thread.new do
481
- Thread.current[:onyxcord_name] = "#{@current_thread}-temp-msg"
482
-
481
+ OnyxCord::AsyncRuntime.async do
483
482
  message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce, poll)
484
- sleep(timeout)
483
+ OnyxCord::AsyncRuntime.sleep(timeout)
485
484
  message.delete
486
485
  end
487
486
 
@@ -504,7 +503,6 @@ module OnyxCord
504
503
  filename ||= File.basename(file.path)
505
504
  filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
506
505
  end
507
- # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
508
506
  file.define_singleton_method(:original_filename) { filename } if filename
509
507
  file.define_singleton_method(:path) { filename } if filename
510
508
  end
@@ -867,6 +865,37 @@ module OnyxCord
867
865
  ApplicationCommand.new(JSON.parse(resp), self, server_id)
868
866
  end
869
867
 
868
+ # @return [OnyxCord::ApplicationCommands::Registry]
869
+ def commands
870
+ @modern_command_registry ||= OnyxCord::ApplicationCommands::Registry.new(self)
871
+ end
872
+
873
+ def slash(name, description: nil, **attributes, &block)
874
+ commands.slash(name, description: description, **attributes, &block)
875
+ end
876
+
877
+ def user_command(name, **attributes, &block)
878
+ commands.user(name, **attributes, &block)
879
+ end
880
+
881
+ def message_command(name, **attributes, &block)
882
+ commands.message(name, **attributes, &block)
883
+ end
884
+
885
+ def sync_application_commands!(server_id: nil, delete_unknown: false)
886
+ commands.sync!(server_id: server_id, delete_unknown: delete_unknown)
887
+ end
888
+
889
+ def bulk_overwrite_global_application_commands(commands)
890
+ response = API::Application.bulk_overwrite_global_commands(@token, profile.id, commands)
891
+ JSON.parse(response).map { |data| ApplicationCommand.new(data, self) }
892
+ end
893
+
894
+ def bulk_overwrite_guild_application_commands(server_id, commands)
895
+ response = API::Application.bulk_overwrite_guild_commands(@token, profile.id, server_id.resolve_id, commands)
896
+ JSON.parse(response).map { |data| ApplicationCommand.new(data, self, server_id) }
897
+ end
898
+
870
899
  # @yieldparam [OptionBuilder]
871
900
  # @yieldparam [PermissionBuilder]
872
901
  # @example
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'lru_redux'
3
4
  require 'onyxcord/api'
4
5
  require 'onyxcord/api/server'
5
6
  require 'onyxcord/api/invite'
@@ -26,17 +27,15 @@ module OnyxCord
26
27
  # Initializes this cache
27
28
  def init_cache
28
29
  @cache_policy ||= OnyxCord.configuration.normalize_cache(:full)
30
+ sizes = OnyxCord.configuration.cache_sizes
29
31
 
30
- @users = cache_enabled?(:users) ? {} : nil
31
-
32
+ @users = cache_enabled?(:users) ? LruRedux::ThreadSafeCache.new(sizes.users) : nil
32
33
  @voice_regions = cache_enabled?(:voice_regions) ? {} : nil
33
-
34
- @servers = cache_enabled?(:servers) ? {} : nil
35
-
36
- @channels = cache_enabled?(:channels) ? {} : nil
37
- @pm_channels = cache_enabled?(:pm_channels) ? {} : nil
38
- @thread_members = cache_enabled?(:thread_members) ? {} : nil
39
- @server_previews = cache_enabled?(:server_previews) ? {} : nil
34
+ @servers = cache_enabled?(:servers) ? LruRedux::ThreadSafeCache.new(sizes.servers) : nil
35
+ @channels = cache_enabled?(:channels) ? LruRedux::ThreadSafeCache.new(sizes.channels) : nil
36
+ @pm_channels = cache_enabled?(:pm_channels) ? LruRedux::ThreadSafeCache.new(sizes.pm_channels) : nil
37
+ @thread_members = cache_enabled?(:thread_members) ? LruRedux::ThreadSafeCache.new(sizes.thread_members) : nil
38
+ @server_previews = cache_enabled?(:server_previews) ? LruRedux::ThreadSafeCache.new(sizes.server_previews) : nil
40
39
  end
41
40
 
42
41
  def cache_enabled?(key)
@@ -46,7 +45,7 @@ module OnyxCord
46
45
  def cache_stats
47
46
  CACHE_STORES.each_with_object({}) do |(key, ivar), stats|
48
47
  store = instance_variable_get(ivar)
49
- stats[key] = store.respond_to?(:size) ? store.size : 0
48
+ stats[key] = store.respond_to?(:count) ? store.count : 0
50
49
  end
51
50
  end
52
51
 
@@ -56,7 +55,7 @@ module OnyxCord
56
55
  keys.each_with_object({}) do |key, pruned|
57
56
  ivar = CACHE_STORES.fetch(key)
58
57
  store = instance_variable_get(ivar)
59
- pruned[key] = store.respond_to?(:size) ? store.size : 0
58
+ pruned[key] = store.respond_to?(:count) ? store.count : 0
60
59
  store&.clear
61
60
  end
62
61
  end