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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +55 -0
- data/README.md +64 -1
- data/lib/onyxcord/api.rb +58 -55
- 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/bot.rb +47 -15
- data/lib/onyxcord/data/interaction.rb +4 -0
- data/lib/onyxcord/event_executor.rb +56 -6
- data/lib/onyxcord/gateway.rb +15 -15
- data/lib/onyxcord/http.rb +115 -0
- data/lib/onyxcord/json.rb +49 -0
- data/lib/onyxcord/rate_limiter/async_rest.rb +149 -0
- data/lib/onyxcord/version.rb +1 -1
- data/lib/onyxcord/webhooks/version.rb +1 -1
- data/lib/onyxcord/websocket.rb +2 -15
- metadata +10 -4
- data/.devcontainer/Dockerfile +0 -13
- data/.devcontainer/devcontainer.json +0 -29
- data/.devcontainer/postcreate.sh +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c794e2e7f5ad51dea52e27c049fb7d08fb7b54c84b452243fe549cd54f46dd5d
|
|
4
|
+
data.tar.gz: 2f7e1a9df9cf56085e31dd7bc13e843f8c5bee2e9083817305e6936117c92dd4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dce8555d60a8bf6662bfc357c64918e75ff803ab30c43c93dab390756ee52e0057f20914367ec4cce89e5a6906c0745f99b89f42953bb9b91648caa3fe296378
|
|
7
|
+
data.tar.gz: 7b25cd2d344182abbdce7fb5f74f8cffb7dea3ad478d6a03e9018ea138635fac13e9203184f656f677c39b85b26f1a8287f24ac6a855b5b9ae64ec12165e8c72
|
data/.gitignore
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
async_rate_limiter.before_request(key, major_parameter)
|
|
138
157
|
|
|
139
158
|
response = nil
|
|
140
|
-
|
|
159
|
+
loop do
|
|
141
160
|
response = OnyxCord::HTTP.request(type, url, body, **headers)
|
|
161
|
+
break unless response.code == 502
|
|
142
162
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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::
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
if response&.code == 429
|
|
197
|
+
if response.code == 429
|
|
193
198
|
trace("429 #{key} #{major_parameter}")
|
|
194
|
-
|
|
195
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|