onyxcord 2.0.0 → 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: '00639da6b7f1bb86015ad2789db3c8a31a8a2865d26d594bb0bdf15914dbb534'
4
- data.tar.gz: 0ac2f4e68b91633c3d1006faf2def1d6df6a99a9898fc2c38de819d5048f41f4
3
+ metadata.gz: 2e5d5e025fe9ad9abb42d0fca6da5fe8bf9f32bfbe527de27a2da742bfe5f262
4
+ data.tar.gz: e7b494fd1d4821c2c517f24ceca4a6da27720f552618b71f5a5452c5a3ef915b
5
5
  SHA512:
6
- metadata.gz: ae1eab8467d518c3bd98148274b3bc152e1c9d8b72696c27fe621e069a7d2ff75ff33dddfda22b00702f25a85b38702a31accaf1d00d0366d831cc96b38cea3b
7
- data.tar.gz: dc2bb8086656c80b8d97c7992f99a75aed3efd7481a9de4c52713edbbc02536ab6074d27931b88430b92a72a3b5412e58fdff8ee5ccb4e936edd19f08c7edbc0
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,52 @@
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
+
3
50
  ## 2.0.0 - 2026-06-28
4
51
 
5
52
  ### Arquitetura & Performance (Major Refactoring)
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
data/lib/onyxcord/api.rb CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  require 'onyxcord/http'
4
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
@@ -61,16 +63,25 @@ module OnyxCord::API
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 }
@@ -110,11 +121,18 @@ module OnyxCord::API
110
121
 
111
122
  # Make an API request, including rate limit handling.
112
123
  def request(key, major_parameter, type, *attributes)
113
- # Parse attributes: URL is first, body is second (if present), rest is headers 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)
114
133
  url = attributes.shift
115
134
  headers_or_body = attributes
116
135
 
117
- # Separate body and headers from the positional args
118
136
  body = nil
119
137
  headers = {}
120
138
 
@@ -126,89 +144,74 @@ module OnyxCord::API
126
144
  end
127
145
  end
128
146
 
129
- # Extract content_type from headers for HTTPX
130
147
  content_type = headers.delete(:content_type)
131
148
  headers['content-type'] = 'application/json' if content_type == :json
132
149
 
133
- # Add user agent
134
150
  headers['user-agent'] = user_agent
135
151
 
152
+ retries = 0
153
+ max_retries = key == :gateway ? 0 : 3
154
+
136
155
  begin
137
- rate_limiter.before_request(key, major_parameter)
156
+ async_rate_limiter.before_request(key, major_parameter)
138
157
 
139
158
  response = nil
140
- begin
159
+ loop do
141
160
  response = OnyxCord::HTTP.request(type, url, body, **headers)
161
+ break unless response.code == 502
142
162
 
143
- if response.code == 403
144
- noprm = OnyxCord::Errors::NoPermission.new
145
- noprm.define_singleton_method(:_response) { response }
146
- raise noprm, "The bot doesn't have the required permission to do this!"
147
- end
163
+ retries += 1
164
+ break unless retries < max_retries
148
165
 
149
- # Retry on 502
150
- if response.code == 502
151
- OnyxCord::LOGGER.warn('Got a 502 while sending a request! Not a big deal, retrying the request')
152
- return request(key, major_parameter, type, url, body, headers)
153
- end
154
-
155
- # Handle error status codes
156
- if response.code >= 400 && response.code != 429
157
- data = begin
158
- JSON.parse(response.body)
159
- rescue StandardError
160
- nil
161
- 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
162
169
 
163
- raise "HTTP #{response.code}: #{response.body}" unless data
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
164
175
 
