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.

Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +126 -0
  3. data/.codeclimate.yml +16 -0
  4. data/.github/CONTRIBUTING.md +13 -0
  5. data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  7. data/.github/pull_request_template.md +37 -0
  8. data/.gitignore +5 -0
  9. data/.rubocop.yml +39 -33
  10. data/.travis.yml +27 -2
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +808 -208
  13. data/Gemfile +4 -1
  14. data/LICENSE.txt +1 -1
  15. data/README.md +108 -53
  16. data/Rakefile +14 -1
  17. data/bin/console +1 -0
  18. data/bin/travis_build_docs.sh +17 -0
  19. data/discordrb-webhooks.gemspec +26 -0
  20. data/discordrb.gemspec +24 -15
  21. data/lib/discordrb.rb +75 -2
  22. data/lib/discordrb/allowed_mentions.rb +36 -0
  23. data/lib/discordrb/api.rb +126 -27
  24. data/lib/discordrb/api/channel.rb +165 -43
  25. data/lib/discordrb/api/invite.rb +10 -7
  26. data/lib/discordrb/api/server.rb +240 -61
  27. data/lib/discordrb/api/user.rb +26 -24
  28. data/lib/discordrb/api/webhook.rb +83 -0
  29. data/lib/discordrb/await.rb +1 -2
  30. data/lib/discordrb/bot.rb +417 -149
  31. data/lib/discordrb/cache.rb +42 -10
  32. data/lib/discordrb/colour_rgb.rb +43 -0
  33. data/lib/discordrb/commands/command_bot.rb +186 -31
  34. data/lib/discordrb/commands/container.rb +30 -16
  35. data/lib/discordrb/commands/parser.rb +102 -47
  36. data/lib/discordrb/commands/rate_limiter.rb +18 -17
  37. data/lib/discordrb/container.rb +245 -41
  38. data/lib/discordrb/data.rb +27 -2511
  39. data/lib/discordrb/data/activity.rb +264 -0
  40. data/lib/discordrb/data/application.rb +50 -0
  41. data/lib/discordrb/data/attachment.rb +56 -0
  42. data/lib/discordrb/data/audit_logs.rb +345 -0
  43. data/lib/discordrb/data/channel.rb +849 -0
  44. data/lib/discordrb/data/embed.rb +251 -0
  45. data/lib/discordrb/data/emoji.rb +82 -0
  46. data/lib/discordrb/data/integration.rb +83 -0
  47. data/lib/discordrb/data/invite.rb +137 -0
  48. data/lib/discordrb/data/member.rb +297 -0
  49. data/lib/discordrb/data/message.rb +334 -0
  50. data/lib/discordrb/data/overwrite.rb +102 -0
  51. data/lib/discordrb/data/profile.rb +91 -0
  52. data/lib/discordrb/data/reaction.rb +33 -0
  53. data/lib/discordrb/data/recipient.rb +34 -0
  54. data/lib/discordrb/data/role.rb +191 -0
  55. data/lib/discordrb/data/server.rb +1002 -0
  56. data/lib/discordrb/data/user.rb +204 -0
  57. data/lib/discordrb/data/voice_region.rb +45 -0
  58. data/lib/discordrb/data/voice_state.rb +41 -0
  59. data/lib/discordrb/data/webhook.rb +145 -0
  60. data/lib/discordrb/errors.rb +36 -2
  61. data/lib/discordrb/events/bans.rb +7 -5
  62. data/lib/discordrb/events/channels.rb +2 -0
  63. data/lib/discordrb/events/generic.rb +19 -3
  64. data/lib/discordrb/events/guilds.rb +129 -6
  65. data/lib/discordrb/events/invites.rb +125 -0
  66. data/lib/discordrb/events/members.rb +6 -2
  67. data/lib/discordrb/events/message.rb +86 -36
  68. data/lib/discordrb/events/presence.rb +23 -16
  69. data/lib/discordrb/events/raw.rb +47 -0
  70. data/lib/discordrb/events/reactions.rb +159 -0
  71. data/lib/discordrb/events/roles.rb +7 -6
  72. data/lib/discordrb/events/typing.rb +9 -5
  73. data/lib/discordrb/events/voice_server_update.rb +47 -0
  74. data/lib/discordrb/events/voice_state_update.rb +29 -9
  75. data/lib/discordrb/events/webhooks.rb +64 -0
  76. data/lib/discordrb/gateway.rb +219 -88
  77. data/lib/discordrb/id_object.rb +39 -0
  78. data/lib/discordrb/light.rb +1 -1
  79. data/lib/discordrb/light/integrations.rb +1 -1
  80. data/lib/discordrb/light/light_bot.rb +1 -1
  81. data/lib/discordrb/logger.rb +12 -11
  82. data/lib/discordrb/paginator.rb +57 -0
  83. data/lib/discordrb/permissions.rb +148 -14
  84. data/lib/discordrb/version.rb +1 -1
  85. data/lib/discordrb/voice/encoder.rb +14 -15
  86. data/lib/discordrb/voice/network.rb +86 -45
  87. data/lib/discordrb/voice/sodium.rb +96 -0
  88. data/lib/discordrb/voice/voice_bot.rb +52 -40
  89. data/lib/discordrb/webhooks.rb +12 -0
  90. data/lib/discordrb/websocket.rb +2 -2
  91. 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
