onyxcord 1.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 +7 -0
- data/.devcontainer/Dockerfile +13 -0
- data/.devcontainer/devcontainer.json +29 -0
- data/.devcontainer/postcreate.sh +4 -0
- data/.github/CONTRIBUTING.md +13 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- data/.github/pull_request_template.md +37 -0
- data/.github/workflows/ci.yml +78 -0
- data/.github/workflows/codeql.yml +65 -0
- data/.github/workflows/deploy.yml +54 -0
- data/.github/workflows/release.yml +51 -0
- data/.gitignore +16 -0
- data/.markdownlint.json +4 -0
- data/.overcommit.yml +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +129 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +305 -0
- data/Rakefile +17 -0
- data/bin/console +15 -0
- data/bin/setup +7 -0
- data/lib/onyxcord/allowed_mentions.rb +43 -0
- data/lib/onyxcord/api/application.rb +316 -0
- data/lib/onyxcord/api/channel.rb +700 -0
- data/lib/onyxcord/api/interaction.rb +67 -0
- data/lib/onyxcord/api/invite.rb +44 -0
- data/lib/onyxcord/api/server.rb +775 -0
- data/lib/onyxcord/api/user.rb +158 -0
- data/lib/onyxcord/api/webhook.rb +163 -0
- data/lib/onyxcord/api.rb +335 -0
- data/lib/onyxcord/await.rb +51 -0
- data/lib/onyxcord/bot.rb +1971 -0
- data/lib/onyxcord/cache.rb +326 -0
- data/lib/onyxcord/colour_rgb.rb +43 -0
- data/lib/onyxcord/commands/command_bot.rb +511 -0
- data/lib/onyxcord/commands/container.rb +112 -0
- data/lib/onyxcord/commands/events.rb +11 -0
- data/lib/onyxcord/commands/parser.rb +327 -0
- data/lib/onyxcord/commands/rate_limiter.rb +144 -0
- data/lib/onyxcord/configuration.rb +125 -0
- data/lib/onyxcord/container.rb +988 -0
- data/lib/onyxcord/data/activity.rb +271 -0
- data/lib/onyxcord/data/application.rb +341 -0
- data/lib/onyxcord/data/attachment.rb +91 -0
- data/lib/onyxcord/data/audit_logs.rb +438 -0
- data/lib/onyxcord/data/avatar_decoration.rb +26 -0
- data/lib/onyxcord/data/call.rb +22 -0
- data/lib/onyxcord/data/channel.rb +1355 -0
- data/lib/onyxcord/data/channel_tag.rb +69 -0
- data/lib/onyxcord/data/collectibles.rb +47 -0
- data/lib/onyxcord/data/component.rb +583 -0
- data/lib/onyxcord/data/embed.rb +258 -0
- data/lib/onyxcord/data/emoji.rb +123 -0
- data/lib/onyxcord/data/install_params.rb +24 -0
- data/lib/onyxcord/data/integration.rb +144 -0
- data/lib/onyxcord/data/interaction.rb +1141 -0
- data/lib/onyxcord/data/invite.rb +137 -0
- data/lib/onyxcord/data/member.rb +528 -0
- data/lib/onyxcord/data/message.rb +612 -0
- data/lib/onyxcord/data/message_activity.rb +41 -0
- data/lib/onyxcord/data/overwrite.rb +109 -0
- data/lib/onyxcord/data/poll.rb +365 -0
- data/lib/onyxcord/data/primary_server.rb +60 -0
- data/lib/onyxcord/data/profile.rb +79 -0
- data/lib/onyxcord/data/reaction.rb +64 -0
- data/lib/onyxcord/data/recipient.rb +34 -0
- data/lib/onyxcord/data/role.rb +449 -0
- data/lib/onyxcord/data/role_connection_data.rb +69 -0
- data/lib/onyxcord/data/role_subscription.rb +41 -0
- data/lib/onyxcord/data/scheduled_event.rb +513 -0
- data/lib/onyxcord/data/server.rb +1614 -0
- data/lib/onyxcord/data/server_preview.rb +68 -0
- data/lib/onyxcord/data/snapshot.rb +112 -0
- data/lib/onyxcord/data/team.rb +98 -0
- data/lib/onyxcord/data/timestamp.rb +69 -0
- data/lib/onyxcord/data/user.rb +324 -0
- data/lib/onyxcord/data/voice_region.rb +46 -0
- data/lib/onyxcord/data/voice_state.rb +41 -0
- data/lib/onyxcord/data/webhook.rb +238 -0
- data/lib/onyxcord/data.rb +57 -0
- data/lib/onyxcord/errors.rb +246 -0
- data/lib/onyxcord/event_executor.rb +80 -0
- data/lib/onyxcord/events/await.rb +48 -0
- data/lib/onyxcord/events/bans.rb +60 -0
- data/lib/onyxcord/events/channels.rb +225 -0
- data/lib/onyxcord/events/generic.rb +129 -0
- data/lib/onyxcord/events/guilds.rb +269 -0
- data/lib/onyxcord/events/integrations.rb +100 -0
- data/lib/onyxcord/events/interactions.rb +624 -0
- data/lib/onyxcord/events/invites.rb +127 -0
- data/lib/onyxcord/events/lifetime.rb +31 -0
- data/lib/onyxcord/events/members.rb +110 -0
- data/lib/onyxcord/events/message.rb +399 -0
- data/lib/onyxcord/events/polls.rb +118 -0
- data/lib/onyxcord/events/presence.rb +131 -0
- data/lib/onyxcord/events/raw.rb +74 -0
- data/lib/onyxcord/events/reactions.rb +218 -0
- data/lib/onyxcord/events/roles.rb +87 -0
- data/lib/onyxcord/events/scheduled_events.rb +171 -0
- data/lib/onyxcord/events/threads.rb +100 -0
- data/lib/onyxcord/events/typing.rb +73 -0
- data/lib/onyxcord/events/voice_server_update.rb +48 -0
- data/lib/onyxcord/events/voice_state_update.rb +106 -0
- data/lib/onyxcord/events/webhooks.rb +65 -0
- data/lib/onyxcord/gateway.rb +890 -0
- data/lib/onyxcord/id_object.rb +39 -0
- data/lib/onyxcord/light/data.rb +62 -0
- data/lib/onyxcord/light/integrations.rb +73 -0
- data/lib/onyxcord/light/light_bot.rb +58 -0
- data/lib/onyxcord/light.rb +8 -0
- data/lib/onyxcord/logger.rb +120 -0
- data/lib/onyxcord/message_components.rb +70 -0
- data/lib/onyxcord/paginator.rb +60 -0
- data/lib/onyxcord/permissions.rb +255 -0
- data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
- data/lib/onyxcord/rate_limiter/rest.rb +89 -0
- data/lib/onyxcord/version.rb +7 -0
- data/lib/onyxcord/voice/encoder.rb +115 -0
- data/lib/onyxcord/voice/network.rb +380 -0
- data/lib/onyxcord/voice/opcodes.rb +29 -0
- data/lib/onyxcord/voice/sodium.rb +157 -0
- data/lib/onyxcord/voice/timer.rb +19 -0
- data/lib/onyxcord/voice/voice_bot.rb +386 -0
- data/lib/onyxcord/webhooks.rb +14 -0
- data/lib/onyxcord/websocket.rb +62 -0
- data/lib/onyxcord.rb +180 -0
- data/onyxcord-webhooks.gemspec +30 -0
- data/onyxcord.gemspec +50 -0
- metadata +421 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'websocket-client-simple'
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
require 'onyxcord/websocket'
|
|
8
|
+
require 'onyxcord/voice/opcodes'
|
|
9
|
+
|
|
10
|
+
begin
|
|
11
|
+
LIBSODIUM_AVAILABLE = if ENV['ONYXCORD_NONACL']
|
|
12
|
+
false
|
|
13
|
+
else
|
|
14
|
+
require 'onyxcord/voice/sodium'
|
|
15
|
+
end
|
|
16
|
+
rescue LoadError
|
|
17
|
+
puts "libsodium not available! You can continue to use onyxcord as normal but voice support won't work.
|
|
18
|
+
Read https://github.com/kruldevb/OnyxCord/wiki/Installing-libsodium for more details."
|
|
19
|
+
LIBSODIUM_AVAILABLE = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module OnyxCord::Voice
|
|
23
|
+
# Signifies to Discord that encryption should be used
|
|
24
|
+
# @deprecated Discord now supports multiple encryption options.
|
|
25
|
+
# TODO: Resolve replacement for this constant.
|
|
26
|
+
ENCRYPTED_MODE = 'aead_xchacha20_poly1305_rtpsize'
|
|
27
|
+
|
|
28
|
+
# Signifies to Discord that no encryption should be used
|
|
29
|
+
# @deprecated Discord no longer supports unencrypted voice communication.
|
|
30
|
+
PLAIN_MODE = 'plain'
|
|
31
|
+
|
|
32
|
+
# Encryption modes supported by Discord
|
|
33
|
+
ENCRYPTION_MODES = %w[aead_xchacha20_poly1305_rtpsize].freeze
|
|
34
|
+
|
|
35
|
+
# Represents a UDP connection to a voice server. This connection is used to send the actual audio data.
|
|
36
|
+
class VoiceUDP
|
|
37
|
+
# @return [true, false] whether or not UDP communications are encrypted.
|
|
38
|
+
# @deprecated Discord no longer supports unencrypted voice communication.
|
|
39
|
+
attr_accessor :encrypted
|
|
40
|
+
alias_method :encrypted?, :encrypted
|
|
41
|
+
|
|
42
|
+
# Sets the secret key used for encryption
|
|
43
|
+
attr_writer :secret_key
|
|
44
|
+
|
|
45
|
+
# The UDP encryption mode
|
|
46
|
+
attr_reader :mode
|
|
47
|
+
|
|
48
|
+
# @!visibility private
|
|
49
|
+
attr_writer :mode
|
|
50
|
+
|
|
51
|
+
# Creates a new UDP connection. Only creates a socket as the discovery reply may come before the data is
|
|
52
|
+
# initialized.
|
|
53
|
+
def initialize
|
|
54
|
+
@socket = UDPSocket.new
|
|
55
|
+
@encrypted = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Initializes the UDP socket with data obtained from opcode 2.
|
|
59
|
+
# @param ip [String] The IP address to connect to.
|
|
60
|
+
# @param port [Integer] The port to connect to.
|
|
61
|
+
# @param ssrc [Integer] The Super Secret Relay Code (SSRC). Discord uses this to identify different voice users
|
|
62
|
+
# on the same endpoint.
|
|
63
|
+
def connect(ip, port, ssrc)
|
|
64
|
+
@ip = ip
|
|
65
|
+
@port = port
|
|
66
|
+
@ssrc = ssrc
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Waits for a UDP discovery reply, and returns the sent data.
|
|
70
|
+
# @return [Array(String, Integer)] the IP and port received from the discovery reply.
|
|
71
|
+
def receive_discovery_reply
|
|
72
|
+
# Wait for a UDP message
|
|
73
|
+
message = @socket.recv(74)
|
|
74
|
+
ip = message[8..-3].delete("\0")
|
|
75
|
+
port = message[-2..].unpack1('n')
|
|
76
|
+
[ip, port]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Makes an audio packet from a buffer and sends it to Discord.
|
|
80
|
+
# @param buf [String] The audio data to send, must be exactly one Opus frame
|
|
81
|
+
# @param sequence [Integer] The packet sequence number, incremented by one for subsequent packets
|
|
82
|
+
# @param time [Integer] When this packet should be played back, in no particular unit (essentially just the
|
|
83
|
+
# sequence number multiplied by 960)
|
|
84
|
+
def send_audio(buf, sequence, time)
|
|
85
|
+
# Header of the audio packet
|
|
86
|
+
header = generate_header(sequence, time)
|
|
87
|
+
|
|
88
|
+
nonce = generate_nonce
|
|
89
|
+
buf = encrypt_audio(buf, header, nonce)
|
|
90
|
+
data = header + buf + nonce.byteslice(0, 4)
|
|
91
|
+
|
|
92
|
+
send_packet(data)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Sends the UDP discovery packet with the internally stored SSRC. Discord will send a reply afterwards which can
|
|
96
|
+
# be received using {#receive_discovery_reply}
|
|
97
|
+
def send_discovery
|
|
98
|
+
# Create empty packet
|
|
99
|
+
discovery_packet = ''
|
|
100
|
+
|
|
101
|
+
# Add Type request (0x1 = request, 0x2 = response)
|
|
102
|
+
discovery_packet += [0x1].pack('n')
|
|
103
|
+
|
|
104
|
+
# Add Length (excluding Type and itself = 70)
|
|
105
|
+
discovery_packet += [70].pack('n')
|
|
106
|
+
|
|
107
|
+
# Add SSRC
|
|
108
|
+
discovery_packet += [@ssrc].pack('N')
|
|
109
|
+
|
|
110
|
+
# Add 66 zeroes so the packet is 74 bytes long
|
|
111
|
+
discovery_packet += "\0" * 66
|
|
112
|
+
|
|
113
|
+
send_packet(discovery_packet)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# Encrypts audio data using libsodium
|
|
119
|
+
# @param buf [String] The encoded audio data to be encrypted
|
|
120
|
+
# @param header [String] The RTP header of the packet, used as associated data
|
|
121
|
+
# @param nonce [String] The nonce to be used to encrypt the data
|
|
122
|
+
# @return [String] the audio data, encrypted
|
|
123
|
+
def encrypt_audio(buf, header, nonce)
|
|
124
|
+
raise 'No secret key found, despite encryption being enabled!' unless @secret_key
|
|
125
|
+
|
|
126
|
+
case @mode
|
|
127
|
+
when 'aead_xchacha20_poly1305_rtpsize'
|
|
128
|
+
OnyxCord::Voice::XChaCha20AEAD.encrypt(buf, header, nonce, @secret_key)
|
|
129
|
+
else
|
|
130
|
+
raise "`#{@mode}' is not a supported encryption mode"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def send_packet(packet)
|
|
135
|
+
@socket.send(packet, 0, @ip, @port)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @return [String]
|
|
139
|
+
def generate_nonce
|
|
140
|
+
case @mode
|
|
141
|
+
when 'aead_xchacha20_poly1305_rtpsize'
|
|
142
|
+
case @incremental_nonce
|
|
143
|
+
when nil, 0xff_ff_ff_ff
|
|
144
|
+
@incremental_nonce = 0
|
|
145
|
+
else
|
|
146
|
+
@incremental_nonce += 1
|
|
147
|
+
end
|
|
148
|
+
[@incremental_nonce].pack('N').ljust(24, "\0")
|
|
149
|
+
else
|
|
150
|
+
raise "`#{@mode}' is not a supported encryption mode"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @return [String]
|
|
155
|
+
def generate_header(sequence, time)
|
|
156
|
+
[0x80, 0x78, sequence, time, @ssrc].pack('CCnNN')
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Represents a websocket client connection to the voice server. The websocket connection (sometimes called vWS) is
|
|
161
|
+
# used to manage general data about the connection, such as sending the speaking packet, which determines the green
|
|
162
|
+
# circle around users on Discord, and obtaining UDP connection info.
|
|
163
|
+
class VoiceWS
|
|
164
|
+
# The version of the voice gateway that's supposed to be used.
|
|
165
|
+
VOICE_GATEWAY_VERSION = 8
|
|
166
|
+
|
|
167
|
+
# @return [VoiceUDP] the UDP voice connection over which the actual audio data is sent.
|
|
168
|
+
attr_reader :udp
|
|
169
|
+
|
|
170
|
+
# Makes a new voice websocket client, but doesn't connect it (see {#connect} for that)
|
|
171
|
+
# @param channel [Channel] The voice channel to connect to
|
|
172
|
+
# @param bot [Bot] The regular bot to which this vWS is bound
|
|
173
|
+
# @param token [String] The authentication token which is also used for REST requests
|
|
174
|
+
# @param session [String] The voice session ID Discord sends over the regular websocket
|
|
175
|
+
# @param endpoint [String] The endpoint URL to connect to
|
|
176
|
+
def initialize(channel, bot, token, session, endpoint)
|
|
177
|
+
raise 'libsodium is unavailable - unable to create voice bot! Please read https://github.com/kruldevb/OnyxCord/wiki/Installing-libsodium' unless LIBSODIUM_AVAILABLE
|
|
178
|
+
|
|
179
|
+
@channel = channel
|
|
180
|
+
@bot = bot
|
|
181
|
+
@token = token
|
|
182
|
+
@session = session
|
|
183
|
+
|
|
184
|
+
@endpoint = endpoint
|
|
185
|
+
|
|
186
|
+
@udp = VoiceUDP.new
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Send a connection init packet (op 0)
|
|
190
|
+
# @param server_id [Integer] The ID of the server to connect to
|
|
191
|
+
# @param bot_user_id [Integer] The ID of the bot that is connecting
|
|
192
|
+
# @param session_id [String] The voice session ID
|
|
193
|
+
# @param token [String] The Discord authentication token
|
|
194
|
+
def send_init(server_id, bot_user_id, session_id, token)
|
|
195
|
+
send_opcode(
|
|
196
|
+
Opcodes::IDENTIFY,
|
|
197
|
+
{
|
|
198
|
+
server_id: server_id,
|
|
199
|
+
user_id: bot_user_id,
|
|
200
|
+
session_id: session_id,
|
|
201
|
+
token: token
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Sends the UDP connection packet (op 1)
|
|
207
|
+
# @param ip [String] The IP to bind UDP to
|
|
208
|
+
# @param port [Integer] The port to bind UDP to
|
|
209
|
+
# @param mode [Object] Which mode to use for the voice connection
|
|
210
|
+
def send_udp_connection(ip, port, mode)
|
|
211
|
+
send_opcode(
|
|
212
|
+
Opcodes::SELECT_PROTOCOL,
|
|
213
|
+
{
|
|
214
|
+
protocol: 'udp',
|
|
215
|
+
data: {
|
|
216
|
+
address: ip,
|
|
217
|
+
port: port,
|
|
218
|
+
mode: mode
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Send a heartbeat (op 3), has to be done every @heartbeat_interval seconds or the connection will terminate
|
|
225
|
+
def send_heartbeat
|
|
226
|
+
millis = Time.now.strftime('%s%L').to_i
|
|
227
|
+
@bot.debug("Sending voice heartbeat at #{millis}")
|
|
228
|
+
|
|
229
|
+
send_opcode(
|
|
230
|
+
Opcodes::HEARTBEAT,
|
|
231
|
+
{
|
|
232
|
+
t: millis,
|
|
233
|
+
seq_ack: @seq
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Send a speaking packet (op 5). This determines the green circle around the avatar in the voice channel
|
|
239
|
+
# @param value [true, false, Integer] Whether or not the bot should be speaking, can also be a bitmask denoting audio type.
|
|
240
|
+
def send_speaking(value)
|
|
241
|
+
@bot.debug("Speaking: #{value}")
|
|
242
|
+
send_opcode(
|
|
243
|
+
Opcodes::SPEAKING,
|
|
244
|
+
{
|
|
245
|
+
speaking: value,
|
|
246
|
+
delay: 0
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def send_opcode(opcode, data)
|
|
252
|
+
@bot.debug("Sending voice opcode #{opcode} with data: #{data}")
|
|
253
|
+
@client.send({
|
|
254
|
+
op: opcode,
|
|
255
|
+
d: data
|
|
256
|
+
}.to_json)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Event handlers; public for websocket-simple to work correctly
|
|
260
|
+
# @!visibility private
|
|
261
|
+
def websocket_open
|
|
262
|
+
# Give the current thread a name ('Voice Web Socket Internal')
|
|
263
|
+
Thread.current[:onyxcord_name] = 'vws-i'
|
|
264
|
+
|
|
265
|
+
# Send the init packet
|
|
266
|
+
send_init(@channel.server.id, @bot.profile.id, @session, @token)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# @!visibility private
|
|
270
|
+
def websocket_message(msg)
|
|
271
|
+
@bot.debug("Received VWS message! #{msg}")
|
|
272
|
+
packet = JSON.parse(msg)
|
|
273
|
+
|
|
274
|
+
@seq = packet['seq'] if packet['seq']
|
|
275
|
+
|
|
276
|
+
case packet['op']
|
|
277
|
+
when OnyxCord::Voice::Opcodes::READY
|
|
278
|
+
# Opcode 2 contains data to initialize the UDP connection
|
|
279
|
+
@ws_data = packet['d']
|
|
280
|
+
|
|
281
|
+
@ssrc = @ws_data['ssrc']
|
|
282
|
+
@port = @ws_data['port']
|
|
283
|
+
|
|
284
|
+
@udp_mode = (ENCRYPTION_MODES & @ws_data['modes']).first
|
|
285
|
+
|
|
286
|
+
@udp.connect(@ws_data['ip'], @port, @ssrc)
|
|
287
|
+
@udp.send_discovery
|
|
288
|
+
when OnyxCord::Voice::Opcodes::SESSION_DESCRIPTION
|
|
289
|
+
# Opcode 4 sends the secret key used for encryption
|
|
290
|
+
@ws_data = packet['d']
|
|
291
|
+
|
|
292
|
+
# Reset the sequence when starting a new session
|
|
293
|
+
@seq = 0
|
|
294
|
+
|
|
295
|
+
@ready = true
|
|
296
|
+
@udp.secret_key = @ws_data['secret_key'].pack('C*')
|
|
297
|
+
@udp.mode = @ws_data['mode']
|
|
298
|
+
when OnyxCord::Voice::Opcodes::HELLO
|
|
299
|
+
# Opcode 8 contains the heartbeat interval.
|
|
300
|
+
@heartbeat_interval = packet['d']['heartbeat_interval']
|
|
301
|
+
send_heartbeat
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Communication goes like this:
|
|
306
|
+
# me discord
|
|
307
|
+
# | |
|
|
308
|
+
# websocket connect -> |
|
|
309
|
+
# | |
|
|
310
|
+
# | <- websocket opcode 2
|
|
311
|
+
# | |
|
|
312
|
+
# UDP discovery -> |
|
|
313
|
+
# | |
|
|
314
|
+
# | <- UDP reply packet
|
|
315
|
+
# | |
|
|
316
|
+
# websocket opcode 1 -> |
|
|
317
|
+
# | |
|
|
318
|
+
# ...
|
|
319
|
+
def connect
|
|
320
|
+
# Connect websocket
|
|
321
|
+
@thread = Thread.new do
|
|
322
|
+
Thread.current[:onyxcord_name] = 'vws'
|
|
323
|
+
init_ws
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
@bot.debug('Started websocket initialization, now waiting for UDP discovery reply')
|
|
327
|
+
|
|
328
|
+
# Now wait for opcode 2 and the resulting UDP reply packet
|
|
329
|
+
ip, port = @udp.receive_discovery_reply
|
|
330
|
+
@bot.debug("UDP discovery reply received! #{ip} #{port}")
|
|
331
|
+
|
|
332
|
+
# Send UDP init packet with received UDP data
|
|
333
|
+
send_udp_connection(ip, port, @udp_mode)
|
|
334
|
+
|
|
335
|
+
@bot.debug('Waiting for op 4 now')
|
|
336
|
+
|
|
337
|
+
# Wait for op 4, then finish
|
|
338
|
+
sleep 0.05 until @ready
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Disconnects the websocket and kills the thread
|
|
342
|
+
def destroy
|
|
343
|
+
@heartbeat_running = false
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
private
|
|
347
|
+
|
|
348
|
+
def heartbeat_loop
|
|
349
|
+
@heartbeat_running = true
|
|
350
|
+
while @heartbeat_running
|
|
351
|
+
if @heartbeat_interval
|
|
352
|
+
sleep @heartbeat_interval / 1000.0
|
|
353
|
+
send_heartbeat
|
|
354
|
+
else
|
|
355
|
+
# If no interval has been set yet, sleep a second and check again
|
|
356
|
+
sleep 1
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def init_ws
|
|
362
|
+
host = "wss://#{@endpoint}/?v=#{VOICE_GATEWAY_VERSION}"
|
|
363
|
+
@bot.debug("Connecting VWS to host: #{host}")
|
|
364
|
+
|
|
365
|
+
# Connect the WS
|
|
366
|
+
@client = OnyxCord::WebSocket.new(
|
|
367
|
+
host,
|
|
368
|
+
method(:websocket_open),
|
|
369
|
+
method(:websocket_message),
|
|
370
|
+
proc { |e| OnyxCord::LOGGER.error "VWS error: #{e}" },
|
|
371
|
+
proc { |e| OnyxCord::LOGGER.warn "VWS close: #{e}" }
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@bot.debug('VWS connected')
|
|
375
|
+
|
|
376
|
+
# Block any further execution
|
|
377
|
+
heartbeat_loop
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Voice websocket opcodes
|
|
4
|
+
# @!visibility private
|
|
5
|
+
module OnyxCord::Voice::Opcodes
|
|
6
|
+
IDENTIFY = 0
|
|
7
|
+
SELECT_PROTOCOL = 1
|
|
8
|
+
READY = 2
|
|
9
|
+
HEARTBEAT = 3
|
|
10
|
+
SESSION_DESCRIPTION = 4
|
|
11
|
+
SPEAKING = 5
|
|
12
|
+
HEARTBEAT_ACK = 6
|
|
13
|
+
RESUME = 7
|
|
14
|
+
HELLO = 8
|
|
15
|
+
RESUMED = 9
|
|
16
|
+
CLIENT_CONNECT = 10
|
|
17
|
+
CLIENT_DISCONNECT = 11
|
|
18
|
+
DAVE_PREPARE_TRANSITION = 21
|
|
19
|
+
DAVE_EXECUTE_TRANSITION = 22
|
|
20
|
+
DAVE_TRANSITION_READY = 23
|
|
21
|
+
DAVE_PREPARE_EPOCH = 24
|
|
22
|
+
DAVE_MLS_EXTERNAL_SENDER = 25
|
|
23
|
+
DAVE_MLS_KEY_PACKAGE = 26
|
|
24
|
+
DAVE_MLS_PROPOSALS = 27
|
|
25
|
+
DAVE_MLS_COMMIT_WELCOME = 28
|
|
26
|
+
DAVE_MLS_ANNOUNCE_COMMIT_TRANSITION = 29
|
|
27
|
+
DAVE_MLS_WELCOME = 30
|
|
28
|
+
DAVE_MLS_INVALID_COMMIT_WELCOME = 31
|
|
29
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
# :nodoc:
|
|
7
|
+
module OnyxCord::Voice
|
|
8
|
+
# @!visibility private
|
|
9
|
+
module Sodium
|
|
10
|
+
extend FFI::Library
|
|
11
|
+
ffi_lib 'sodium'
|
|
12
|
+
|
|
13
|
+
# @!group Constants
|
|
14
|
+
|
|
15
|
+
# Initializes libsodium
|
|
16
|
+
# @return [Integer] 0 on success
|
|
17
|
+
attach_function :sodium_init, [], :int
|
|
18
|
+
|
|
19
|
+
# Returns the key size (in bytes)
|
|
20
|
+
# @return [Integer]
|
|
21
|
+
attach_function :crypto_aead_xchacha20poly1305_ietf_keybytes, [], :size_t
|
|
22
|
+
|
|
23
|
+
# Returns the nonce size (in bytes)
|
|
24
|
+
# @return [Integer]
|
|
25
|
+
attach_function :crypto_aead_xchacha20poly1305_ietf_npubbytes, [], :size_t
|
|
26
|
+
|
|
27
|
+
# Returns the authentication tag size (in bytes)
|
|
28
|
+
# @return [Integer]
|
|
29
|
+
attach_function :crypto_aead_xchacha20poly1305_ietf_abytes, [], :size_t
|
|
30
|
+
|
|
31
|
+
# @!endgroup
|
|
32
|
+
|
|
33
|
+
# @!group AEAD Encrypt/Decrypt
|
|
34
|
+
|
|
35
|
+
# Performs authenticated encryption using XChaCha20-Poly1305
|
|
36
|
+
#
|
|
37
|
+
# @!macro [attach] crypto_aead_xchacha20poly1305_ietf_encrypt
|
|
38
|
+
# @param c [FFI::Pointer] output buffer for ciphertext
|
|
39
|
+
# @param clen_p [FFI::Pointer] output pointer for ciphertext length
|
|
40
|
+
# @param m [FFI::Pointer] input message pointer
|
|
41
|
+
# @param mlen [Integer] length of the message
|
|
42
|
+
# @param ad [FFI::Pointer] pointer to associated data
|
|
43
|
+
# @param adlen [Integer] length of associated data
|
|
44
|
+
# @param nsec [FFI::Pointer, nil] (not used, must be nil)
|
|
45
|
+
# @param npub [FFI::Pointer] nonce pointer
|
|
46
|
+
# @param k [FFI::Pointer] key pointer
|
|
47
|
+
# @return [Integer] 0 on success
|
|
48
|
+
attach_function :crypto_aead_xchacha20poly1305_ietf_encrypt, %i[
|
|
49
|
+
pointer pointer pointer ulong_long
|
|
50
|
+
pointer ulong_long
|
|
51
|
+
pointer pointer pointer
|
|
52
|
+
], :int
|
|
53
|
+
|
|
54
|
+
# Decrypts XChaCha20-Poly1305 AEAD-encrypted data
|
|
55
|
+
# @!macro [attach] crypto_aead_xchacha20poly1305_ietf_decrypt
|
|
56
|
+
# @param m [FFI::Pointer] output buffer for decrypted message
|
|
57
|
+
# @param mlen_p [FFI::Pointer] output pointer for decrypted length
|
|
58
|
+
# @param nsec [FFI::Pointer, nil] (not used, must be nil)
|
|
59
|
+
# @param c [FFI::Pointer] ciphertext pointer
|
|
60
|
+
# @param clen [Integer] length of ciphertext
|
|
61
|
+
# @param ad [FFI::Pointer] pointer to associated data
|
|
62
|
+
# @param adlen [Integer] length of associated data
|
|
63
|
+
# @param npub [FFI::Pointer] nonce pointer
|
|
64
|
+
# @param k [FFI::Pointer] key pointer
|
|
65
|
+
# @return [Integer] 0 on success
|
|
66
|
+
attach_function :crypto_aead_xchacha20poly1305_ietf_decrypt, %i[
|
|
67
|
+
pointer pointer pointer pointer ulong_long
|
|
68
|
+
pointer ulong_long pointer pointer
|
|
69
|
+
], :int
|
|
70
|
+
|
|
71
|
+
# @!endgroup
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Sodium.sodium_init
|
|
75
|
+
|
|
76
|
+
# High-level wrapper class
|
|
77
|
+
class XChaCha20AEAD
|
|
78
|
+
KEY_BYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes
|
|
79
|
+
NONCE_BYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes
|
|
80
|
+
TAG_BYTES = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes
|
|
81
|
+
|
|
82
|
+
# Generates a random key
|
|
83
|
+
# @return [String] binary key
|
|
84
|
+
def self.generate_key
|
|
85
|
+
SecureRandom.random_bytes(KEY_BYTES)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Generates a random nonce
|
|
89
|
+
# @return [String] binary nonce
|
|
90
|
+
def self.generate_nonce
|
|
91
|
+
SecureRandom.random_bytes(NONCE_BYTES)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Encrypts a message using XChaCha20-Poly1305
|
|
95
|
+
#
|
|
96
|
+
# @param message [String] plaintext to encrypt
|
|
97
|
+
# @param key [String] 32-byte encryption key
|
|
98
|
+
# @param nonce [String] 24-byte nonce
|
|
99
|
+
# @param add [String] optional associated data
|
|
100
|
+
# @return [String] ciphertext (includes the auth tag)
|
|
101
|
+
def self.encrypt(message, add, nonce, key)
|
|
102
|
+
raise ArgumentError, 'Invalid key size' unless key.bytesize == KEY_BYTES
|
|
103
|
+
raise ArgumentError, 'Invalid nonce size' unless nonce.bytesize == NONCE_BYTES
|
|
104
|
+
|
|
105
|
+
message_ptr = FFI::MemoryPointer.from_string(message)
|
|
106
|
+
ad_ptr = FFI::MemoryPointer.from_string(add)
|
|
107
|
+
|
|
108
|
+
c_len = message.bytesize + TAG_BYTES
|
|
109
|
+
ciphertext = FFI::MemoryPointer.new(:uchar, c_len)
|
|
110
|
+
clen_p = FFI::MemoryPointer.new(:ulong_long)
|
|
111
|
+
|
|
112
|
+
result = Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
|
113
|
+
ciphertext, clen_p,
|
|
114
|
+
message_ptr, message.bytesize,
|
|
115
|
+
ad_ptr, add.bytesize,
|
|
116
|
+
nil,
|
|
117
|
+
FFI::MemoryPointer.from_string(nonce),
|
|
118
|
+
FFI::MemoryPointer.from_string(key)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
raise 'Encryption failed' unless result.zero?
|
|
122
|
+
|
|
123
|
+
ciphertext.read_string(clen_p.read_ulong_long)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Decrypts a ciphertext using XChaCha20-Poly1305
|
|
127
|
+
#
|
|
128
|
+
# @param ciphertext [String] the encrypted data (with tag)
|
|
129
|
+
# @param key [String] 32-byte decryption key
|
|
130
|
+
# @param nonce [String] 24-byte nonce
|
|
131
|
+
# @param add [String] optional associated data
|
|
132
|
+
# @return [String] decrypted plaintext
|
|
133
|
+
def self.decrypt(ciphertext, add, nonce, key)
|
|
134
|
+
raise ArgumentError, 'Invalid key size' unless key.bytesize == KEY_BYTES
|
|
135
|
+
raise ArgumentError, 'Invalid nonce size' unless nonce.bytesize == NONCE_BYTES
|
|
136
|
+
|
|
137
|
+
c_ptr = FFI::MemoryPointer.from_string(ciphertext)
|
|
138
|
+
ad_ptr = FFI::MemoryPointer.from_string(add)
|
|
139
|
+
|
|
140
|
+
m_ptr = FFI::MemoryPointer.new(:uchar, ciphertext.bytesize - TAG_BYTES)
|
|
141
|
+
mlen_p = FFI::MemoryPointer.new(:ulong_long)
|
|
142
|
+
|
|
143
|
+
result = Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
|
144
|
+
m_ptr, mlen_p,
|
|
145
|
+
nil,
|
|
146
|
+
c_ptr, ciphertext.bytesize,
|
|
147
|
+
ad_ptr, add.bytesize,
|
|
148
|
+
FFI::MemoryPointer.from_string(nonce),
|
|
149
|
+
FFI::MemoryPointer.from_string(key)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
raise 'Decryption failed' unless result.zero?
|
|
153
|
+
|
|
154
|
+
m_ptr.read_string(mlen_p.read_ulong_long)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if RUBY_PLATFORM.match?(/mswin|mingw|windows/)
|
|
4
|
+
|
|
5
|
+
# @!visibility private
|
|
6
|
+
module OnyxCord::Voice::WinTimer
|
|
7
|
+
extend FFI::Library
|
|
8
|
+
|
|
9
|
+
ffi_lib 'winmm'
|
|
10
|
+
|
|
11
|
+
attach_function :time_begin_period, :timeBeginPeriod, [:uint], :uint
|
|
12
|
+
|
|
13
|
+
attach_function :time_end_period, :timeEndPeriod, [:uint], :uint
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
OnyxCord::Voice::WinTimer.time_begin_period(1)
|
|
17
|
+
|
|
18
|
+
at_exit { OnyxCord::Voice::WinTimer.time_end_period(1) }
|
|
19
|
+
end
|