165
- err_klass = OnyxCord::Errors.error_class_for(data['code'] || 0)
166
- e = err_klass.new(data['message'], data['errors'])
167
- OnyxCord::LOGGER.error(e.full_message)
168
- raise e
169
- end
170
- rescue OnyxCord::Errors::NoPermission => e
171
- if e.respond_to?(:_response)
172
- response = e._response
173
- else
174
- OnyxCord::LOGGER.warn("NoPermission doesn't respond_to? _response!")
176
+ if response.code >= 400 && response.code != 429
177
+ data = begin
178
+ JSON.parse(response.body)
179
+ rescue StandardError
180
+ nil
175
181
  end
176
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)
177
188
  raise e
178
- ensure
179
- if response
180
- rate_limiter.record_response(key, major_parameter, response.headers)
181
- else
182
- OnyxCord::LOGGER.ratelimit('Response was nil before trying to preemptively rate limit!')
183
- end
184
189
  end
185
- rescue OnyxCord::Errors::CodeError => e
186
- raise if e.respond_to?(:code) && e.code != 429_000
187
-
188
- raise
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)
189
195
  end
190
196
 
191
- # Handle 429 rate limiting
192
- if response&.code == 429
197
+ if response.code == 429
193
198
  trace("429 #{key} #{major_parameter}")
194
- rate_limiter.handle_rate_limit(key, major_parameter, response)
195
- return request(key, major_parameter, type, url, body, headers)
199
+ async_rate_limiter.handle_rate_limit(key, major_parameter, response)
200
+ return request_async(key, major_parameter, type, url, body, headers)
196
201
  end
197
202
 
198
- # Endpoints that use Elasticsearch can return a 202 when the index isn't ready yet. Wait the
199
- # amount of time indicated by the response body, and then recursively retry and return the request.
200
- if response&.code == 202 && response&.body
203
+ if response.code == 202 && response.body
201
204
  body_data = JSON.parse(response.body)
202
205
 
203
206
  if body_data['code'] == 110_000
204
207
  case body_data['retry_after']
205
208
  when 0, 1, nil
206
- sleep(rand(4.5..5.0))
209
+ OnyxCord::AsyncRuntime.sleep(rand(4.5..5.0))
207
210
  else
208
- sleep(body_data['retry_after'])
211
+ OnyxCord::AsyncRuntime.sleep(body_data['retry_after'])
209
212
  end
210
213
 
211
- return request(key, major_parameter, type, url, body, headers)
214
+ return request_async(key, major_parameter, type, url, body, headers)
212
215
  end
213
216
  end
214
217
 
data/lib/onyxcord/bot.rb CHANGED
@@ -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,9 +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
- Async do
481
+ OnyxCord::AsyncRuntime.async do
481
482
  message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce, poll)
482
- sleep(timeout)
483
+ OnyxCord::AsyncRuntime.sleep(timeout)
483
484
  message.delete
484
485
  end
485
486
 
@@ -864,6 +865,37 @@ module OnyxCord
864
865
  ApplicationCommand.new(JSON.parse(resp), self, server_id)
865
866
  end
866
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
+
867
899
  # @yieldparam [OptionBuilder]
868
900
  # @yieldparam [PermissionBuilder]
869
901
  # @example
@@ -299,6 +299,10 @@ module OnyxCord
299
299
  Interactions::Message.new(JSON.parse(resp), @bot, self)
300
300
  end
301
301
 
302
+ alias edit_original edit_response
303
+ alias delete_original delete_response
304
+ alias followup send_message
305
+
302
306
  # @param message [String, Integer, InteractionMessage, Message] The message created by this interaction to be edited.
303
307
  # @param content [String] The message content.
304
308
  # @param embeds [Array<Hash, Webhooks::Embed>] The embeds for the message.
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'async'
3
+ require 'onyxcord/async/runtime'
4
4
 
5
5
  module OnyxCord
6
- # Event execution strategies used by bot dispatch.
7
6
  module EventExecutor
8
7
  STOP = Object.new.freeze
9
8
 
10
- # Deterministic executor useful for tests, benchmarks, and tiny bots.
11
9
  class Inline
12
10
  def post
13
11
  yield