- RBNACL_AVAILABLE = if ENV['DISCORDRB_NONACL']
12
- false
13
- else
14
- require 'rbnacl'
15
- true
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/meew0/discordrb/wiki/Installing-libsodium for more details."
20
- RBNACL_AVAILABLE = false
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
- ENCRYPTED_MODE = 'xsalsa20_poly1305'.freeze
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
- PLAIN_MODE = 'plain'.freeze
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 endpoint [String] The voice endpoint to connect to.
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(endpoint, port, ssrc)
51
- @endpoint = endpoint
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].to_i
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
- # Encrypt data, if necessary
80
- buf = encrypt_audio(header, buf) if encrypted?
87
+ nonce = generate_nonce(header)
88
+ buf = encrypt_audio(buf, nonce)
81
89
 
82
- send_packet(header + buf)
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 RbNaCl
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(header, buf)
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
- # The nonce is the header of the voice packet with 12 null bytes appended
106
- nonce = header + ([0] * 12).pack('C*')
117
+ secret_box = Discordrb::Voice::SecretBox.new(@secret_key)
107
118
 
108
- box.encrypt(nonce, buf)
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, @endpoint, @port)
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 'RbNaCl is unavailable - unable to create voice bot! Please read https://github.com/meew0/discordrb/wiki/Installing-libsodium' unless RBNACL_AVAILABLE
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.gsub(':80', '')
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: nil
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
- @udp.connect(@endpoint, @port, @ssrc)
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::Logger.error "VWS error: #{e}" },
307
- proc { |e| Discordrb::Logger.warn "VWS close: #{e}" }
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 (20 ms as defined by Discord)
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 (20 ms).
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 = 20 ms) at which length adjustments should start.
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. e. packets will be sent every N seconds). Be careful not to set it too low as to not spam Discord's servers.
64
- # The ideal length is 20 ms (accessible by the {Discordrb::Voice::IDEAL_LENGTH} constant), this value should be
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, encrypted)
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
- @udp.encrypted?
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 100 ms to take effect, which is usually negligible.
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
- if wait_for_confirmation
164
- @has_stopped_playing = false
165
- sleep IDEAL_LENGTH / 1000.0 until @has_stopped_playing
166
- @has_stopped_playing = false
167
- end
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. e. they wait for the playback to
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
- break
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
- break if @retry_attempts.zero?
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.respond_to? :pid
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
- Process.kill('TERM', encoded_io.pid)
224
- rescue => e
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).unpack('l<')[0]
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
- break
283
+ next :stop
277
284
  end
278
285
 
279
- header = header_str.unpack('s<')[0]
286
+ header = header_str.unpack1('s<')
280
287
 
281
- raise 'Negative header in DCA file! Your file is likely corrupted.' if header < 0
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
- break
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 @io stream as Discord requires it
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 > 0
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 > 0
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::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.')
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 20 ms since nobody is going to hear it anyway
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 cleanup
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