discordrb 3.1.1 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of discordrb might be problematic. Click here for more details.
- checksums.yaml +5 -5
- data/.circleci/config.yml +126 -0
- data/.codeclimate.yml +16 -0
- data/.github/CONTRIBUTING.md +13 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
- data/.github/pull_request_template.md +37 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +39 -33
- data/.travis.yml +27 -2
- data/.yardopts +1 -1
- data/CHANGELOG.md +808 -208
- data/Gemfile +4 -1
- data/LICENSE.txt +1 -1
- data/README.md +108 -53
- data/Rakefile +14 -1
- data/bin/console +1 -0
- data/bin/travis_build_docs.sh +17 -0
- data/discordrb-webhooks.gemspec +26 -0
- data/discordrb.gemspec +24 -15
- data/lib/discordrb.rb +75 -2
- data/lib/discordrb/allowed_mentions.rb +36 -0
- data/lib/discordrb/api.rb +126 -27
- data/lib/discordrb/api/channel.rb +165 -43
- data/lib/discordrb/api/invite.rb +10 -7
- data/lib/discordrb/api/server.rb +240 -61
- data/lib/discordrb/api/user.rb +26 -24
- data/lib/discordrb/api/webhook.rb +83 -0
- data/lib/discordrb/await.rb +1 -2
- data/lib/discordrb/bot.rb +417 -149
- data/lib/discordrb/cache.rb +42 -10
- data/lib/discordrb/colour_rgb.rb +43 -0
- data/lib/discordrb/commands/command_bot.rb +186 -31
- data/lib/discordrb/commands/container.rb +30 -16
- data/lib/discordrb/commands/parser.rb +102 -47
- data/lib/discordrb/commands/rate_limiter.rb +18 -17
- data/lib/discordrb/container.rb +245 -41
- data/lib/discordrb/data.rb +27 -2511
- data/lib/discordrb/data/activity.rb +264 -0
- data/lib/discordrb/data/application.rb +50 -0
- data/lib/discordrb/data/attachment.rb +56 -0
- data/lib/discordrb/data/audit_logs.rb +345 -0
- data/lib/discordrb/data/channel.rb +849 -0
- data/lib/discordrb/data/embed.rb +251 -0
- data/lib/discordrb/data/emoji.rb +82 -0
- data/lib/discordrb/data/integration.rb +83 -0
- data/lib/discordrb/data/invite.rb +137 -0
- data/lib/discordrb/data/member.rb +297 -0
- data/lib/discordrb/data/message.rb +334 -0
- data/lib/discordrb/data/overwrite.rb +102 -0
- data/lib/discordrb/data/profile.rb +91 -0
- data/lib/discordrb/data/reaction.rb +33 -0
- data/lib/discordrb/data/recipient.rb +34 -0
- data/lib/discordrb/data/role.rb +191 -0
- data/lib/discordrb/data/server.rb +1002 -0
- data/lib/discordrb/data/user.rb +204 -0
- data/lib/discordrb/data/voice_region.rb +45 -0
- data/lib/discordrb/data/voice_state.rb +41 -0
- data/lib/discordrb/data/webhook.rb +145 -0
- data/lib/discordrb/errors.rb +36 -2
- data/lib/discordrb/events/bans.rb +7 -5
- data/lib/discordrb/events/channels.rb +2 -0
- data/lib/discordrb/events/generic.rb +19 -3
- data/lib/discordrb/events/guilds.rb +129 -6
- data/lib/discordrb/events/invites.rb +125 -0
- data/lib/discordrb/events/members.rb +6 -2
- data/lib/discordrb/events/message.rb +86 -36
- data/lib/discordrb/events/presence.rb +23 -16
- data/lib/discordrb/events/raw.rb +47 -0
- data/lib/discordrb/events/reactions.rb +159 -0
- data/lib/discordrb/events/roles.rb +7 -6
- data/lib/discordrb/events/typing.rb +9 -5
- data/lib/discordrb/events/voice_server_update.rb +47 -0
- data/lib/discordrb/events/voice_state_update.rb +29 -9
- data/lib/discordrb/events/webhooks.rb +64 -0
- data/lib/discordrb/gateway.rb +219 -88
- data/lib/discordrb/id_object.rb +39 -0
- data/lib/discordrb/light.rb +1 -1
- data/lib/discordrb/light/integrations.rb +1 -1
- data/lib/discordrb/light/light_bot.rb +1 -1
- data/lib/discordrb/logger.rb +12 -11
- data/lib/discordrb/paginator.rb +57 -0
- data/lib/discordrb/permissions.rb +148 -14
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/encoder.rb +14 -15
- data/lib/discordrb/voice/network.rb +86 -45
- data/lib/discordrb/voice/sodium.rb +96 -0
- data/lib/discordrb/voice/voice_bot.rb +52 -40
- data/lib/discordrb/webhooks.rb +12 -0
- data/lib/discordrb/websocket.rb +2 -2
- metadata +137 -34
@@ -1,58 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'websocket-client-simple'
|
4
|
-
require 'resolv'
|
5
4
|
require 'socket'
|
6
5
|
require 'json'
|
7
6
|
|
8
7
|
require 'discordrb/websocket'
|
9
8
|
|
10
9
|
begin
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
end
|
10
|
+
LIBSODIUM_AVAILABLE = if ENV['DISCORDRB_NONACL']
|
11
|
+
false
|
12
|
+
else
|
13
|
+
require 'discordrb/voice/sodium'
|
14
|
+
end
|
17
15
|
rescue LoadError
|
18
16
|
puts "libsodium not available! You can continue to use discordrb as normal but voice support won't work.
|
19
|
-
Read https://github.com/
|
20
|
-
|
17
|
+
Read https://github.com/shardlab/discordrb/wiki/Installing-libsodium for more details."
|
18
|
+
LIBSODIUM_AVAILABLE = false
|
21
19
|
end
|
22
20
|
|
23
21
|
module Discordrb::Voice
|
24
22
|
# Signifies to Discord that encryption should be used
|
25
|
-
|
23
|
+
# @deprecated Discord now supports multiple encryption options.
|
24
|
+
# TODO: Resolve replacement for this constant.
|
25
|
+
ENCRYPTED_MODE = 'xsalsa20_poly1305'
|
26
26
|
|
27
27
|
# Signifies to Discord that no encryption should be used
|
28
|
-
|
28
|
+
# @deprecated Discord no longer supports unencrypted voice communication.
|
29
|
+
PLAIN_MODE = 'plain'
|
30
|
+
|
31
|
+
# Encryption modes supported by Discord
|
32
|
+
ENCRYPTION_MODES = %w[xsalsa20_poly1305_lite xsalsa20_poly1305_suffix xsalsa20_poly1305].freeze
|
29
33
|
|
30
34
|
# Represents a UDP connection to a voice server. This connection is used to send the actual audio data.
|
31
35
|
class VoiceUDP
|
32
36
|
# @return [true, false] whether or not UDP communications are encrypted.
|
37
|
+
# @deprecated Discord no longer supports unencrypted voice communication.
|
33
38
|
attr_accessor :encrypted
|
34
39
|
alias_method :encrypted?, :encrypted
|
35
40
|
|
36
41
|
# Sets the secret key used for encryption
|
37
42
|
attr_writer :secret_key
|
38
43
|
|
44
|
+
# The UDP encryption mode
|
45
|
+
attr_reader :mode # rubocop:disable Style/BisectedAttrAccessor
|
46
|
+
|
47
|
+
# @!visibility private
|
48
|
+
attr_writer :mode # rubocop:disable Style/BisectedAttrAccessor
|
49
|
+
|
39
50
|
# Creates a new UDP connection. Only creates a socket as the discovery reply may come before the data is
|
40
51
|
# initialized.
|
41
52
|
def initialize
|
42
53
|
@socket = UDPSocket.new
|
54
|
+
@encrypted = true
|
43
55
|
end
|
44
56
|
|
45
57
|
# Initializes the UDP socket with data obtained from opcode 2.
|
46
|
-
# @param
|
58
|
+
# @param ip [String] The IP address to connect to.
|
47
59
|
# @param port [Integer] The port to connect to.
|
48
60
|
# @param ssrc [Integer] The Super Secret Relay Code (SSRC). Discord uses this to identify different voice users
|
49
61
|
# on the same endpoint.
|
50
|
-
def connect(
|
51
|
-
@
|
52
|
-
@endpoint = @endpoint[6..-1] if @endpoint.start_with? 'wss://'
|
53
|
-
@endpoint = @endpoint.gsub(':80', '') # The endpoint may contain a port, we don't want that
|
54
|
-
@endpoint = Resolv.getaddress @endpoint
|
55
|
-
|
62
|
+
def connect(ip, port, ssrc)
|
63
|
+
@ip = ip
|
56
64
|
@port = port
|
57
65
|
@ssrc = ssrc
|
58
66
|
end
|
@@ -63,7 +71,7 @@ module Discordrb::Voice
|
|
63
71
|
# Wait for a UDP message
|
64
72
|
message = @socket.recv(70)
|
65
73
|
ip = message[4..-3].delete("\0")
|
66
|
-
port = message[-2..-1].
|
74
|
+
port = message[-2..-1].unpack1('n')
|
67
75
|
[ip, port]
|
68
76
|
end
|
69
77
|
|
@@ -76,10 +84,15 @@ module Discordrb::Voice
|
|
76
84
|
# Header of the audio packet
|
77
85
|
header = [0x80, 0x78, sequence, time, @ssrc].pack('CCnNN')
|
78
86
|
|
79
|
-
|
80
|
-
buf = encrypt_audio(
|
87
|
+
nonce = generate_nonce(header)
|
88
|
+
buf = encrypt_audio(buf, nonce)
|
81
89
|
|
82
|
-
|
90
|
+
data = header + buf
|
91
|
+
|
92
|
+
# xsalsa20_poly1305 does not require an appended nonce
|
93
|
+
data += nonce unless @mode == 'xsalsa20_poly1305'
|
94
|
+
|
95
|
+
send_packet(data)
|
83
96
|
end
|
84
97
|
|
85
98
|
# Sends the UDP discovery packet with the internally stored SSRC. Discord will send a reply afterwards which can
|
@@ -94,22 +107,47 @@ module Discordrb::Voice
|
|
94
107
|
|
95
108
|
private
|
96
109
|
|
97
|
-
# Encrypts audio data using
|
98
|
-
# @param header [String] The header of the packet, to be used as the nonce
|
110
|
+
# Encrypts audio data using libsodium
|
99
111
|
# @param buf [String] The encoded audio data to be encrypted
|
112
|
+
# @param nonce [String] The nonce to be used to encrypt the data
|
100
113
|
# @return [String] the audio data, encrypted
|
101
|
-
def encrypt_audio(
|
114
|
+
def encrypt_audio(buf, nonce)
|
102
115
|
raise 'No secret key found, despite encryption being enabled!' unless @secret_key
|
103
|
-
box = RbNaCl::SecretBox.new(@secret_key)
|
104
116
|
|
105
|
-
|
106
|
-
nonce = header + ([0] * 12).pack('C*')
|
117
|
+
secret_box = Discordrb::Voice::SecretBox.new(@secret_key)
|
107
118
|
|
108
|
-
|
119
|
+
# Nonces must be 24 bytes in length. We right pad with null bytes for poly1305 and poly1305_lite
|
120
|
+
secret_box.box(nonce.ljust(24, "\0"), buf)
|
109
121
|
end
|
110
122
|
|
111
123
|
def send_packet(packet)
|
112
|
-
@socket.send(packet, 0, @
|
124
|
+
@socket.send(packet, 0, @ip, @port)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @param header [String] The header of the packet, to be used as the nonce
|
128
|
+
# @return [String]
|
129
|
+
# @note
|
130
|
+
# The nonce generated depends on the encryption mode.
|
131
|
+
# In xsalsa20_poly1305 the nonce is the header plus twelve null bytes for padding.
|
132
|
+
# In xsalsa20_poly1305_suffix, the nonce is 24 random bytes
|
133
|
+
# In xsalsa20_poly1305_lite, the nonce is an incremental 4 byte int.
|
134
|
+
def generate_nonce(header)
|
135
|
+
case @mode
|
136
|
+
when 'xsalsa20_poly1305'
|
137
|
+
header
|
138
|
+
when 'xsalsa20_poly1305_suffix'
|
139
|
+
Random.urandom(24)
|
140
|
+
when 'xsalsa20_poly1305_lite'
|
141
|
+
case @lite_nonce
|
142
|
+
when nil, 0xff_ff_ff_ff
|
143
|
+
@lite_nonce = 0
|
144
|
+
else
|
145
|
+
@lite_nonce += 1
|
146
|
+
end
|
147
|
+
[@lite_nonce].pack('N')
|
148
|
+
else
|
149
|
+
raise "`#{@mode}' is not a supported encryption mode"
|
150
|
+
end
|
113
151
|
end
|
114
152
|
end
|
115
153
|
|
@@ -117,6 +155,9 @@ module Discordrb::Voice
|
|
117
155
|
# used to manage general data about the connection, such as sending the speaking packet, which determines the green
|
118
156
|
# circle around users on Discord, and obtaining UDP connection info.
|
119
157
|
class VoiceWS
|
158
|
+
# The version of the voice gateway that's supposed to be used.
|
159
|
+
VOICE_GATEWAY_VERSION = 4
|
160
|
+
|
120
161
|
# @return [VoiceUDP] the UDP voice connection over which the actual audio data is sent.
|
121
162
|
attr_reader :udp
|
122
163
|
|
@@ -127,14 +168,14 @@ module Discordrb::Voice
|
|
127
168
|
# @param session [String] The voice session ID Discord sends over the regular websocket
|
128
169
|
# @param endpoint [String] The endpoint URL to connect to
|
129
170
|
def initialize(channel, bot, token, session, endpoint)
|
130
|
-
raise '
|
171
|
+
raise 'libsodium is unavailable - unable to create voice bot! Please read https://github.com/shardlab/discordrb/wiki/Installing-libsodium' unless LIBSODIUM_AVAILABLE
|
131
172
|
|
132
173
|
@channel = channel
|
133
174
|
@bot = bot
|
134
175
|
@token = token
|
135
176
|
@session = session
|
136
177
|
|
137
|
-
@endpoint = endpoint.
|
178
|
+
@endpoint = endpoint.split(':').first
|
138
179
|
|
139
180
|
@udp = VoiceUDP.new
|
140
181
|
end
|
@@ -181,12 +222,12 @@ module Discordrb::Voice
|
|
181
222
|
|
182
223
|
@client.send({
|
183
224
|
op: 3,
|
184
|
-
d:
|
225
|
+
d: millis
|
185
226
|
}.to_json)
|
186
227
|
end
|
187
228
|
|
188
229
|
# Send a speaking packet (op 5). This determines the green circle around the avatar in the voice channel
|
189
|
-
# @param value [true, false] Whether or not the bot should be speaking
|
230
|
+
# @param value [true, false, Integer] Whether or not the bot should be speaking, can also be a bitmask denoting audio type.
|
190
231
|
def send_speaking(value)
|
191
232
|
@bot.debug("Speaking: #{value}")
|
192
233
|
@client.send({
|
@@ -218,18 +259,23 @@ module Discordrb::Voice
|
|
218
259
|
# Opcode 2 contains data to initialize the UDP connection
|
219
260
|
@ws_data = packet['d']
|
220
261
|
|
221
|
-
@heartbeat_interval = @ws_data['heartbeat_interval']
|
222
262
|
@ssrc = @ws_data['ssrc']
|
223
263
|
@port = @ws_data['port']
|
224
|
-
@udp_mode = mode
|
225
264
|
|
226
|
-
@
|
265
|
+
@udp_mode = (ENCRYPTION_MODES & @ws_data['modes']).first
|
266
|
+
|
267
|
+
@udp.connect(@ws_data['ip'], @port, @ssrc)
|
227
268
|
@udp.send_discovery
|
228
269
|
when 4
|
229
270
|
# Opcode 4 sends the secret key used for encryption
|
230
271
|
@ws_data = packet['d']
|
272
|
+
|
231
273
|
@ready = true
|
232
274
|
@udp.secret_key = @ws_data['secret_key'].pack('C*')
|
275
|
+
@udp.mode = @ws_data['mode']
|
276
|
+
when 8
|
277
|
+
# Opcode 8 contains the heartbeat interval.
|
278
|
+
@heartbeat_interval = packet['d']['heartbeat_interval']
|
233
279
|
end
|
234
280
|
end
|
235
281
|
|
@@ -276,11 +322,6 @@ module Discordrb::Voice
|
|
276
322
|
|
277
323
|
private
|
278
324
|
|
279
|
-
# @return [String] the mode string that signifies whether encryption should be used or not
|
280
|
-
def mode
|
281
|
-
@udp.encrypted? ? ENCRYPTED_MODE : PLAIN_MODE
|
282
|
-
end
|
283
|
-
|
284
325
|
def heartbeat_loop
|
285
326
|
@heartbeat_running = true
|
286
327
|
while @heartbeat_running
|
@@ -295,7 +336,7 @@ module Discordrb::Voice
|
|
295
336
|
end
|
296
337
|
|
297
338
|
def init_ws
|
298
|
-
host = "wss://#{@endpoint}:443"
|
339
|
+
host = "wss://#{@endpoint}:443/?v=#{VOICE_GATEWAY_VERSION}"
|
299
340
|
@bot.debug("Connecting VWS to host: #{host}")
|
300
341
|
|
301
342
|
# Connect the WS
|
@@ -303,8 +344,8 @@ module Discordrb::Voice
|
|
303
344
|
host,
|
304
345
|
method(:websocket_open),
|
305
346
|
method(:websocket_message),
|
306
|
-
proc { |e| Discordrb::
|
307
|
-
proc { |e| Discordrb::
|
347
|
+
proc { |e| Discordrb::LOGGER.error "VWS error: #{e}" },
|
348
|
+
proc { |e| Discordrb::LOGGER.warn "VWS close: #{e}" }
|
308
349
|
)
|
309
350
|
|
310
351
|
@bot.debug('VWS connected')
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Discordrb::Voice
|
4
|
+
# @!visibility private
|
5
|
+
module Sodium
|
6
|
+
extend FFI::Library
|
7
|
+
|
8
|
+
ffi_lib(['sodium', 'libsodium.so.18', 'libsodium.so.23'])
|
9
|
+
|
10
|
+
# Encryption & decryption
|
11
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305, %i[pointer pointer ulong_long pointer pointer], :int)
|
12
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_open, %i[pointer pointer ulong_long pointer pointer], :int)
|
13
|
+
|
14
|
+
# Constants
|
15
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_keybytes, [], :size_t)
|
16
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_noncebytes, [], :size_t)
|
17
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_zerobytes, [], :size_t)
|
18
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_boxzerobytes, [], :size_t)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Utility class for interacting with required `xsalsa20poly1305` functions for voice transmission
|
22
|
+
# @!visibility private
|
23
|
+
class SecretBox
|
24
|
+
# Exception raised when a key or nonce with invalid length is used
|
25
|
+
class LengthError < RuntimeError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Exception raised when encryption or decryption fails
|
29
|
+
class CryptoError < RuntimeError
|
30
|
+
end
|
31
|
+
|
32
|
+
# Required key length
|
33
|
+
KEY_LENGTH = Sodium.crypto_secretbox_xsalsa20poly1305_keybytes
|
34
|
+
|
35
|
+
# Required nonce length
|
36
|
+
NONCE_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_noncebytes
|
37
|
+
|
38
|
+
# Zero byte padding for encryption
|
39
|
+
ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_zerobytes
|
40
|
+
|
41
|
+
# Zero byte padding for decryption
|
42
|
+
BOX_ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_boxzerobytes
|
43
|
+
|
44
|
+
# @param key [String] Crypto key of length {KEY_LENGTH}
|
45
|
+
def initialize(key)
|
46
|
+
raise(LengthError, 'Key length') if key.bytesize != KEY_LENGTH
|
47
|
+
|
48
|
+
@key = key
|
49
|
+
end
|
50
|
+
|
51
|
+
# Encrypts a message using this box's key
|
52
|
+
# @param nonce [String] encryption nonce for this message
|
53
|
+
# @param message [String] message to be encrypted
|
54
|
+
def box(nonce, message)
|
55
|
+
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
56
|
+
|
57
|
+
message_padded = prepend_zeroes(ZERO_BYTES, message)
|
58
|
+
buffer = zero_string(message_padded.bytesize)
|
59
|
+
|
60
|
+
success = Sodium.crypto_secretbox_xsalsa20poly1305(buffer, message_padded, message_padded.bytesize, nonce, @key)
|
61
|
+
raise(CryptoError, "Encryption failed (#{success})") unless success.zero?
|
62
|
+
|
63
|
+
remove_zeroes(BOX_ZERO_BYTES, buffer)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Decrypts the given ciphertext using this box's key
|
67
|
+
# @param nonce [String] encryption nonce for this ciphertext
|
68
|
+
# @param ciphertext [String] ciphertext to decrypt
|
69
|
+
def open(nonce, ciphertext)
|
70
|
+
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
71
|
+
|
72
|
+
ct_padded = prepend_zeroes(BOX_ZERO_BYTES, ciphertext)
|
73
|
+
buffer = zero_string(ct_padded.bytesize)
|
74
|
+
|
75
|
+
success = Sodium.crypto_secretbox_xsalsa20poly1305_open(buffer, ct_padded, ct_padded.bytesize, nonce, @key)
|
76
|
+
raise(CryptoError, "Decryption failed (#{success})") unless success.zero?
|
77
|
+
|
78
|
+
remove_zeroes(ZERO_BYTES, buffer)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def zero_string(size)
|
84
|
+
str = "\0" * size
|
85
|
+
str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding)
|
86
|
+
end
|
87
|
+
|
88
|
+
def prepend_zeroes(size, string)
|
89
|
+
zero_string(size) + string
|
90
|
+
end
|
91
|
+
|
92
|
+
def remove_zeroes(size, string)
|
93
|
+
string.slice!(size, string.bytesize - size)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -6,7 +6,7 @@ require 'discordrb/logger'
|
|
6
6
|
|
7
7
|
# Voice support
|
8
8
|
module Discordrb::Voice
|
9
|
-
# How long one voice packet should ideally be (
|
9
|
+
# How long one voice packet should ideally be (20ms as defined by Discord)
|
10
10
|
IDEAL_LENGTH = 20.0
|
11
11
|
|
12
12
|
# How many bytes of data to read (1920 bytes * 2 channels) from audio PCM data
|
@@ -22,7 +22,10 @@ module Discordrb::Voice
|
|
22
22
|
# {VoiceBot#adjust_offset}, and {VoiceBot#adjust_average}.
|
23
23
|
class VoiceBot
|
24
24
|
# @return [Channel] the current voice channel
|
25
|
-
attr_reader :channel
|
25
|
+
attr_reader :channel # rubocop:disable Style/BisectedAttrAccessor
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
attr_writer :channel # rubocop:disable Style/BisectedAttrAccessor
|
26
29
|
|
27
30
|
# @return [Integer, nil] the amount of time the stream has been playing, or `nil` if nothing has been played yet.
|
28
31
|
attr_reader :stream_time
|
@@ -36,7 +39,7 @@ module Discordrb::Voice
|
|
36
39
|
# play parts back too fast. How often these measurements should be done depends a lot on the system, and if it's
|
37
40
|
# done too quickly, especially on slow connections, the playback speed will vary wildly; if it's done too slowly
|
38
41
|
# however, small errors will cause quality problems for a longer time.
|
39
|
-
# @return [Integer] how frequently audio length adjustments should be done, in ideal packets (
|
42
|
+
# @return [Integer] how frequently audio length adjustments should be done, in ideal packets (20ms).
|
40
43
|
attr_accessor :adjust_interval
|
41
44
|
|
42
45
|
# This particular value is also important because ffmpeg may take longer to process the first few packets. It is
|
@@ -44,7 +47,7 @@ module Discordrb::Voice
|
|
44
47
|
# shouldn't be any higher than {#adjust_interval}, otherwise no adjustments will take place. If {#adjust_interval}
|
45
48
|
# is at a value higher than 10, this value should not be changed at all.
|
46
49
|
# @see #adjust_interval
|
47
|
-
# @return [Integer] the packet number (1 packet =
|
50
|
+
# @return [Integer] the packet number (1 packet = 20ms) at which length adjustments should start.
|
48
51
|
attr_accessor :adjust_offset
|
49
52
|
|
50
53
|
# This value determines whether or not the adjustment length should be averaged with the previous value. This may
|
@@ -60,8 +63,8 @@ module Discordrb::Voice
|
|
60
63
|
attr_accessor :adjust_debug
|
61
64
|
|
62
65
|
# If this value is set, no length adjustments will ever be done and this value will always be used as the length
|
63
|
-
# (i.
|
64
|
-
# The ideal length is
|
66
|
+
# (i.e. packets will be sent every N seconds). Be careful not to set it too low as to not spam Discord's servers.
|
67
|
+
# The ideal length is 20ms (accessible by the {Discordrb::Voice::IDEAL_LENGTH} constant), this value should be
|
65
68
|
# slightly lower than that because encoding + sending takes time. Note that sending DCA files is significantly
|
66
69
|
# faster than sending regular audio files (usually about four times as fast), so you might want to set this value
|
67
70
|
# to something else if you're sending a DCA file.
|
@@ -74,13 +77,12 @@ module Discordrb::Voice
|
|
74
77
|
attr_accessor :volume
|
75
78
|
|
76
79
|
# @!visibility private
|
77
|
-
def initialize(channel, bot, token, session, endpoint
|
80
|
+
def initialize(channel, bot, token, session, endpoint)
|
78
81
|
@bot = bot
|
79
82
|
@channel = channel
|
80
83
|
|
81
84
|
@ws = VoiceWS.new(channel, bot, token, session, endpoint)
|
82
85
|
@udp = @ws.udp
|
83
|
-
@udp.encrypted = encrypted
|
84
86
|
|
85
87
|
@sequence = @time = 0
|
86
88
|
@skips = 0
|
@@ -95,14 +97,15 @@ module Discordrb::Voice
|
|
95
97
|
|
96
98
|
@encoder = Encoder.new
|
97
99
|
@ws.connect
|
98
|
-
rescue => e
|
100
|
+
rescue StandardError => e
|
99
101
|
Discordrb::LOGGER.log_exception(e)
|
100
102
|
raise
|
101
103
|
end
|
102
104
|
|
103
105
|
# @return [true, false] whether audio data sent will be encrypted.
|
106
|
+
# @deprecated Discord no longer supports unencrypted voice communication.
|
104
107
|
def encrypted?
|
105
|
-
|
108
|
+
true
|
106
109
|
end
|
107
110
|
|
108
111
|
# Set the filter volume. This volume is applied as a filter for decoded audio data. It has the advantage that using
|
@@ -132,7 +135,7 @@ module Discordrb::Voice
|
|
132
135
|
|
133
136
|
alias_method :isplaying?, :playing?
|
134
137
|
|
135
|
-
# Continue playback. This change may take up to
|
138
|
+
# Continue playback. This change may take up to 100ms to take effect, which is usually negligible.
|
136
139
|
def continue
|
137
140
|
@paused = false
|
138
141
|
end
|
@@ -145,7 +148,8 @@ module Discordrb::Voice
|
|
145
148
|
end
|
146
149
|
|
147
150
|
# Sets whether or not the bot is speaking (green circle around user).
|
148
|
-
# @param value [true, false] whether or not the bot should be speaking
|
151
|
+
# @param value [true, false, Integer] whether or not the bot should be speaking, or a bitmask denoting the audio type
|
152
|
+
# @note https://discordapp.com/developers/docs/topics/voice-connections#speaking for information on the speaking bitmask
|
149
153
|
def speaking=(value)
|
150
154
|
@playing = value
|
151
155
|
@ws.send_speaking(value)
|
@@ -160,11 +164,11 @@ module Discordrb::Voice
|
|
160
164
|
@playing = false
|
161
165
|
sleep IDEAL_LENGTH / 1000.0 if @was_playing_before
|
162
166
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
167
|
+
return unless wait_for_confirmation
|
168
|
+
|
169
|
+
@has_stopped_playing = false
|
170
|
+
sleep IDEAL_LENGTH / 1000.0 until @has_stopped_playing
|
171
|
+
@has_stopped_playing = false
|
168
172
|
end
|
169
173
|
|
170
174
|
# Permanently disconnects from the voice channel; to reconnect you will have to call {Bot#voice_connect} again.
|
@@ -174,7 +178,7 @@ module Discordrb::Voice
|
|
174
178
|
@ws.destroy
|
175
179
|
end
|
176
180
|
|
177
|
-
# Plays a stream of raw data to the channel. All playback methods are blocking, i.
|
181
|
+
# Plays a stream of raw data to the channel. All playback methods are blocking, i.e. they wait for the playback to
|
178
182
|
# finish before exiting the method. This doesn't cause a problem if you just use discordrb events/commands to
|
179
183
|
# play stuff, as these are fully threaded, but if you don't want this behaviour anyway, be sure to call these
|
180
184
|
# methods in separate threads.
|
@@ -194,20 +198,20 @@ module Discordrb::Voice
|
|
194
198
|
raise IOError, 'File or stream not found!' if @first_packet
|
195
199
|
|
196
200
|
@bot.debug('EOF while reading, breaking immediately')
|
197
|
-
|
201
|
+
next :stop
|
198
202
|
end
|
199
203
|
|
200
204
|
# Check whether the buffer has enough data
|
201
205
|
if !buf || buf.length != DATA_LENGTH
|
202
206
|
@bot.debug("No data is available! Retrying #{@retry_attempts} more times")
|
203
|
-
|
207
|
+
next :stop if @retry_attempts.zero?
|
204
208
|
|
205
209
|
@retry_attempts -= 1
|
206
210
|
next
|
207
211
|
end
|
208
212
|
|
209
213
|
# Adjust volume
|
210
|
-
buf = @encoder.adjust_volume(buf, @volume) if @volume != 1.0
|
214
|
+
buf = @encoder.adjust_volume(buf, @volume) if @volume != 1.0 # rubocop:disable Lint/FloatComparison
|
211
215
|
|
212
216
|
@first_packet = false
|
213
217
|
|
@@ -216,12 +220,15 @@ module Discordrb::Voice
|
|
216
220
|
end
|
217
221
|
|
218
222
|
# If the stream is a process, kill it
|
219
|
-
if encoded_io
|
223
|
+
if encoded_io&.pid
|
220
224
|
Discordrb::LOGGER.debug("Killing ffmpeg process with pid #{encoded_io.pid.inspect}")
|
221
225
|
|
222
226
|
begin
|
223
|
-
|
224
|
-
|
227
|
+
pid = encoded_io.pid
|
228
|
+
# Windows does not support TERM as a kill signal, so we use KILL. `Process.waitpid` verifies that our
|
229
|
+
# child process has not already completed.
|
230
|
+
Process.kill(Gem.win_platform? ? 'KILL' : 'TERM', pid) if Process.waitpid(pid, Process::WNOHANG).nil?
|
231
|
+
rescue StandardError => e
|
225
232
|
Discordrb::LOGGER.warn('Failed to kill ffmpeg process! You *might* have a process leak now.')
|
226
233
|
Discordrb::LOGGER.warn("Reason: #{e}")
|
227
234
|
end
|
@@ -234,15 +241,15 @@ module Discordrb::Voice
|
|
234
241
|
# Plays an encoded audio file of arbitrary format to the channel.
|
235
242
|
# @see Encoder#encode_file
|
236
243
|
# @see #play
|
237
|
-
def play_file(file)
|
238
|
-
play @encoder.encode_file(file)
|
244
|
+
def play_file(file, options = '')
|
245
|
+
play @encoder.encode_file(file, options)
|
239
246
|
end
|
240
247
|
|
241
248
|
# Plays a stream of encoded audio data of arbitrary format to the channel.
|
242
249
|
# @see Encoder#encode_io
|
243
250
|
# @see #play
|
244
|
-
def play_io(io)
|
245
|
-
play @encoder.encode_io(io)
|
251
|
+
def play_io(io, options = '')
|
252
|
+
play @encoder.encode_io(io, options)
|
246
253
|
end
|
247
254
|
|
248
255
|
# Plays a stream of audio data in the DCA format. This format has the advantage that no recoding has to be
|
@@ -256,13 +263,13 @@ module Discordrb::Voice
|
|
256
263
|
stop_playing(true) if @playing
|
257
264
|
|
258
265
|
@bot.debug "Reading DCA file #{file}"
|
259
|
-
input_stream = open(file)
|
266
|
+
input_stream = File.open(file)
|
260
267
|
|
261
268
|
magic = input_stream.read(4)
|
262
269
|
raise ArgumentError, 'Not a DCA1 file! The file might have been corrupted, please recreate it.' unless magic == 'DCA1'
|
263
270
|
|
264
271
|
# Read the metadata header, then read the metadata and discard it as we don't care about it
|
265
|
-
metadata_header = input_stream.read(4).
|
272
|
+
metadata_header = input_stream.read(4).unpack1('l<')
|
266
273
|
input_stream.read(metadata_header)
|
267
274
|
|
268
275
|
# Play the data, without re-encoding it to opus
|
@@ -273,15 +280,15 @@ module Discordrb::Voice
|
|
273
280
|
|
274
281
|
unless header_str
|
275
282
|
@bot.debug 'Finished DCA parsing (header is nil)'
|
276
|
-
|
283
|
+
next :stop
|
277
284
|
end
|
278
285
|
|
279
|
-
header = header_str.
|
286
|
+
header = header_str.unpack1('s<')
|
280
287
|
|
281
|
-
raise 'Negative header in DCA file! Your file is likely corrupted.' if header
|
288
|
+
raise 'Negative header in DCA file! Your file is likely corrupted.' if header.negative?
|
282
289
|
rescue EOFError
|
283
290
|
@bot.debug 'Finished DCA parsing (EOFError)'
|
284
|
-
|
291
|
+
next :stop
|
285
292
|
end
|
286
293
|
|
287
294
|
# Read bytes
|
@@ -293,7 +300,7 @@ module Discordrb::Voice
|
|
293
300
|
|
294
301
|
private
|
295
302
|
|
296
|
-
# Plays the data from the
|
303
|
+
# Plays the data from the IO stream as Discord requires it
|
297
304
|
def play_internal
|
298
305
|
count = 0
|
299
306
|
@playing = true
|
@@ -312,7 +319,7 @@ module Discordrb::Voice
|
|
312
319
|
break unless @playing
|
313
320
|
|
314
321
|
# If we should skip, get some data, discard it and go to the next iteration
|
315
|
-
if @skips
|
322
|
+
if @skips.positive?
|
316
323
|
@skips -= 1
|
317
324
|
yield
|
318
325
|
next
|
@@ -324,6 +331,11 @@ module Discordrb::Voice
|
|
324
331
|
|
325
332
|
# Get packet data
|
326
333
|
buf = yield
|
334
|
+
|
335
|
+
# Stop doing anything if the stop signal was sent
|
336
|
+
break if buf == :stop
|
337
|
+
|
338
|
+
# Proceed to the next packet if we got nil
|
327
339
|
next unless buf
|
328
340
|
|
329
341
|
# Track intermediate adjustment so we can measure how much encoding contributes to the total time
|
@@ -360,11 +372,11 @@ module Discordrb::Voice
|
|
360
372
|
# If paused, wait
|
361
373
|
sleep 0.1 while @paused
|
362
374
|
|
363
|
-
if @length
|
375
|
+
if @length.positive?
|
364
376
|
# Wait `length` ms, then send the next packet
|
365
377
|
sleep @length / 1000.0
|
366
378
|
else
|
367
|
-
Discordrb::
|
379
|
+
Discordrb::LOGGER.warn('Audio encoding and sending together took longer than Discord expects one packet to be (20 ms)! This may be indicative of network problems.')
|
368
380
|
end
|
369
381
|
end
|
370
382
|
|
@@ -374,13 +386,13 @@ module Discordrb::Voice
|
|
374
386
|
increment_packet_headers
|
375
387
|
@udp.send_audio(Encoder::OPUS_SILENCE, @sequence, @time)
|
376
388
|
|
377
|
-
# Length adjustments don't matter here, we can just wait
|
389
|
+
# Length adjustments don't matter here, we can just wait 20ms since nobody is going to hear it anyway
|
378
390
|
sleep IDEAL_LENGTH / 1000.0
|
379
391
|
end
|
380
392
|
|
381
393
|
@bot.debug('Performing final cleanup after stream ended')
|
382
394
|
|
383
|
-
# Final
|
395
|
+
# Final clean-up
|
384
396
|
stop_playing
|
385
397
|
|
386
398
|
# Notify any stop_playing methods running right now that we have actually stopped
|