@@ -20,8 +18,6 @@ module OnyxCord
20
18
  end
21
19
  end
22
20
 
23
- # Async-based worker pool for event handlers.
24
- # Uses Async tasks (fibers) instead of threads for lightweight concurrency.
25
21
  class Pool
26
22
  attr_reader :queue
27
23
 
@@ -47,7 +43,6 @@ module OnyxCord
47
43
  @queue.size
48
44
  end
49
45
 
50
- # Compatibility with code that checks worker threads.
51
46
  def threads
52
47
  @workers
53
48
  end
@@ -87,6 +82,59 @@ module OnyxCord
87
82
  end
88
83
  end
89
84
 
85
+ class AsyncPool
86
+ attr_reader :queue
87
+
88
+ def initialize(size:, queue_size: nil)
89
+ raise ArgumentError, 'Pool size must be greater than zero' unless size.positive?
90
+
91
+ @size = size
92
+ @queue = ::Async::Queue.new
93
+ @closed = false
94
+ @workers = []
95
+ start_workers
96
+ end
97
+
98
+ def post(&block)
99
+ raise ArgumentError, 'EventExecutor::AsyncPool#post requires a block' unless block
100
+ raise 'Event executor has been shut down' if @closed
101
+
102
+ @queue.enqueue(block)
103
+ end
104
+
105
+ def queue_size
106
+ @queue.size
107
+ end
108
+
109
+ def shutdown
110
+ return if @closed
111
+
112
+ @closed = true
113
+ @size.times { @queue.enqueue(STOP) }
114
+ end
115
+
116
+ def threads
117
+ []
118
+ end
119
+
120
+ private
121
+
122
+ def start_workers
123
+ @workers = Array.new(@size) do
124
+ OnyxCord::AsyncRuntime.async do
125
+ loop do
126
+ job = @queue.dequeue
127
+ break if job.equal?(STOP)
128
+
129
+ job.call
130
+ rescue StandardError => e
131
+ OnyxCord::LOGGER.log_exception(e) if defined?(OnyxCord::LOGGER)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+
90
138
  module_function
91
139
 
92
140
  def build(type, workers:, queue_size: nil)
@@ -95,6 +143,8 @@ module OnyxCord
95
143
  Inline.new
96
144
  when :pool
97
145
  Pool.new(size: workers, queue_size: queue_size)
146
+ when :async_pool
147
+ AsyncPool.new(size: workers, queue_size: queue_size)
98
148
  else
99
149
  raise ArgumentError, "Unknown event executor: #{type.inspect}"
100
150
  end
@@ -3,6 +3,7 @@
3
3
  require 'async'
4
4
  require 'async/http/endpoint'
5
5
  require 'async/websocket/client'
6
+ require 'onyxcord/async/runtime'
6
7
  require 'onyxcord/rate_limiter/gateway'
7
8
 
8
9
  module OnyxCord
@@ -86,25 +87,24 @@ module OnyxCord
86
87
 
87
88
  # Connect to the gateway server inside an Async reactor.
88
89
  def run_async
89
- @ws_thread = Thread.new do
90
- Thread.current[:onyxcord_name] = 'gateway'
91
- Async do |task|
92
- @reactor_task = task
93
- connect_loop
94
- LOGGER.warn('The gateway loop exited!')
95
- end
96
- end
90
+ @task = OnyxCord::AsyncRuntime.async { run }
97
91
 
98
- LOGGER.debug('Gateway thread created! Waiting for confirmation...')
92
+ LOGGER.debug('Gateway task created! Waiting for confirmation...')
99
93
  loop do
100
- sleep(0.5)
94
+ OnyxCord::AsyncRuntime.sleep(0.5)
101
95
  break if @ws_success
102
96
  break if @should_reconnect == false
103
97
  end
104
98
  end
105
99
 
100
+ def run
101
+ @reactor_task = Async::Task.current
102
+ connect_loop
103
+ LOGGER.warn('The gateway loop exited!')
104
+ end
105
+
106
106
  def sync
