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 +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +60 -0
- data/Gemfile +0 -1
- data/README.md +64 -1
- data/lib/onyxcord/api/channel.rb +1 -13
- data/lib/onyxcord/api/webhook.rb +1 -1
- data/lib/onyxcord/api.rb +109 -53
- data/lib/onyxcord/bot.rb +48 -19
- data/lib/onyxcord/cache.rb +10 -11
- data/lib/onyxcord/commands/command_bot.rb +11 -17
- data/lib/onyxcord/configuration.rb +44 -1
- data/lib/onyxcord/data/interaction.rb +15 -13
- data/lib/onyxcord/event_executor.rb +81 -12
- data/lib/onyxcord/gateway.rb +94 -505
- data/lib/onyxcord/http.rb +115 -0
- data/lib/onyxcord/json.rb +49 -0
- data/lib/onyxcord/rate_limiter/rest.rb +1 -1
- data/lib/onyxcord/version.rb +1 -1
- data/lib/onyxcord/voice/network.rb +2 -2
- data/lib/onyxcord/webhooks/builder.rb +150 -0
- data/lib/onyxcord/webhooks/client.rb +184 -0
- data/lib/onyxcord/webhooks/embeds.rb +250 -0
- data/lib/onyxcord/webhooks/modal.rb +284 -0
- data/lib/onyxcord/webhooks/version.rb +9 -0
- data/lib/onyxcord/webhooks/view.rb +578 -0
- data/lib/onyxcord/websocket.rb +46 -40
- data/lib/onyxcord.rb +5 -0
- data/onyxcord-webhooks.gemspec +14 -5
- data/onyxcord.gemspec +20 -7
- metadata +116 -31
- 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: 2e5d5e025fe9ad9abb42d0fca6da5fe8bf9f32bfbe527de27a2da742bfe5f262
|
|
4
|
+
data.tar.gz: e7b494fd1d4821c2c517f24ceca4a6da27720f552618b71f5a5452c5a3ef915b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz: '
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '059ec009fb6fd52704a2906bdfc8fb6a20f0e06dd2ac783e1f466ce4cfabbe71c2fa9cb269a332e69fc3a05cdef7d8e394cfc3a3e1bfb11c94c0c7d43c0dd26d'
|
|
7
|
+
data.tar.gz: c4f5fb863ec9a636d4b7591b50afe16d4a0174c28390f5d42aa84f7ff32a2e904cfe71de4104b4425a7661cb82e71a9b114abc6b874888c52d8ad74683f14d58
|
data/.gitignore
CHANGED
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
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/channel.rb
CHANGED
|
@@ -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
|
|
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.
|
data/lib/onyxcord/api/webhook.rb
CHANGED
|
@@ -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?
|
|
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 '
|
|
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}
|
|
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
|
|
96
|
+
# Performs a raw HTTP request using HTTPX.
|
|
86
97
|
# @param type [Symbol] The type of HTTP request to use.
|
|
87
|
-
# @param
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
156
|
+
async_rate_limiter.before_request(key, major_parameter)
|
|
107
157
|
|
|
108
158
|
response = nil
|
|
109
|
-
|
|
110
|
-
response =
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
151
|
-
case
|
|
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(
|
|
211
|
+
OnyxCord::AsyncRuntime.sleep(body_data['retry_after'])
|
|
156
212
|
end
|
|
157
213
|
|
|
158
|
-
return
|
|
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 '
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
@gateway.
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
data/lib/onyxcord/cache.rb
CHANGED
|
@@ -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) ?
|
|
31
|
-
|
|
32
|
+
@users = cache_enabled?(:users) ? LruRedux::ThreadSafeCache.new(sizes.users) : nil
|
|
32
33
|
@voice_regions = cache_enabled?(:voice_regions) ? {} : nil
|
|
33
|
-
|
|
34
|
-
@
|
|
35
|
-
|
|
36
|
-
@
|
|
37
|
-
@
|
|
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?(:
|
|
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?(:
|
|
58
|
+
pruned[key] = store.respond_to?(:count) ? store.count : 0
|
|
60
59
|
store&.clear
|
|
61
60
|
end
|
|
62
61
|
end
|