onyxcord 2.0.0 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '00639da6b7f1bb86015ad2789db3c8a31a8a2865d26d594bb0bdf15914dbb534'
4
- data.tar.gz: 0ac2f4e68b91633c3d1006faf2def1d6df6a99a9898fc2c38de819d5048f41f4
3
+ metadata.gz: c794e2e7f5ad51dea52e27c049fb7d08fb7b54c84b452243fe549cd54f46dd5d
4
+ data.tar.gz: 2f7e1a9df9cf56085e31dd7bc13e843f8c5bee2e9083817305e6936117c92dd4
5
5
  SHA512:
6
- metadata.gz: ae1eab8467d518c3bd98148274b3bc152e1c9d8b72696c27fe621e069a7d2ff75ff33dddfda22b00702f25a85b38702a31accaf1d00d0366d831cc96b38cea3b
7
- data.tar.gz: dc2bb8086656c80b8d97c7992f99a75aed3efd7481a9de4c52713edbbc02536ab6074d27931b88430b92a72a3b5412e58fdff8ee5ccb4e936edd19f08c7edbc0
6
+ metadata.gz: dce8555d60a8bf6662bfc357c64918e75ff803ab30c43c93dab390756ee52e0057f20914367ec4cce89e5a6906c0745f99b89f42953bb9b91648caa3fe296378
7
+ data.tar.gz: 7b25cd2d344182abbdce7fb5f74f8cffb7dea3ad478d6a03e9018ea138635fac13e9203184f656f677c39b85b26f1a8287f24ac6a855b5b9ae64ec12165e8c72
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,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.6 - 2026-06-28
4
+
5
+ ### Correcoes de empacotamento
6
+
7
+ - Incluidos na gem os arquivos da infraestrutura async que ficaram fora do pacote `2.0.5`: `onyxcord/async/runtime` e `onyxcord/rate_limiter/async_rest`.
8
+ - Incluidos na gem os arquivos da DSL moderna de application commands: `onyxcord/application_commands` e seus componentes internos.
9
+ - Corrige `LoadError: cannot load such file -- onyxcord/async/runtime` ao usar a gem publicada.
10
+
11
+ ## 2.0.5 - 2026-06-28
12
+
13
+ ### Async Runtime (Infraestrutura nao-bloqueante)
14
+
15
+ - **`OnyxCord::AsyncRuntime`**: modulo central que gerencia o reactor `async` com `run`, `async` e `sleep`, reaproveitando reactor existente quando disponivel.
16
+ - **`EventExecutor::AsyncPool`**: novo pool de workers baseado em `Async::Queue` e fibers, sem threads.
17
+ - **Gateway**: `run_async` nao cria mais `Thread.new` — usa `@task = AsyncRuntime.async { run }`. Todos os `sleep` trocados por `AsyncRuntime.sleep`.
18
+ - **WebSocket**: usa `AsyncRuntime.async` em vez de `Async do` solto na classe.
19
+ - **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.
20
+ - **Rate Limiter Async**: novo `OnyxCord::RateLimiter::AsyncRest` que evita `mutex.synchronize { sleep }` bloqueante.
21
+ - **Bot**: `run`/`stop`/`join` refatorados para o runtime async. `send_temporary_message` e `voice_connect` usam sleeps async.
22
+ - Compatibilidade sync mantida: a API publica continua funcionando de forma sincrona quando chamada fora de um reactor.
23
+
24
+ ### Modern Application Commands DSL
25
+
26
+ - **`bot.slash`, `bot.user_command`, `bot.message_command`**: nova DSL para comandos modernos com definicao e handler unificados.
27
+ - **`bot.sync_application_commands!`**: sincroniza todos os commands registrados com a API do Discord de uma vez.
28
+ - **`bot.bulk_overwrite_global_application_commands`** e **`bot.bulk_overwrite_guild_application_commands`**: wrappers para bulk overwrite.
29
+ - **`ApplicationCommands::Context`**: wrapper com `respond`, `defer`, `edit_original`, `delete_original`, `followup` e acesso a `options`, `guild`, `channel`, `user`.
30
+ - **`Interaction#edit_original`**, **`Interaction#delete_original`**, **`Interaction#followup`**: novos aliases dos metodos originais.
31
+ - API legacy (`register_application_command` + `application_command`) mantida com compatibilidade total.
32
+
33
+ ### Exemplo da nova DSL
34
+
35
+ ```ruby
36
+ bot.slash :ban, description: "Bane um membro", default_member_permissions: [:ban_members] do
37
+ user :member, "Membro que sera banido", required: true
38
+ string :reason, "Motivo do banimento", max_length: 512
39
+
40
+ execute do |ctx|
41
+ ctx.defer(ephemeral: true)
42
+ member = ctx.options[:member]
43
+ reason = ctx.options[:reason] || "Sem motivo informado"
44
+ ctx.guild.ban(member, reason: reason)
45
+ ctx.edit_original(content: "Membro banido com sucesso.")
46
+ end
47
+ end
48
+
49
+ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
50
+ ```
51
+
52
+ ### Validacao
53
+
54
+ - `bundle exec rspec`: 460 exemplos, 0 falhas, 3 pendentes.
55
+ - `ruby -c lib/onyxcord/**/*.rb`: todos os arquivos com sintaxe OK.
56
+ - `gem build onyxcord.gemspec`: sucesso.
57
+
3
58
  ## 2.0.0 - 2026-06-28
4
59
 
5
60
  ### 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
 
@@ -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