107
- @ws_thread&.join
107
+ @task&.wait
108
108
  end
109
109
 
110
110
  def open?
@@ -118,7 +118,7 @@ module OnyxCord
118
118
  end
119
119
 
120
120
  def kill
121
- @ws_thread&.kill
121
+ @task&.stop
122
122
  end
123
123
 
124
124
  def notify_ready
@@ -224,7 +224,7 @@ module OnyxCord
224
224
  @heartbeat_task = @reactor_task&.async do
225
225
  loop do
226
226
  if (@session && !@session.suspended?) || !@session
227
- sleep @heartbeat_interval
227
+ OnyxCord::AsyncRuntime.sleep(@heartbeat_interval)
228
228
  if !@closed && @connection
229
229
  @bot.raise_heartbeat_event
230
230
  heartbeat
@@ -232,7 +232,7 @@ module OnyxCord
232
232
  LOGGER.debug('Tried to heartbeat without connection — skipping.')
233
233
  end
234
234
  else
235
- sleep 1
235
+ OnyxCord::AsyncRuntime.sleep(1)
236
236
  end
237
237
  rescue StandardError => e
238
238
  LOGGER.error('Error while heartbeating!')
@@ -260,7 +260,7 @@ module OnyxCord
260
260
 
261
261
  def wait_for_reconnect
262
262
  LOGGER.debug("Reconnecting in #{@falloff} seconds.")
263
- sleep @falloff
263
+ OnyxCord::AsyncRuntime.sleep(@falloff)
264
264
  @falloff *= 1.5
265
265
  @falloff = 115 + (rand * 10) if @falloff > 120
