onyxcord 2.0.8 → 2.1.0
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/CHANGELOG.md +89 -1
- data/lib/onyxcord/allowed_mentions.rb +8 -0
- data/lib/onyxcord/api/channel.rb +8 -10
- data/lib/onyxcord/api/interaction.rb +4 -9
- data/lib/onyxcord/api/webhook.rb +9 -11
- data/lib/onyxcord/api.rb +64 -9
- data/lib/onyxcord/async/runtime.rb +1 -1
- data/lib/onyxcord/bot.rb +18 -4
- data/lib/onyxcord/data/interaction.rb +13 -5
- data/lib/onyxcord/data/message.rb +6 -2
- data/lib/onyxcord/data/server.rb +4 -2
- data/lib/onyxcord/data/voice_state.rb +5 -1
- data/lib/onyxcord/data/webhook.rb +5 -3
- data/lib/onyxcord/errors.rb +18 -3
- data/lib/onyxcord/events/channels.rb +17 -1
- data/lib/onyxcord/events/raw.rb +1 -1
- data/lib/onyxcord/events/voice_state_update.rb +36 -5
- data/lib/onyxcord/gateway.rb +22 -5
- data/lib/onyxcord/http.rb +79 -8
- data/lib/onyxcord/message_payload.rb +78 -0
- data/lib/onyxcord/rate_limiter/async_rest.rb +3 -2
- data/lib/onyxcord/upload.rb +39 -0
- data/lib/onyxcord/version.rb +1 -1
- data/lib/onyxcord/webhooks/version.rb +1 -1
- data/lib/onyxcord.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df2a03d17ca81fd7a31574dbb8f94a53fc18b602a87ccd77d8390c472bb8a89b
|
|
4
|
+
data.tar.gz: 970063416e35a36c1f1f8c97d706a9e6a4895b5d7c1ec44eb19ab1e923b0fdcc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96fd63b818d4e7abcd493f747444692951ec8c2c45a17efdb7772ae5de0aee444f3a22dc410067b8585ad803baa300d52d54856dd3efeddd8938a207d0aa3037
|
|
7
|
+
data.tar.gz: 37d0a1e7c88edc3ced210a92a9bceefd9f980a9280f0802538efed8b4b0de17f92120924a1c2a0121a55d07a33aa9915a2cafca2b915f1630f47e4e7f3049828
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0 - 2026-07-01
|
|
4
|
+
|
|
5
|
+
### Melhorias de REST e gateway
|
|
6
|
+
|
|
7
|
+
- Adiciona `OnyxCord::MessagePayload` e `OnyxCord::Upload` para centralizar payloads, anexos e multipart.
|
|
8
|
+
- Melhora rate limit global async para bloquear novas requests enquanto o limite global esta ativo.
|
|
9
|
+
- Adiciona retry REST para falhas temporarias `500`, `502`, `503`, `504` e erros de conexao.
|
|
10
|
+
- Adiciona erros HTTP tipados com `status`, `code`, `headers`, `route`, `body` e `response`.
|
|
11
|
+
- Torna o reconnect do gateway mais defensivo: quedas antes do `HELLO` voltam para `IDENTIFY`, quedas depois do `HELLO` tentam `RESUME`.
|
|
12
|
+
- Adiciona `AllowedMentions.none` e `AllowedMentions.all`.
|
|
13
|
+
- Ajusta edicao de mensagens para limpar `embeds` ao editar `content`, limpar `content` ao editar `embeds`, e preservar campos com `:keep`.
|
|
14
|
+
- Valida payloads antes da API para limites de embeds/anexos e combinacoes invalidas de Components V2.
|
|
15
|
+
|
|
16
|
+
## 2.0.20 - 2026-06-30
|
|
17
|
+
|
|
18
|
+
### Correcoes de REST
|
|
19
|
+
|
|
20
|
+
- Porta o formato multipart do disnake: lista de partes `files[n]` e `payload_json`, com reset do arquivo antes do envio.
|
|
21
|
+
|
|
22
|
+
## 2.0.19 - 2026-06-30
|
|
23
|
+
|
|
24
|
+
### Correcoes de REST
|
|
25
|
+
|
|
26
|
+
- Adiciona diagnostico de metodo, URL sanitizada, headers e formato do body em erros HTTP sem JSON.
|
|
27
|
+
|
|
28
|
+
## 2.0.18 - 2026-06-30
|
|
29
|
+
|
|
30
|
+
### Correcoes de REST
|
|
31
|
+
|
|
32
|
+
- Normaliza multipart de followup webhook com `payload_json` primeiro e sem campos vazios.
|
|
33
|
+
|
|
34
|
+
## 2.0.17 - 2026-06-30
|
|
35
|
+
|
|
36
|
+
### Correcoes de REST
|
|
37
|
+
|
|
38
|
+
- Envia `payload_json` com `Content-Type: application/json` e `.txt` como `text/plain` em multipart.
|
|
39
|
+
|
|
40
|
+
## 2.0.16 - 2026-06-30
|
|
41
|
+
|
|
42
|
+
### Correcoes de REST
|
|
43
|
+
|
|
44
|
+
- Forca HTTP/1.1 na sessao REST do HTTPX para evitar `protocol_error` em interacoes e followups.
|
|
45
|
+
|
|
46
|
+
## 2.0.15 - 2026-06-30
|
|
47
|
+
|
|
48
|
+
### Correcoes de REST
|
|
49
|
+
|
|
50
|
+
- Envia uploads `multipart/form-data` por `Net::HTTP` no adaptador REST para evitar rejeicao `HTTP 400` do Cloudflare em anexos.
|
|
51
|
+
|
|
52
|
+
## 2.0.14 - 2026-06-30
|
|
53
|
+
|
|
54
|
+
### Correcoes de REST
|
|
55
|
+
|
|
56
|
+
- Corrige uploads `multipart/form-data` no adaptador HTTPX usando `form:` nativo, evitando `HTTP 400` HTML do Cloudflare em anexos.
|
|
57
|
+
|
|
58
|
+
## 2.0.13 - 2026-06-28
|
|
59
|
+
|
|
60
|
+
### Correcoes de gateway
|
|
61
|
+
|
|
62
|
+
- Evita chamadas REST em `Bot#update_voice_state` ao processar `VOICE_STATE_UPDATE`.
|
|
63
|
+
- Torna `GUILD_UPDATE` tolerante a guild ainda nao cacheada.
|
|
64
|
+
|
|
65
|
+
## 2.0.12 - 2026-06-28
|
|
66
|
+
|
|
67
|
+
### Correcoes de gateway
|
|
68
|
+
|
|
69
|
+
- Evita chamadas REST durante a criação de eventos do Gateway para `VOICE_STATE_UPDATE` e `CHANNEL_CREATE`.
|
|
70
|
+
- Trata `Async::Cancel` durante dispatch do Gateway como cancelamento esperado, sem registrar erro fatal.
|
|
71
|
+
|
|
72
|
+
## 2.0.11 - 2026-06-28
|
|
73
|
+
|
|
74
|
+
### Correcoes de estabilidade
|
|
75
|
+
|
|
76
|
+
- Isola a sessao HTTP persistente por thread para evitar que tarefas em segundo plano disputem a mesma conexao REST das interacoes.
|
|
77
|
+
|
|
78
|
+
## 2.0.10 - 2026-06-28
|
|
79
|
+
|
|
80
|
+
### Correcoes de voz
|
|
81
|
+
|
|
82
|
+
- Mantem `channel_id` no cache de `VoiceState` mesmo quando o objeto do canal ainda nao foi resolvido.
|
|
83
|
+
- Expoe `channel_id` e `old_channel_id` em `VoiceStateUpdateEvent`, corrigindo entradas em call que pareciam `nil -> nil`.
|
|
84
|
+
- Permite que raw dispatch handlers filtrem pacotes com chaves string ou symbol.
|
|
85
|
+
|
|
86
|
+
## 2.0.9 - 2026-06-28
|
|
87
|
+
|
|
88
|
+
### Correcoes de estabilidade
|
|
89
|
+
|
|
90
|
+
- Tolera um heartbeat sem ACK antes de reconectar, evitando queda por ACK atrasado isolado.
|
|
91
|
+
|
|
3
92
|
## 2.0.8 - 2026-06-28
|
|
4
93
|
|
|
5
94
|
### Correcoes de gateway
|
|
@@ -166,4 +255,3 @@ bot.sync_application_commands!(server_id: ENV.fetch('DISCORD_SERVER_ID'))
|
|
|
166
255
|
- `bundle exec rspec`: 456 exemplos, 0 falhas, 3 pendentes.
|
|
167
256
|
- RuboCop nos arquivos alterados: sem offenses.
|
|
168
257
|
- `gem build onyxcord.gemspec`: sucesso.
|
|
169
|
-
- `gem build onyxcord-webhooks.gemspec`: sucesso.
|
|
@@ -30,6 +30,14 @@ module OnyxCord
|
|
|
30
30
|
@replied_user = replied_user
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def self.none
|
|
34
|
+
new(parse: [], users: [], roles: [], replied_user: false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.all
|
|
38
|
+
new(parse: %w[users roles everyone], replied_user: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
33
41
|
# @!visibility private
|
|
34
42
|
def to_hash
|
|
35
43
|
{
|
data/lib/onyxcord/api/channel.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'onyxcord/
|
|
3
|
+
require 'onyxcord/message_payload'
|
|
4
4
|
|
|
5
5
|
# API calls for Channel
|
|
6
6
|
module OnyxCord::API::Channel
|
|
@@ -9,18 +9,12 @@ module OnyxCord::API::Channel
|
|
|
9
9
|
# Build attachment metadata payload for multipart uploads.
|
|
10
10
|
# Returns an array of { id:, filename: } hashes.
|
|
11
11
|
def attachment_payload(attachments)
|
|
12
|
-
|
|
13
|
-
{ id: index, filename: File.basename(attachment.path) }
|
|
14
|
-
end
|
|
12
|
+
OnyxCord::MessagePayload.attachment_payload(attachments)
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
# Build multipart body with named file fields and JSON payload.
|
|
18
16
|
def multipart_body(body, attachments)
|
|
19
|
-
|
|
20
|
-
["files[#{index}]", attachment]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
{ **files, payload_json: body.to_json }
|
|
17
|
+
OnyxCord::MessagePayload.multipart_body(body, attachments)
|
|
24
18
|
end
|
|
25
19
|
|
|
26
20
|
# Get a channel's data
|
|
@@ -113,6 +107,7 @@ module OnyxCord::API::Channel
|
|
|
113
107
|
tts = false unless [true, false].include?(tts)
|
|
114
108
|
components = OnyxCord::MessageComponents.payload(components) unless components.nil?
|
|
115
109
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
|
|
110
|
+
OnyxCord::MessagePayload.validate!(content: message, embeds: embeds, components: components, flags: flags, attachments: attachments, poll: poll)
|
|
116
111
|
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
|
|
117
112
|
body = if attachments
|
|
118
113
|
multipart_body(body, attachments)
|
|
@@ -151,12 +146,14 @@ module OnyxCord::API::Channel
|
|
|
151
146
|
def edit_message(token, channel_id, message_id, message, mentions = nil, embeds = nil, components = nil, flags = nil)
|
|
152
147
|
components = OnyxCord::MessageComponents.payload(components) unless components.nil? || components == :undef
|
|
153
148
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
|
|
149
|
+
body = OnyxCord::MessagePayload.edit_body(message, embeds)
|
|
150
|
+
body.merge!(allowed_mentions: mentions, components: components, flags: flags)
|
|
154
151
|
OnyxCord::API.request(
|
|
155
152
|
:channels_cid_messages_mid,
|
|
156
153
|
channel_id,
|
|
157
154
|
:patch,
|
|
158
155
|
"#{OnyxCord::API.api_base}/channels/#{channel_id}/messages/#{message_id}",
|
|
159
|
-
|
|
156
|
+
body.reject { |_, v| v == :undef }.to_json,
|
|
160
157
|
Authorization: token,
|
|
161
158
|
content_type: :json
|
|
162
159
|
)
|
|
@@ -655,6 +652,7 @@ module OnyxCord::API::Channel
|
|
|
655
652
|
# Start a thread in a forum or media channel.
|
|
656
653
|
# https://discord.com/developers/docs/resources/channel#start-thread-in-forum-or-media-channel
|
|
657
654
|
def start_thread_in_forum_or_media_channel(token, channel_id, name, message, attachments = nil, rate_limit_per_user = nil, auto_archive_duration = nil, applied_tags = nil, reason = nil)
|
|
655
|
+
OnyxCord::MessagePayload.validate!(attachments: attachments)
|
|
658
656
|
body = { name: name, message: message, rate_limit_per_user: rate_limit_per_user, auto_archive_duration: auto_archive_duration, applied_tags: applied_tags }.compact
|
|
659
657
|
|
|
660
658
|
body = if attachments
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'onyxcord/
|
|
3
|
+
require 'onyxcord/message_payload'
|
|
4
4
|
|
|
5
5
|
# API calls for interactions.
|
|
6
6
|
module OnyxCord::API::Interaction
|
|
@@ -9,18 +9,12 @@ module OnyxCord::API::Interaction
|
|
|
9
9
|
# Build attachment metadata payload for multipart uploads.
|
|
10
10
|
# Returns an array of { id:, filename: } hashes.
|
|
11
11
|
def attachment_payload(attachments)
|
|
12
|
-
|
|
13
|
-
{ id: index, filename: File.basename(attachment.path) }
|
|
14
|
-
end
|
|
12
|
+
OnyxCord::MessagePayload.attachment_payload(attachments)
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
# Build multipart body with named file fields and JSON payload.
|
|
18
16
|
def multipart_body(body, attachments)
|
|
19
|
-
|
|
20
|
-
["files[#{index}]", attachment]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
{ **files, payload_json: body.to_json }
|
|
17
|
+
OnyxCord::MessagePayload.multipart_body(body, attachments)
|
|
24
18
|
end
|
|
25
19
|
|
|
26
20
|
# Respond to an interaction.
|
|
@@ -28,6 +22,7 @@ module OnyxCord::API::Interaction
|
|
|
28
22
|
def create_interaction_response(interaction_token, interaction_id, type, content = nil, tts = nil, embeds = nil, allowed_mentions = nil, flags = nil, components = nil, attachments = nil, choices = nil, with_response = nil, poll = nil)
|
|
29
23
|
components = OnyxCord::MessageComponents.payload(components) unless components.nil?
|
|
30
24
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
|
|
25
|
+
OnyxCord::MessagePayload.validate!(content: content, embeds: embeds, components: components, flags: flags, attachments: attachments, poll: poll)
|
|
31
26
|
data = { tts: tts, content: content, embeds: embeds, allowed_mentions: allowed_mentions, flags: flags, components: components, attachments: attachments ? attachment_payload(attachments) : nil, choices: choices, poll: poll }.compact
|
|
32
27
|
|
|
33
28
|
body = if attachments
|
data/lib/onyxcord/api/webhook.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'onyxcord/
|
|
3
|
+
require 'onyxcord/message_payload'
|
|
4
4
|
|
|
5
5
|
# API calls for Webhook object
|
|
6
6
|
module OnyxCord::API::Webhook
|
|
@@ -9,18 +9,12 @@ module OnyxCord::API::Webhook
|
|
|
9
9
|
# Build attachment metadata payload for multipart uploads.
|
|
10
10
|
# Returns an array of { id:, filename: } hashes.
|
|
11
11
|
def attachment_payload(attachments)
|
|
12
|
-
|
|
13
|
-
{ id: index, filename: File.basename(attachment.path) }
|
|
14
|
-
end
|
|
12
|
+
OnyxCord::MessagePayload.attachment_payload(attachments)
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
# Build multipart body with named file fields and JSON payload.
|
|
18
16
|
def multipart_body(body, attachments)
|
|
19
|
-
|
|
20
|
-
["files[#{index}]", attachment]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
{ **files, payload_json: body.to_json }
|
|
17
|
+
OnyxCord::MessagePayload.multipart_body(body, attachments)
|
|
24
18
|
end
|
|
25
19
|
|
|
26
20
|
# Get a webhook
|
|
@@ -49,9 +43,12 @@ module OnyxCord::API::Webhook
|
|
|
49
43
|
# Execute a webhook via token.
|
|
50
44
|
# https://discord.com/developers/docs/resources/webhook#execute-webhook
|
|
51
45
|
def token_execute_webhook(webhook_token, webhook_id, wait = false, content = nil, username = nil, avatar_url = nil, tts = nil, file = nil, embeds = nil, allowed_mentions = nil, flags = nil, components = nil, attachments = nil, poll = nil)
|
|
46
|
+
raise ArgumentError, 'cannot mix file and attachments' if file && attachments
|
|
47
|
+
|
|
52
48
|
components = OnyxCord::MessageComponents.payload(components) unless components.nil?
|
|
53
49
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
|
|
54
|
-
|
|
50
|
+
OnyxCord::MessagePayload.validate!(content: content, embeds: embeds, components: components, flags: flags, attachments: attachments, poll: poll)
|
|
51
|
+
body = { content: content, username: username, avatar_url: avatar_url, tts: tts == true ? true : nil, embeds: embeds&.map(&:to_hash), allowed_mentions: allowed_mentions, flags: flags, components: components&.any? ? components : nil, attachments: attachments ? attachment_payload(attachments) : nil, poll: poll }.compact
|
|
55
52
|
|
|
56
53
|
body = if file
|
|
57
54
|
{ file: file, payload_json: body.to_json }
|
|
@@ -145,7 +142,8 @@ module OnyxCord::API::Webhook
|
|
|
145
142
|
def token_edit_message(webhook_token, webhook_id, message_id, content = nil, embeds = nil, allowed_mentions = nil, components = nil, attachments = nil, flags = nil, poll = nil)
|
|
146
143
|
components = OnyxCord::MessageComponents.payload(components) unless components.nil?
|
|
147
144
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
|
|
148
|
-
|
|
145
|
+
OnyxCord::MessagePayload.validate!(content: content, embeds: embeds, components: components, flags: flags, attachments: attachments, poll: poll)
|
|
146
|
+
body = OnyxCord::MessagePayload.edit_body(content, embeds).merge({ allowed_mentions: allowed_mentions, components: components, attachments: attachments ? attachment_payload(attachments) : nil, flags: flags, poll: poll }.compact)
|
|
149
147
|
|
|
150
148
|
body = if attachments
|
|
151
149
|
multipart_body(body, attachments)
|
data/lib/onyxcord/api.rb
CHANGED
|
@@ -105,9 +105,17 @@ module OnyxCord::API
|
|
|
105
105
|
response = OnyxCord::HTTP.request(type, url, body, **headers)
|
|
106
106
|
|
|
107
107
|
if response.code == 403
|
|
108
|
-
|
|
108
|
+
route = request_diagnostic(type, url, body, headers)
|
|
109
|
+
noprm = OnyxCord::Errors::NoPermission.new(
|
|
110
|
+
"The bot doesn't have the required permission to do this!",
|
|
111
|
+
status: response.code,
|
|
112
|
+
headers: response.headers,
|
|
113
|
+
route: route,
|
|
114
|
+
body: response.body,
|
|
115
|
+
response: response
|
|
116
|
+
)
|
|
109
117
|
noprm.define_singleton_method(:_response) { response }
|
|
110
|
-
raise noprm
|
|
118
|
+
raise noprm
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
# Retry on 502 Bad Gateway
|
|
@@ -157,20 +165,38 @@ module OnyxCord::API
|
|
|
157
165
|
|
|
158
166
|
response = nil
|
|
159
167
|
loop do
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
begin
|
|
169
|
+
response = OnyxCord::HTTP.request(type, url, body, **headers)
|
|
170
|
+
rescue StandardError => e
|
|
171
|
+
retries += 1
|
|
172
|
+
raise unless retries < max_retries
|
|
173
|
+
|
|
174
|
+
OnyxCord::LOGGER.warn("Temporary HTTP failure while sending request (#{e.class}), retrying")
|
|
175
|
+
OnyxCord::AsyncRuntime.sleep(retries * 0.5)
|
|
176
|
+
next
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
break unless [500, 502, 503, 504].include?(response.code)
|
|
162
180
|
|
|
163
181
|
retries += 1
|
|
164
182
|
break unless retries < max_retries
|
|
165
183
|
|
|
166
|
-
OnyxCord::LOGGER.warn(
|
|
184
|
+
OnyxCord::LOGGER.warn("Got HTTP #{response.code} while sending a request, retrying")
|
|
167
185
|
OnyxCord::AsyncRuntime.sleep(retries * 0.5)
|
|
168
186
|
end
|
|
169
187
|
|
|
170
188
|
if response.code == 403
|
|
171
|
-
|
|
189
|
+
route = request_diagnostic(type, url, body, headers)
|
|
190
|
+
noprm = OnyxCord::Errors::NoPermission.new(
|
|
191
|
+
"The bot doesn't have the required permission to do this!",
|
|
192
|
+
status: response.code,
|
|
193
|
+
headers: response.headers,
|
|
194
|
+
route: route,
|
|
195
|
+
body: response.body,
|
|
196
|
+
response: response
|
|
197
|
+
)
|
|
172
198
|
noprm.define_singleton_method(:_response) { response }
|
|
173
|
-
raise noprm
|
|
199
|
+
raise noprm
|
|
174
200
|
end
|
|
175
201
|
|
|
176
202
|
if response.code >= 400 && response.code != 429
|
|
@@ -180,10 +206,20 @@ module OnyxCord::API
|
|
|
180
206
|
nil
|
|
181
207
|
end
|
|
182
208
|
|
|
183
|
-
|
|
209
|
+
route = request_diagnostic(type, url, body, headers)
|
|
210
|
+
unless data
|
|
211
|
+
raise OnyxCord::Errors::HTTPError.new(
|
|
212
|
+
"HTTP #{response.code} #{route}: #{response.body}",
|
|
213
|
+
status: response.code,
|
|
214
|
+
headers: response.headers,
|
|
215
|
+
route: route,
|
|
216
|
+
body: response.body,
|
|
217
|
+
response: response
|
|
218
|
+
)
|
|
219
|
+
end
|
|
184
220
|
|
|
185
221
|
err_klass = OnyxCord::Errors.error_class_for(data['code'] || 0)
|
|
186
|
-
e = err_klass.new(data['message'], data['errors'])
|
|
222
|
+
e = err_klass.new(data['message'], data['errors'], status: response.code, headers: response.headers, route: route, body: response.body, response: response)
|
|
187
223
|
OnyxCord::LOGGER.error(e.full_message)
|
|
188
224
|
raise e
|
|
189
225
|
end
|
|
@@ -243,6 +279,25 @@ module OnyxCord::API
|
|
|
243
279
|
end
|
|
244
280
|
end
|
|
245
281
|
|
|
282
|
+
def request_diagnostic(type, url, body, headers)
|
|
283
|
+
clean_url = url.to_s.sub(%r{/webhooks/(\d+)/[^?]+}, '/webhooks/\1/[token]')
|
|
284
|
+
.sub(%r{/interactions/(\d+)/[^/]+}, '/interactions/\1/[token]')
|
|
285
|
+
header_keys = headers.keys.map(&:to_s).sort.join(',')
|
|
286
|
+
body_info = if body.is_a?(Hash)
|
|
287
|
+
body.map do |key, value|
|
|
288
|
+
path = value.path if value.respond_to?(:path)
|
|
289
|
+
detail = path ? "file:#{File.basename(path)}" : value.to_s.bytesize
|
|
290
|
+
"#{key}=#{detail}"
|
|
291
|
+
end.join(',')
|
|
292
|
+
else
|
|
293
|
+
body.nil? ? 'nil' : "#{body.class}:#{body.to_s.bytesize}"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
"(#{type.to_s.upcase} #{clean_url} headers=#{header_keys} body=#{body_info})"
|
|
297
|
+
rescue StandardError => e
|
|
298
|
+
"(diagnostic_failed=#{e.class}: #{e.message})"
|
|
299
|
+
end
|
|
300
|
+
|
|
246
301
|
# Make an icon URL from server and icon IDs
|
|
247
302
|
def icon_url(server_id, icon_id, format = 'webp')
|
|
248
303
|
"#{cdn_url}/icons/#{server_id}/#{icon_id}.#{format}"
|
data/lib/onyxcord/bot.rb
CHANGED
|
@@ -1119,12 +1119,12 @@ module OnyxCord
|
|
|
1119
1119
|
@session_id = data['session_id']
|
|
1120
1120
|
|
|
1121
1121
|
server_id = data['guild_id'].to_i
|
|
1122
|
-
server =
|
|
1122
|
+
server = @servers&.[](server_id)
|
|
1123
1123
|
return unless server
|
|
1124
1124
|
|
|
1125
1125
|
user_id = data['user_id'].to_i
|
|
1126
1126
|
old_voice_state = server.voice_states[user_id]
|
|
1127
|
-
old_channel_id = old_voice_state
|
|
1127
|
+
old_channel_id = old_voice_state&.channel_id || old_voice_state&.voice_channel&.id
|
|
1128
1128
|
|
|
1129
1129
|
server.update_voice_state(data)
|
|
1130
1130
|
|
|
@@ -1132,7 +1132,8 @@ module OnyxCord
|
|
|
1132
1132
|
if user_id == @profile.id && existing_voice
|
|
1133
1133
|
new_channel_id = data['channel_id']
|
|
1134
1134
|
if new_channel_id
|
|
1135
|
-
|
|
1135
|
+
channel_id = new_channel_id.to_i
|
|
1136
|
+
new_channel = @channels&.[](channel_id) || server.channels.find { |channel| channel.id == channel_id }
|
|
1136
1137
|
existing_voice.channel = new_channel
|
|
1137
1138
|
else
|
|
1138
1139
|
voice_destroy(server_id)
|
|
@@ -1248,7 +1249,15 @@ module OnyxCord
|
|
|
1248
1249
|
|
|
1249
1250
|
# Internal handler for GUILD_UPDATE
|
|
1250
1251
|
def update_guild(data)
|
|
1251
|
-
|
|
1252
|
+
server_id = data['id'].to_i
|
|
1253
|
+
server = @servers&.[](server_id)
|
|
1254
|
+
|
|
1255
|
+
if server
|
|
1256
|
+
server.update_data(data)
|
|
1257
|
+
else
|
|
1258
|
+
LOGGER.warn("GUILD_UPDATE received for uncached server #{server_id}; caching from payload")
|
|
1259
|
+
ensure_server(data, true)
|
|
1260
|
+
end
|
|
1252
1261
|
end
|
|
1253
1262
|
|
|
1254
1263
|
# Internal handler for GUILD_DELETE
|
|
@@ -1868,6 +1877,11 @@ module OnyxCord
|
|
|
1868
1877
|
raise_event(event)
|
|
1869
1878
|
end
|
|
1870
1879
|
rescue Exception => e
|
|
1880
|
+
if defined?(Async::Cancel) && e.is_a?(Async::Cancel)
|
|
1881
|
+
LOGGER.debug('Gateway message handling was cancelled.')
|
|
1882
|
+
return
|
|
1883
|
+
end
|
|
1884
|
+
|
|
1871
1885
|
LOGGER.error('Gateway message error!')
|
|
1872
1886
|
log_exception(e)
|
|
1873
1887
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'onyxcord/webhooks'
|
|
4
|
-
require 'onyxcord/
|
|
4
|
+
require 'onyxcord/message_payload'
|
|
5
5
|
|
|
6
6
|
module OnyxCord
|
|
7
7
|
# Base class for interaction objects.
|
|
@@ -260,7 +260,7 @@ module OnyxCord
|
|
|
260
260
|
components ||= view
|
|
261
261
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components, force: has_components || components_v2)
|
|
262
262
|
data = builder.to_json_hash
|
|
263
|
-
resp = OnyxCord::API::Interaction.edit_original_interaction_response(@token, @application_id,
|
|
263
|
+
resp = OnyxCord::API::Interaction.edit_original_interaction_response(@token, @application_id, edit_content(content, data), edit_embeds(embeds, data), data[:allowed_mentions], components.to_a, attachments, flags, data[:poll])
|
|
264
264
|
|
|
265
265
|
Interactions::Message.new(JSON.parse(resp), @bot, self)
|
|
266
266
|
end
|
|
@@ -324,7 +324,7 @@ module OnyxCord
|
|
|
324
324
|
data = builder.to_json_hash
|
|
325
325
|
|
|
326
326
|
resp = OnyxCord::API::Webhook.token_edit_message(
|
|
327
|
-
@token, @application_id, message.resolve_id,
|
|
327
|
+
@token, @application_id, message.resolve_id, edit_content(content, data), edit_embeds(embeds, data), data[:allowed_mentions], components.to_a, attachments, flags, data[:poll]
|
|
328
328
|
)
|
|
329
329
|
Interactions::Message.new(JSON.parse(resp), @bot, self)
|
|
330
330
|
end
|
|
@@ -392,9 +392,17 @@ module OnyxCord
|
|
|
392
392
|
# @param poll [Poll, Poll::Builder, Hash, nil]
|
|
393
393
|
def prepare_builder(builder, content, embeds, allowed_mentions, poll)
|
|
394
394
|
builder.poll = poll
|
|
395
|
-
builder.content = content
|
|
395
|
+
builder.content = content unless content == OnyxCord::MessagePayload::KEEP
|
|
396
396
|
builder.allowed_mentions = allowed_mentions
|
|
397
|
-
embeds&.each { |embed| builder << embed }
|
|
397
|
+
embeds&.each { |embed| builder << embed } unless embeds == OnyxCord::MessagePayload::KEEP
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def edit_content(content, data)
|
|
401
|
+
content == OnyxCord::MessagePayload::KEEP ? OnyxCord::MessagePayload::KEEP : data[:content]
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def edit_embeds(embeds, data)
|
|
405
|
+
embeds == OnyxCord::MessagePayload::KEEP ? OnyxCord::MessagePayload::KEEP : data[:embeds]
|
|
398
406
|
end
|
|
399
407
|
|
|
400
408
|
# @!visibility private
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'onyxcord/
|
|
3
|
+
require 'onyxcord/message_payload'
|
|
4
4
|
|
|
5
5
|
module OnyxCord
|
|
6
6
|
# A message on Discord that was sent to a text channel
|
|
@@ -304,7 +304,11 @@ module OnyxCord
|
|
|
304
304
|
# @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2) can be edited.
|
|
305
305
|
# @return [Message] the resulting message.
|
|
306
306
|
def edit(new_content, new_embeds = nil, new_components = nil, flags = 0)
|
|
307
|
-
new_embeds =
|
|
307
|
+
new_embeds = if new_embeds == OnyxCord::MessagePayload::KEEP
|
|
308
|
+
new_embeds
|
|
309
|
+
else
|
|
310
|
+
(new_embeds.instance_of?(Array) ? new_embeds.map { |embed| embed.respond_to?(:to_hash) ? embed.to_hash : embed } : [new_embeds&.to_hash]).compact
|
|
311
|
+
end
|
|
308
312
|
new_components = new_components.to_a
|
|
309
313
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, new_components)
|
|
310
314
|
|
data/lib/onyxcord/data/server.rb
CHANGED
|
@@ -697,13 +697,15 @@ module OnyxCord
|
|
|
697
697
|
end
|
|
698
698
|
|
|
699
699
|
# Update the existing voice state (or the one we just created)
|
|
700
|
-
|
|
700
|
+
channel_id = data['channel_id'].to_i
|
|
701
|
+
channel = @channels_by_id[channel_id]
|
|
701
702
|
@voice_states[user_id].update(
|
|
702
703
|
channel,
|
|
703
704
|
data['mute'],
|
|
704
705
|
data['deaf'],
|
|
705
706
|
data['self_mute'],
|
|
706
|
-
data['self_deaf']
|
|
707
|
+
data['self_deaf'],
|
|
708
|
+
channel_id
|
|
707
709
|
)
|
|
708
710
|
else
|
|
709
711
|
# The user is not in a voice channel anymore, so delete its voice state
|
|
@@ -22,6 +22,9 @@ module OnyxCord
|
|
|
22
22
|
# @return [Channel] the voice channel this voice state's member is in.
|
|
23
23
|
attr_reader :voice_channel
|
|
24
24
|
|
|
25
|
+
# @return [Integer, nil] the ID of the voice channel this member is in.
|
|
26
|
+
attr_reader :channel_id
|
|
27
|
+
|
|
25
28
|
# @!visibility private
|
|
26
29
|
def initialize(user_id)
|
|
27
30
|
@user_id = user_id
|
|
@@ -30,8 +33,9 @@ module OnyxCord
|
|
|
30
33
|
# Update this voice state with new data from Discord
|
|
31
34
|
# @note For internal use only.
|
|
32
35
|
# @!visibility private
|
|
33
|
-
def update(channel, mute, deaf, self_mute, self_deaf)
|
|
36
|
+
def update(channel, mute, deaf, self_mute, self_deaf, channel_id = nil)
|
|
34
37
|
@voice_channel = channel
|
|
38
|
+
@channel_id = channel_id&.to_i || channel&.id
|
|
35
39
|
@mute = mute
|
|
36
40
|
@deaf = deaf
|
|
37
41
|
@self_mute = self_mute
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'onyxcord/webhooks/builder'
|
|
4
4
|
require 'onyxcord/webhooks/view'
|
|
5
|
-
require 'onyxcord/
|
|
5
|
+
require 'onyxcord/message_payload'
|
|
6
6
|
|
|
7
7
|
module OnyxCord
|
|
8
8
|
# A webhook on a server channel
|
|
@@ -178,7 +178,7 @@ module OnyxCord
|
|
|
178
178
|
def edit_message(message, content: nil, embeds: nil, allowed_mentions: nil, builder: nil, components: nil, flags: 0, has_components: false, components_v2: false)
|
|
179
179
|
raise OnyxCord::Errors::UnauthorizedWebhook unless @token
|
|
180
180
|
|
|
181
|
-
params = {
|
|
181
|
+
params = { allowed_mentions: allowed_mentions }.compact
|
|
182
182
|
|
|
183
183
|
builder ||= Webhooks::Builder.new
|
|
184
184
|
view ||= Webhooks::View.new
|
|
@@ -189,7 +189,9 @@ module OnyxCord
|
|
|
189
189
|
components ||= view
|
|
190
190
|
flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components, force: has_components || components_v2)
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
edit_content = content == OnyxCord::MessagePayload::KEEP || !content.nil? ? content : data[:content]
|
|
193
|
+
edit_embeds = embeds == OnyxCord::MessagePayload::KEEP || !embeds.nil? ? embeds : data[:embeds]
|
|
194
|
+
resp = API::Webhook.token_edit_message(@token, @id, message.resolve_id, edit_content, edit_embeds, data[:allowed_mentions], components.to_a, nil, flags)
|
|
193
195
|
Message.new(JSON.parse(resp), @bot)
|
|
194
196
|
end
|
|
195
197
|
|
data/lib/onyxcord/errors.rb
CHANGED
|
@@ -14,8 +14,22 @@ module OnyxCord
|
|
|
14
14
|
# Raised when a message is over the character limit
|
|
15
15
|
class MessageTooLong < RuntimeError; end
|
|
16
16
|
|
|
17
|
+
class HTTPError < RuntimeError
|
|
18
|
+
attr_reader :status, :code, :headers, :route, :body, :response
|
|
19
|
+
|
|
20
|
+
def initialize(message = nil, status: nil, code: 0, headers: {}, route: nil, body: nil, response: nil)
|
|
21
|
+
super(message)
|
|
22
|
+
@status = status
|
|
23
|
+
@code = code || 0
|
|
24
|
+
@headers = headers
|
|
25
|
+
@route = route
|
|
26
|
+
@body = body
|
|
27
|
+
@response = response
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
17
31
|
# Raised when the bot can't do something because its permissions on the server are insufficient
|
|
18
|
-
class NoPermission <
|
|
32
|
+
class NoPermission < HTTPError; end
|
|
19
33
|
|
|
20
34
|
# Raised when the bot gets a HTTP 502 error, which is usually caused by Cloudflare.
|
|
21
35
|
class CloudflareError < RuntimeError; end
|
|
@@ -24,7 +38,7 @@ module OnyxCord
|
|
|
24
38
|
class UnauthorizedWebhook < RuntimeError; end
|
|
25
39
|
|
|
26
40
|
# Generic class for errors denoted by API error codes
|
|
27
|
-
class CodeError <
|
|
41
|
+
class CodeError < HTTPError
|
|
28
42
|
class << self
|
|
29
43
|
# @return [Integer] The error code represented by this error class.
|
|
30
44
|
attr_reader :code
|
|
@@ -33,10 +47,11 @@ module OnyxCord
|
|
|
33
47
|
# Create a new error with a particular message (the code should be defined by the class instance variable)
|
|
34
48
|
# @param message [String] the message to use
|
|
35
49
|
# @param errors [Hash] API errors
|
|
36
|
-
def initialize(message, errors = nil)
|
|
50
|
+
def initialize(message, errors = nil, status: nil, headers: {}, route: nil, body: nil, response: nil)
|
|
37
51
|
@message = message
|
|
38
52
|
|
|
39
53
|
@errors = errors ? flatten_errors(errors) : []
|
|
54
|
+
super(full_message, status: status, code: code, headers: headers, route: route, body: body, response: response)
|
|
40
55
|
end
|
|
41
56
|
|
|
42
57
|
# @return [Integer] The error code represented by this error.
|
|
@@ -32,7 +32,23 @@ module OnyxCord::Events
|
|
|
32
32
|
# @!visibility private
|
|
33
33
|
def initialize(data, bot)
|
|
34
34
|
@bot = bot
|
|
35
|
-
@channel = data.is_a?(OnyxCord::Channel)
|
|
35
|
+
@channel = if data.is_a?(OnyxCord::Channel)
|
|
36
|
+
data
|
|
37
|
+
else
|
|
38
|
+
cached_channel(bot, data['id']) || OnyxCord::Channel.new(data, bot, cached_server(bot, data['guild_id']))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cached_channel(bot, channel_id)
|
|
43
|
+
channels = bot.instance_variable_get(:@channels)
|
|
44
|
+
channels&.[](channel_id.to_i)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cached_server(bot, server_id)
|
|
48
|
+
return nil unless server_id
|
|
49
|
+
|
|
50
|
+
servers = bot.instance_variable_get(:@servers)
|
|
51
|
+
servers&.[](server_id.to_i)
|
|
36
52
|
end
|
|
37
53
|
end
|
|
38
54
|
|
data/lib/onyxcord/events/raw.rb
CHANGED
|
@@ -6,11 +6,17 @@ require 'onyxcord/data'
|
|
|
6
6
|
module OnyxCord::Events
|
|
7
7
|
# Event raised when a user's voice state updates
|
|
8
8
|
class VoiceStateUpdateEvent < Event
|
|
9
|
-
attr_reader :user, :token, :suppress, :session_id, :self_mute, :self_deaf, :mute, :deaf, :server, :channel
|
|
9
|
+
attr_reader :user, :user_id, :token, :suppress, :session_id, :self_mute, :self_deaf, :mute, :deaf, :server, :channel
|
|
10
10
|
|
|
11
11
|
# @return [Channel, nil] the old channel this user was on, or nil if the user is newly joining voice.
|
|
12
12
|
attr_reader :old_channel
|
|
13
13
|
|
|
14
|
+
# @return [Integer, nil] the current voice channel ID, or nil if the user left voice.
|
|
15
|
+
attr_reader :channel_id
|
|
16
|
+
|
|
17
|
+
# @return [Integer, nil] the previous voice channel ID, or nil if the user joined voice.
|
|
18
|
+
attr_reader :old_channel_id
|
|
19
|
+
|
|
14
20
|
# @!visibility private
|
|
15
21
|
def initialize(data, old_channel_id, bot)
|
|
16
22
|
@bot = bot
|
|
@@ -22,12 +28,35 @@ module OnyxCord::Events
|
|
|
22
28
|
@self_deaf = data['self_deaf']
|
|
23
29
|
@mute = data['mute']
|
|
24
30
|
@deaf = data['deaf']
|
|
25
|
-
@
|
|
31
|
+
@user_id = data['user_id']&.to_i
|
|
32
|
+
@channel_id = data['channel_id']&.to_i
|
|
33
|
+
@old_channel_id = old_channel_id&.to_i
|
|
34
|
+
@server = cached_server(bot, data['guild_id'])
|
|
26
35
|
return unless @server
|
|
27
36
|
|
|
28
|
-
@channel = bot
|
|
29
|
-
@old_channel = bot
|
|
30
|
-
@user = bot
|
|
37
|
+
@channel = cached_channel(bot, @channel_id, @server)
|
|
38
|
+
@old_channel = cached_channel(bot, @old_channel_id, @server)
|
|
39
|
+
@user = cached_user(bot, data)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cached_server(bot, server_id)
|
|
43
|
+
servers = bot.instance_variable_get(:@servers)
|
|
44
|
+
servers&.[](server_id.to_i)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def cached_channel(bot, channel_id, server)
|
|
48
|
+
return nil unless channel_id
|
|
49
|
+
|
|
50
|
+
channels = bot.instance_variable_get(:@channels)
|
|
51
|
+
channels&.[](channel_id.to_i) || server.channels.find { |channel| channel.id == channel_id.to_i }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cached_user(bot, data)
|
|
55
|
+
user_data = data.dig('member', 'user') || data['user']
|
|
56
|
+
return bot.ensure_user(user_data) if user_data
|
|
57
|
+
|
|
58
|
+
users = bot.instance_variable_get(:@users)
|
|
59
|
+
users&.[](data['user_id'].to_i)
|
|
31
60
|
end
|
|
32
61
|
end
|
|
33
62
|
|
|
@@ -39,6 +68,8 @@ module OnyxCord::Events
|
|
|
39
68
|
|
|
40
69
|
[
|
|
41
70
|
matches_all(@attributes[:from], event.user) do |a, e|
|
|
71
|
+
next unless e
|
|
72
|
+
|
|
42
73
|
a == case a
|
|
43
74
|
when String
|
|
44
75
|
e.name
|
data/lib/onyxcord/gateway.rb
CHANGED
|
@@ -84,6 +84,7 @@ module OnyxCord
|
|
|
84
84
|
@connection = nil
|
|
85
85
|
@closed = true
|
|
86
86
|
@pipe_broken = false
|
|
87
|
+
@missed_heartbeat_acks = 0
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
# Connect to the gateway server inside an Async reactor.
|
|
@@ -142,10 +143,17 @@ module OnyxCord
|
|
|
142
143
|
def heartbeat
|
|
143
144
|
if check_heartbeat_acks
|
|
144
145
|
unless @last_heartbeat_acked
|
|
145
|
-
|
|
146
|
-
@
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
@missed_heartbeat_acks += 1
|
|
147
|
+
if @missed_heartbeat_acks >= 2
|
|
148
|
+
LOGGER.warn('Last heartbeats were not acked — zombie connection! Reconnecting')
|
|
149
|
+
@pipe_broken = true
|
|
150
|
+
reconnect
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
LOGGER.warn('Last heartbeat was not acked — waiting one more interval before reconnecting')
|
|
155
|
+
else
|
|
156
|
+
@missed_heartbeat_acks = 0
|
|
149
157
|
end
|
|
150
158
|
@last_heartbeat_acked = false
|
|
151
159
|
end
|
|
@@ -194,6 +202,7 @@ module OnyxCord
|
|
|
194
202
|
|
|
195
203
|
def reconnect(attempt_resume = true)
|
|
196
204
|
@session.suspend if @session && attempt_resume
|
|
205
|
+
@session.invalidate if attempt_resume && @session && !@received_hello
|
|
197
206
|
@instant_reconnect = true
|
|
198
207
|
@should_reconnect = true
|
|
199
208
|
close(4000)
|
|
@@ -219,6 +228,7 @@ module OnyxCord
|
|
|
219
228
|
|
|
220
229
|
def setup_heartbeats(interval)
|
|
221
230
|
@last_heartbeat_acked = true
|
|
231
|
+
@missed_heartbeat_acks = 0
|
|
222
232
|
return if @heartbeat_task
|
|
223
233
|
|
|
224
234
|
@heartbeat_interval = interval
|
|
@@ -292,6 +302,7 @@ module OnyxCord
|
|
|
292
302
|
|
|
293
303
|
@zlib_reader = Zlib::Inflate.new
|
|
294
304
|
@pipe_broken = false
|
|
305
|
+
@received_hello = false
|
|
295
306
|
@closed = false
|
|
296
307
|
|
|
297
308
|
endpoint = websocket_endpoint(url)
|
|
@@ -307,9 +318,11 @@ module OnyxCord
|
|
|
307
318
|
end
|
|
308
319
|
end
|
|
309
320
|
rescue StandardError => e
|
|
321
|
+
@session&.invalidate unless @received_hello
|
|
310
322
|
LOGGER.error('Error connecting to gateway!')
|
|
311
323
|
LOGGER.log_exception(e)
|
|
312
324
|
ensure
|
|
325
|
+
@session&.invalidate unless @received_hello
|
|
313
326
|
@closed = true
|
|
314
327
|
@connection = nil
|
|
315
328
|
end
|
|
@@ -415,6 +428,7 @@ module OnyxCord
|
|
|
415
428
|
|
|
416
429
|
# Op 10
|
|
417
430
|
def handle_hello(packet)
|
|
431
|
+
@received_hello = true
|
|
418
432
|
LOGGER.debug('Hello!')
|
|
419
433
|
interval = packet['d']['heartbeat_interval'].to_f / 1000.0
|
|
420
434
|
setup_heartbeats(interval)
|
|
@@ -432,7 +446,10 @@ module OnyxCord
|
|
|
432
446
|
# Op 11
|
|
433
447
|
def handle_heartbeat_ack(packet)
|
|
434
448
|
LOGGER.debug("Heartbeat ACK: #{packet.inspect}")
|
|
435
|
-
|
|
449
|
+
if @check_heartbeat_acks
|
|
450
|
+
@last_heartbeat_acked = true
|
|
451
|
+
@missed_heartbeat_acks = 0
|
|
452
|
+
end
|
|
436
453
|
end
|
|
437
454
|
|
|
438
455
|
def handle_internal_close(e)
|
data/lib/onyxcord/http.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'httpx'
|
|
4
|
+
require 'net/http'
|
|
4
5
|
require 'onyxcord/json'
|
|
6
|
+
require 'onyxcord/upload'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require 'uri'
|
|
5
9
|
|
|
6
10
|
module OnyxCord
|
|
7
11
|
# Modern HTTP adapter wrapping HTTPX with persistent connections, automatic
|
|
@@ -22,8 +26,9 @@ module OnyxCord
|
|
|
22
26
|
def initialize(httpx_response)
|
|
23
27
|
@raw = httpx_response
|
|
24
28
|
@body = httpx_response.body.to_s
|
|
25
|
-
@code = httpx_response.status
|
|
26
|
-
|
|
29
|
+
@code = httpx_response.respond_to?(:status) ? httpx_response.status : httpx_response.code.to_i
|
|
30
|
+
response_headers = httpx_response.respond_to?(:headers) ? httpx_response.headers : httpx_response.to_hash
|
|
31
|
+
@headers = normalize_headers(response_headers)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def to_s
|
|
@@ -39,21 +44,27 @@ module OnyxCord
|
|
|
39
44
|
def normalize_headers(headers)
|
|
40
45
|
headers.to_h.transform_keys do |key|
|
|
41
46
|
key.to_s.tr('-', '_').downcase.to_sym
|
|
47
|
+
end.transform_values do |value|
|
|
48
|
+
value.is_a?(Array) && value.size == 1 ? value.first : value
|
|
42
49
|
end
|
|
43
50
|
end
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
module_function
|
|
47
54
|
|
|
48
|
-
# The shared HTTPX session with persistent connections.
|
|
55
|
+
# The shared HTTPX session with persistent connections for the current thread.
|
|
49
56
|
def session
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
Thread.current[:onyxcord_http_session] ||= HTTPX.plugin(:persistent)
|
|
58
|
+
.plugin(:follow_redirects)
|
|
59
|
+
.with(
|
|
60
|
+
fallback_protocol: 'http/1.1',
|
|
61
|
+
ssl: { alpn_protocols: ['http/1.1'] }
|
|
62
|
+
)
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
# Reset the HTTP session (useful for tests).
|
|
55
66
|
def reset!
|
|
56
|
-
|
|
67
|
+
Thread.current[:onyxcord_http_session] = nil
|
|
57
68
|
end
|
|
58
69
|
|
|
59
70
|
# Perform a raw HTTP request and return a {Response}.
|
|
@@ -69,9 +80,9 @@ module OnyxCord
|
|
|
69
80
|
when :get
|
|
70
81
|
http.get(url)
|
|
71
82
|
when :post
|
|
72
|
-
if
|
|
83
|
+
if multipart?(body)
|
|
73
84
|
# Multipart upload
|
|
74
|
-
|
|
85
|
+
post_multipart(url, body, headers)
|
|
75
86
|
else
|
|
76
87
|
http.post(url, body: body)
|
|
77
88
|
end
|
|
@@ -91,6 +102,66 @@ module OnyxCord
|
|
|
91
102
|
Response.new(raw)
|
|
92
103
|
end
|
|
93
104
|
|
|
105
|
+
def post_multipart(url, body, headers)
|
|
106
|
+
uri = URI(url)
|
|
107
|
+
boundary = "----OnyxCord#{SecureRandom.hex(12)}"
|
|
108
|
+
request = Net::HTTP::Post.new(uri)
|
|
109
|
+
headers.each { |key, value| request[key.to_s] = value }
|
|
110
|
+
request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
|
111
|
+
request.body = multipart_body(body, boundary)
|
|
112
|
+
|
|
113
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
114
|
+
http.request(request)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def multipart_body(body, boundary)
|
|
119
|
+
output = String.new.b
|
|
120
|
+
|
|
121
|
+
multipart_parts(body).each do |part|
|
|
122
|
+
key = part[:name]
|
|
123
|
+
value = part[:value]
|
|
124
|
+
output << "--#{boundary}\r\n"
|
|
125
|
+
if part[:filename]
|
|
126
|
+
value.rewind if value.respond_to?(:rewind)
|
|
127
|
+
filename = part[:filename]
|
|
128
|
+
output << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"\r\n"
|
|
129
|
+
output << "Content-Type: #{part[:content_type] || multipart_content_type(filename)}\r\n\r\n"
|
|
130
|
+
output << value.read.to_s
|
|
131
|
+
else
|
|
132
|
+
output << "Content-Disposition: form-data; name=\"#{key}\"\r\n"
|
|
133
|
+
output << "\r\n"
|
|
134
|
+
output << value.to_s
|
|
135
|
+
end
|
|
136
|
+
output << "\r\n"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
output << "--#{boundary}--\r\n"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def multipart_content_type(filename)
|
|
143
|
+
File.extname(filename).casecmp('.txt').zero? ? 'text/plain' : 'application/octet-stream'
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def multipart?(body)
|
|
147
|
+
return true if body.is_a?(Array) && body.all? { |part| part.is_a?(Hash) && part[:name] && part.key?(:value) }
|
|
148
|
+
|
|
149
|
+
body.is_a?(Hash) && body.any? { |_, value| value.respond_to?(:read) || value.respond_to?(:path) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def multipart_parts(body)
|
|
153
|
+
return body if body.is_a?(Array)
|
|
154
|
+
|
|
155
|
+
body.map do |key, value|
|
|
156
|
+
if value.respond_to?(:read) || value.respond_to?(:path)
|
|
157
|
+
upload = OnyxCord::Upload.wrap(value)
|
|
158
|
+
{ name: key, value: upload, filename: upload.filename, content_type: upload.content_type }
|
|
159
|
+
else
|
|
160
|
+
{ name: key, value: value }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
94
165
|
# Convenience wrappers
|
|
95
166
|
def get(url, **headers)
|
|
96
167
|
request(:get, url, nil, **headers)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'onyxcord/message_components'
|
|
4
|
+
require 'onyxcord/upload'
|
|
5
|
+
|
|
6
|
+
module OnyxCord
|
|
7
|
+
module MessagePayload
|
|
8
|
+
MAX_ATTACHMENTS = 10
|
|
9
|
+
MAX_EMBEDS = 10
|
|
10
|
+
KEEP = :keep
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def attachment_payload(attachments)
|
|
15
|
+
uploads(attachments).map.with_index { |upload, index| { id: index, filename: upload.filename } }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def multipart_body(body, attachments)
|
|
19
|
+
parts = uploads(attachments).map.with_index do |upload, index|
|
|
20
|
+
{
|
|
21
|
+
name: "files[#{index}]",
|
|
22
|
+
value: upload,
|
|
23
|
+
filename: upload.filename,
|
|
24
|
+
content_type: upload.content_type || 'application/octet-stream'
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
parts << { name: 'payload_json', value: body.to_json }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate!(content: nil, embeds: nil, components: nil, flags: nil, attachments: nil, poll: nil)
|
|
32
|
+
validate_limit!('attachments', attachments, MAX_ATTACHMENTS)
|
|
33
|
+
validate_limit!('embeds', embeds, MAX_EMBEDS)
|
|
34
|
+
validate_components_v2!(content, embeds, components, flags, poll)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def edit_body(content, embeds)
|
|
38
|
+
content_keep = content == KEEP
|
|
39
|
+
embeds_keep = embeds == KEEP
|
|
40
|
+
content_given = !content_keep && !content.nil?
|
|
41
|
+
embeds_given = !embeds_keep && !embeds.nil?
|
|
42
|
+
body = {}
|
|
43
|
+
body[:content] = content if !content_keep && (content_given || embeds_given)
|
|
44
|
+
body[:embeds] = embeds_given ? embeds : [] if !embeds_keep && (embeds_given || content_given)
|
|
45
|
+
body
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def uploads(attachments)
|
|
49
|
+
Array(attachments).map { |attachment| OnyxCord::Upload.wrap(attachment) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def validate_limit!(name, values, max)
|
|
53
|
+
return if values.nil? || Array(values).length <= max
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, "#{name} cannot exceed #{max} elements"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def validate_components_v2!(content, embeds, components, flags, poll)
|
|
59
|
+
return unless components_v2?(components, flags)
|
|
60
|
+
return unless present?(content) || present?(embeds) || present?(poll)
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, 'Components V2 messages cannot include content, embeds, or poll'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def components_v2?(components, flags)
|
|
66
|
+
OnyxCord::MessageComponents.components_v2?(components) ||
|
|
67
|
+
(OnyxCord::MessageComponents.flag_value(flags) & OnyxCord::MessageComponents::IS_COMPONENTS_V2).positive?
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def present?(value)
|
|
71
|
+
return false if value.nil?
|
|
72
|
+
return false if value == KEEP
|
|
73
|
+
return !value.empty? if value.respond_to?(:empty?)
|
|
74
|
+
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -21,6 +21,7 @@ module OnyxCord
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def before_request(route, major_parameter)
|
|
24
|
+
wait_for(@global_lock)
|
|
24
25
|
wait_for(mutex_for(route, major_parameter))
|
|
25
26
|
end
|
|
26
27
|
|
|
@@ -142,8 +143,8 @@ module OnyxCord
|
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
def global_wait(time)
|
|
145
|
-
|
|
146
|
+
async_wait(time, @global_lock)
|
|
146
147
|
end
|
|
147
148
|
end
|
|
148
149
|
end
|
|
149
|
-
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OnyxCord
|
|
4
|
+
# Small wrapper for file uploads while keeping raw File/Tempfile support.
|
|
5
|
+
class Upload
|
|
6
|
+
attr_reader :io, :filename, :content_type
|
|
7
|
+
|
|
8
|
+
def self.wrap(file)
|
|
9
|
+
file.is_a?(self) ? file : new(file)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(io, filename: nil, content_type: nil)
|
|
13
|
+
@io = io
|
|
14
|
+
@filename = filename || upload_filename(io)
|
|
15
|
+
@content_type = content_type
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def read(...)
|
|
19
|
+
@io.read(...)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rewind
|
|
23
|
+
@io.rewind if @io.respond_to?(:rewind)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def path
|
|
27
|
+
@io.path if @io.respond_to?(:path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def upload_filename(io)
|
|
33
|
+
path = io.path if io.respond_to?(:path)
|
|
34
|
+
return File.basename(path) if path && !path.empty?
|
|
35
|
+
|
|
36
|
+
'upload.dat'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/onyxcord/version.rb
CHANGED
data/lib/onyxcord.rb
CHANGED
|
@@ -8,6 +8,8 @@ require 'onyxcord/version'
|
|
|
8
8
|
require 'onyxcord/configuration'
|
|
9
9
|
require 'onyxcord/event_executor'
|
|
10
10
|
require 'onyxcord/message_components'
|
|
11
|
+
require 'onyxcord/message_payload'
|
|
12
|
+
require 'onyxcord/upload'
|
|
11
13
|
require 'onyxcord/webhooks'
|
|
12
14
|
require 'onyxcord/bot'
|
|
13
15
|
require 'onyxcord/commands/command_bot'
|
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
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gustavo Silva
|
|
@@ -486,11 +486,13 @@ files:
|
|
|
486
486
|
- lib/onyxcord/light/light_bot.rb
|
|
487
487
|
- lib/onyxcord/logger.rb
|
|
488
488
|
- lib/onyxcord/message_components.rb
|
|
489
|
+
- lib/onyxcord/message_payload.rb
|
|
489
490
|
- lib/onyxcord/paginator.rb
|
|
490
491
|
- lib/onyxcord/permissions.rb
|
|
491
492
|
- lib/onyxcord/rate_limiter/async_rest.rb
|
|
492
493
|
- lib/onyxcord/rate_limiter/gateway.rb
|
|
493
494
|
- lib/onyxcord/rate_limiter/rest.rb
|
|
495
|
+
- lib/onyxcord/upload.rb
|
|
494
496
|
- lib/onyxcord/version.rb
|
|
495
497
|
- lib/onyxcord/voice/encoder.rb
|
|
496
498
|
- lib/onyxcord/voice/network.rb
|
|
@@ -530,7 +532,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
530
532
|
- !ruby/object:Gem::Version
|
|
531
533
|
version: '0'
|
|
532
534
|
requirements: []
|
|
533
|
-
rubygems_version:
|
|
535
|
+
rubygems_version: 3.6.9
|
|
534
536
|
specification_version: 4
|
|
535
537
|
summary: Discord API for Ruby with Components V2 support
|
|
536
538
|
test_files: []
|