266
266
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httpx'
4
+ require 'onyxcord/json'
5
+
6
+ module OnyxCord
7
+ # Modern HTTP adapter wrapping HTTPX with persistent connections, automatic
8
+ # retries on 502, and a response interface compatible with the rest of OnyxCord.
9
+ module HTTP
10
+ # Lightweight response wrapper so call-sites that relied on RestClient's
11
+ # `.body`, `.headers`, `.code` interface keep working unchanged.
12
+ class Response
13
+ # @return [String] the response body
14
+ attr_reader :body
15
+
16
+ # @return [Integer] the HTTP status code
17
+ attr_reader :code
18
+
19
+ # @return [Hash] the response headers (symbol keys, underscored)
20
+ attr_reader :headers
21
+
22
+ def initialize(httpx_response)
23
+ @raw = httpx_response
24
+ @body = httpx_response.body.to_s
25
+ @code = httpx_response.status
26
+ @headers = normalize_headers(httpx_response.headers)
27
+ end
28
+
29
+ def to_s
30
+ @body
31
+ end
32
+
33
+ # RestClient compatibility — some code calls `response` directly as a
34
+ # string (implicit to_s).
35
+ alias_method :to_str, :to_s
36
+
37
+ private
38
+
39
+ def normalize_headers(headers)
40
+ headers.to_h.transform_keys do |key|
41
+ key.to_s.tr('-', '_').downcase.to_sym
42
+ end
43
+ end
44
+ end
45
+
46
+ module_function
47
+
48
+ # The shared HTTPX session with persistent connections.
49
+ def session
50
+ @session ||= HTTPX.plugin(:persistent)
51
+ .plugin(:follow_redirects)
52
+ end
53
+
54
+ # Reset the HTTP session (useful for tests).
55
+ def reset!
56
+ @session = nil
57
+ end
58
+
59
+ # Perform a raw HTTP request and return a {Response}.
60
+ # @param type [Symbol] HTTP method (:get, :post, :put, :patch, :delete)
61
+ # @param url [String] The full URL.
62
+ # @param body [String, Hash, nil] The request body.
63
+ # @param headers [Hash] Request headers.
64
+ # @return [Response]
65
+ def request(type, url, body = nil, **headers)
66
+ http = session.with(headers: headers)
67
+
68
+ raw = case type
69
+ when :get
70
+ http.get(url)
71
+ when :post
72
+ if body.is_a?(Hash) && body.any? { |_, v| v.respond_to?(:read) || v.respond_to?(:path) }
73
+ # Multipart upload
74
+ http.plugin(:multipart).post(url, form: body)
75
+ else
76
+ http.post(url, body: body)
77
+ end
78
+ when :put
79
+ http.put(url, body: body)
80
+ when :patch
81
+ http.patch(url, body: body)
82
+ when :delete
83
+ http.delete(url)
84
+ else
85
+ raise ArgumentError, "Unknown HTTP method: #{type}"
86
+ end
87
+
88
+ # HTTPX returns an error response object on network failures
89
+ raise raw.error if raw.is_a?(HTTPX::ErrorResponse)
90
+
91
+ Response.new(raw)
92
+ end
93
+
94
+ # Convenience wrappers
95
+ def get(url, **headers)
96
+ request(:get, url, nil, **headers)
97
+ end
98
+
99
+ def post(url, body = nil, **headers)
100
+ request(:post, url, body, **headers)
101
+ end
102
+
103
+ def put(url, body = nil, **headers)
104
+ request(:put, url, body, **headers)
105
+ end
106
+
107
+ def patch(url, body = nil, **headers)
108
+ request(:patch, url, body, **headers)
109
+ end
110
+
111
+ def delete(url, **headers)
112
+ request(:delete, url, nil, **headers)
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+
5
+ # Configure Oj in compat mode so that all stdlib JSON.parse / .to_json calls
6
+ # are transparently accelerated. This is loaded early by the main onyxcord.rb
7
+ # entry point so every module benefits automatically.
8
+ module OnyxCord
9
+ # Fast Oj-backed JSON wrapper providing compatibility with stdlib JSON methods.
10
+ module JSON
11
+ Oj.default_options = { mode: :compat, symbol_keys: false }
12
+
13
+ module_function
14
+
15
+ # Fast JSON decode using Oj via stdlib JSON compatibility.
16
+ # @param data [String] The JSON string to decode.
17
+ # @return [Hash, Array, String, Numeric, nil]
18
+ def decode(data, *args)
19
+ ::JSON.parse(data, *args)
20
+ end
21
+
22
+ # Fast JSON encode using Oj via stdlib JSON compatibility.
23
+ # @param data [Object] The object to encode as JSON.
24
+ # @return [String]
25
+ def encode(data, *args)
26
+ ::JSON.generate(data, *args)
27
+ end
28
+
29
+ # Alias parse to decode for standard library compatibility
30
+ def parse(data, *args)
31
+ ::JSON.parse(data, *args)
32
+ end
33
+
34
+ # Alias generate to encode for standard library compatibility
35
+ def generate(data, *args)
36
+ ::JSON.generate(data, *args)
37
+ end
38
+
39
+ # Alias load to decode for Oj/JSON compatibility
40
+ def load(data, *args)
41
+ ::JSON.parse(data, *args)
42
+ end
43
+
44
+ # Alias dump to encode for Oj/JSON compatibility
45
+ def dump(data, *args)
46
+ ::JSON.generate(data, *args)
47
+ end
48
+ end
49
+ 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.5'
7
7
  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.5
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"
@@ -474,7 +471,9 @@ files:
474
471
  - lib/onyxcord/events/voice_state_update.rb
475
472
  - lib/onyxcord/events/webhooks.rb
476
473
  - lib/onyxcord/gateway.rb
474
+ - lib/onyxcord/http.rb
477
475
  - lib/onyxcord/id_object.rb
476
+ - lib/onyxcord/json.rb
478
477
  - lib/onyxcord/light.rb
479
478
  - lib/onyxcord/light/data.rb
480
479
  - lib/onyxcord/light/integrations.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