discordrb 3.3.0 → 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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +126 -0
  3. data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
  6. data/.github/pull_request_template.md +37 -0
  7. data/.rubocop.yml +34 -37
  8. data/.travis.yml +5 -6
  9. data/CHANGELOG.md +472 -347
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +61 -79
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +6 -6
  16. data/discordrb.gemspec +17 -17
  17. data/lib/discordrb.rb +73 -0
  18. data/lib/discordrb/allowed_mentions.rb +36 -0
  19. data/lib/discordrb/api.rb +40 -15
  20. data/lib/discordrb/api/channel.rb +57 -39
  21. data/lib/discordrb/api/invite.rb +3 -3
  22. data/lib/discordrb/api/server.rb +55 -50
  23. data/lib/discordrb/api/user.rb +8 -8
  24. data/lib/discordrb/api/webhook.rb +6 -6
  25. data/lib/discordrb/await.rb +0 -1
  26. data/lib/discordrb/bot.rb +164 -72
  27. data/lib/discordrb/cache.rb +4 -2
  28. data/lib/discordrb/colour_rgb.rb +43 -0
  29. data/lib/discordrb/commands/command_bot.rb +22 -6
  30. data/lib/discordrb/commands/container.rb +20 -23
  31. data/lib/discordrb/commands/parser.rb +18 -18
  32. data/lib/discordrb/commands/rate_limiter.rb +3 -2
  33. data/lib/discordrb/container.rb +77 -17
  34. data/lib/discordrb/data.rb +25 -4180
  35. data/lib/discordrb/data/activity.rb +264 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +56 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +849 -0
  40. data/lib/discordrb/data/embed.rb +251 -0
  41. data/lib/discordrb/data/emoji.rb +82 -0
  42. data/lib/discordrb/data/integration.rb +83 -0
  43. data/lib/discordrb/data/invite.rb +137 -0
  44. data/lib/discordrb/data/member.rb +297 -0
  45. data/lib/discordrb/data/message.rb +334 -0
  46. data/lib/discordrb/data/overwrite.rb +102 -0
  47. data/lib/discordrb/data/profile.rb +91 -0
  48. data/lib/discordrb/data/reaction.rb +33 -0
  49. data/lib/discordrb/data/recipient.rb +34 -0
  50. data/lib/discordrb/data/role.rb +191 -0
  51. data/lib/discordrb/data/server.rb +1002 -0
  52. data/lib/discordrb/data/user.rb +204 -0
  53. data/lib/discordrb/data/voice_region.rb +45 -0
  54. data/lib/discordrb/data/voice_state.rb +41 -0
  55. data/lib/discordrb/data/webhook.rb +145 -0
  56. data/lib/discordrb/errors.rb +2 -1
  57. data/lib/discordrb/events/bans.rb +7 -5
  58. data/lib/discordrb/events/channels.rb +2 -0
  59. data/lib/discordrb/events/guilds.rb +16 -9
  60. data/lib/discordrb/events/invites.rb +125 -0
  61. data/lib/discordrb/events/members.rb +6 -2
  62. data/lib/discordrb/events/message.rb +69 -27
  63. data/lib/discordrb/events/presence.rb +14 -4
  64. data/lib/discordrb/events/raw.rb +1 -3
  65. data/lib/discordrb/events/reactions.rb +49 -3
  66. data/lib/discordrb/events/typing.rb +6 -4
  67. data/lib/discordrb/events/voice_server_update.rb +47 -0
  68. data/lib/discordrb/events/voice_state_update.rb +15 -10
  69. data/lib/discordrb/events/webhooks.rb +9 -6
  70. data/lib/discordrb/gateway.rb +72 -57
  71. data/lib/discordrb/id_object.rb +39 -0
  72. data/lib/discordrb/light/integrations.rb +1 -1
  73. data/lib/discordrb/light/light_bot.rb +1 -1
  74. data/lib/discordrb/logger.rb +4 -4
  75. data/lib/discordrb/paginator.rb +57 -0
  76. data/lib/discordrb/permissions.rb +103 -8
  77. data/lib/discordrb/version.rb +1 -1
  78. data/lib/discordrb/voice/encoder.rb +3 -3
  79. data/lib/discordrb/voice/network.rb +84 -43
  80. data/lib/discordrb/voice/sodium.rb +96 -0
  81. data/lib/discordrb/voice/voice_bot.rb +34 -26
  82. metadata +93 -55
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # Mixin for objects that have IDs
5
+ module IDObject
6
+ # @return [Integer] the ID which uniquely identifies this object across Discord.
7
+ attr_reader :id
8
+ alias_method :resolve_id, :id
9
+ alias_method :hash, :id
10
+
11
+ # ID based comparison
12
+ def ==(other)
13
+ Discordrb.id_compare(@id, other)
14
+ end
15
+
16
+ alias_method :eql?, :==
17
+
18
+ # Estimates the time this object was generated on based on the beginning of the ID. This is fairly accurate but
19
+ # shouldn't be relied on as Discord might change its algorithm at any time
20
+ # @return [Time] when this object was created at
21
+ def creation_time
22
+ # Milliseconds
23
+ ms = (@id >> 22) + DISCORD_EPOCH
24
+ Time.at(ms / 1000.0)
25
+ end
26
+
27
+ # Creates an artificial snowflake at the given point in time. Useful for comparing against.
28
+ # @param time [Time] The time the snowflake should represent.
29
+ # @return [Integer] a snowflake with the timestamp data as the given time
30
+ def self.synthesise(time)
31
+ ms = (time.to_f * 1000).to_i
32
+ (ms - DISCORD_EPOCH) << 22
33
+ end
34
+
35
+ class << self
36
+ alias_method :synthesize, :synthesise
37
+ end
38
+ end
39
+ end
@@ -49,7 +49,7 @@ module Discordrb::Light
49
49
  # Twitch account connection of the server owner).
50
50
  attr_reader :server_connection
51
51
 
52
- # @return [Connection] the connection integrated with the server (i. e. your connection)
52
+ # @return [Connection] the connection integrated with the server (i.e. your connection)
53
53
  attr_reader :integrated_connection
54
54
 
55
55
  # @!visibility private
@@ -6,7 +6,7 @@ require 'discordrb/api/user'
6
6
  require 'discordrb/light/data'
7
7
  require 'discordrb/light/integrations'
8
8
 
9
- # This module contains classes to allow connections to bots without a connection to the gateway socket, i. e. bots
9
+ # This module contains classes to allow connections to bots without a connection to the gateway socket, i.e. bots
10
10
  # that only use the REST part of the API.
11
11
  module Discordrb::Light
12
12
  # A bot that only uses the REST part of the API. Hierarchically unrelated to the regular {Discordrb::Bot}. Useful to
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Discordrb
4
4
  # The format log timestamps should be in, in strftime format
5
- LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'.freeze
5
+ LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'
6
6
 
7
7
  # Logs debug messages
8
8
  class Logger
@@ -18,7 +18,7 @@ module Discordrb
18
18
  # Creates a new logger.
19
19
  # @param fancy [true, false] Whether this logger uses fancy mode (ANSI escape codes to make the output colourful)
20
20
  # @param streams [Array<IO>, Array<#puts & #flush>] the streams the logger should write to.
21
- def initialize(fancy = false, streams = [STDOUT])
21
+ def initialize(fancy = false, streams = [$stdout])
22
22
  @fancy = fancy
23
23
  self.mode = :normal
24
24
 
@@ -38,10 +38,10 @@ module Discordrb
38
38
  }.freeze
39
39
 
40
40
  # The ANSI format code that resets formatting
41
- FORMAT_RESET = "\u001B[0m".freeze
41
+ FORMAT_RESET = "\u001B[0m"
42
42
 
43
43
  # The ANSI format code that makes something bold
44
- FORMAT_BOLD = "\u001B[1m".freeze
44
+ FORMAT_BOLD = "\u001B[1m"
45
45
 
46
46
  MODES.each do |mode, hash|
47
47
  define_method(mode) do |message|
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discordrb
4
+ # Utility class for wrapping paginated endpoints. It is [Enumerable](https://ruby-doc.org/core-2.5.1/Enumerable.html),
5
+ # similar to an `Array`, so most of the same methods can be used to filter the results of the request
6
+ # that it wraps. If you simply want an array of all of the results, `#to_a` can be called.
7
+ class Paginator
8
+ include Enumerable
9
+
10
+ # Creates a new {Paginator}
11
+ # @param limit [Integer] the maximum number of items to request before stopping
12
+ # @param direction [:up, :down] the order in which results are returned in
13
+ # @yield [Array, nil] the last page of results, or nil if this is the first iteration.
14
+ # This should be used to request the next page of results.
15
+ # @yieldreturn [Array] the next page of results
16
+ def initialize(limit, direction, &block)
17
+ @count = 0
18
+ @limit = limit
19
+ @direction = direction
20
+ @block = block
21
+ end
22
+
23
+ # Yields every item produced by the wrapped request, until it returns
24
+ # no more results or the configured `limit` is reached.
25
+ def each
26
+ last_page = nil
27
+ until limit_check
28
+ page = @block.call(last_page)
29
+ return if page.empty?
30
+
31
+ enumerator = case @direction
32
+ when :down
33
+ page.each
34
+ when :up
35
+ page.reverse_each
36
+ end
37
+
38
+ enumerator.each do |item|
39
+ yield item
40
+ @count += 1
41
+ break if limit_check
42
+ end
43
+
44
+ last_page = page
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Whether the paginator limit has been exceeded
51
+ def limit_check
52
+ return false if @limit.nil?
53
+
54
+ @count >= @limit
55
+ end
56
+ end
57
+ end
@@ -4,8 +4,7 @@ module Discordrb
4
4
  # List of permissions Discord uses
5
5
  class Permissions
6
6
  # This hash maps bit positions to logical permissions.
7
- # I'm not sure what the unlabeled bits are reserved for.
8
- Flags = {
7
+ FLAGS = {
9
8
  # Bit => Permission # Value
10
9
  0 => :create_instant_invite, # 1
11
10
  1 => :kick_members, # 2
@@ -16,7 +15,7 @@ module Discordrb
16
15
  6 => :add_reactions, # 64
17
16
  7 => :view_audit_log, # 128
18
17
  8 => :priority_speaker, # 256
19
- # 9 # 512
18
+ 9 => :stream, # 512
20
19
  10 => :read_messages, # 1024
21
20
  11 => :send_messages, # 2048
22
21
  12 => :send_tts_messages, # 4096
@@ -26,7 +25,7 @@ module Discordrb
26
25
  16 => :read_message_history, # 65536
27
26
  17 => :mention_everyone, # 131072
28
27
  18 => :use_external_emoji, # 262144
29
- # 19 # 524288
28
+ 19 => :view_server_insights, # 524288
30
29
  20 => :connect, # 1048576
31
30
  21 => :speak, # 2097152
32
31
  22 => :mute_members, # 4194304
@@ -40,8 +39,9 @@ module Discordrb
40
39
  30 => :manage_emojis # 1073741824
41
40
  }.freeze
42
41
 
43
- Flags.each do |position, flag|
42
+ FLAGS.each do |position, flag|
44
43
  attr_reader flag
44
+
45
45
  define_method "can_#{flag}=" do |value|
46
46
  new_bits = @bits
47
47
  if value
@@ -49,7 +49,7 @@ module Discordrb
49
49
  else
50
50
  new_bits &= ~(1 << position)
51
51
  end
52
- @writer.write(new_bits) if @writer
52
+ @writer&.write(new_bits)
53
53
  @bits = new_bits
54
54
  init_vars
55
55
  end
@@ -69,7 +69,7 @@ module Discordrb
69
69
 
70
70
  # Initialize the instance variables based on the bitset.
71
71
  def init_vars
72
- Flags.each do |position, flag|
72
+ FLAGS.each do |position, flag|
73
73
  flag_set = ((@bits >> position) & 0x1) == 1
74
74
  instance_variable_set "@#{flag}", flag_set
75
75
  end
@@ -85,7 +85,7 @@ module Discordrb
85
85
  def self.bits(list)
86
86
  value = 0
87
87
 
88
- Flags.each do |position, flag|
88
+ FLAGS.each do |position, flag|
89
89
  value += 2**position if list.include? flag
90
90
  end
91
91
 
@@ -121,4 +121,99 @@ module Discordrb
121
121
  bits == other.bits
122
122
  end
123
123
  end
124
+
125
+ # Mixin to calculate resulting permissions from overrides etc.
126
+ module PermissionCalculator
127
+ # Checks whether this user can do the particular action, regardless of whether it has the permission defined,
128
+ # through for example being the server owner or having the Manage Roles permission
129
+ # @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
130
+ # @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
131
+ # @example Check if the bot can send messages to a specific channel in a server.
132
+ # bot_profile = bot.profile.on(event.server)
133
+ # can_send_messages = bot_profile.permission?(:send_messages, channel)
134
+ # @return [true, false] whether or not this user has the permission.
135
+ def permission?(action, channel = nil)
136
+ # If the member is the server owner, it irrevocably has all permissions.
137
+ return true if owner?
138
+
139
+ # First, check whether the user has Manage Roles defined.
140
+ # (Coincidentally, Manage Permissions is the same permission as Manage Roles, and a
141
+ # Manage Permissions deny overwrite will override Manage Roles, so we can just check for
142
+ # Manage Roles once and call it a day.)
143
+ return true if defined_permission?(:administrator, channel)
144
+
145
+ # Otherwise, defer to defined_permission
146
+ defined_permission?(action, channel)
147
+ end
148
+
149
+ # Checks whether this user has a particular permission defined (i.e. not implicit, through for example
150
+ # Manage Roles)
151
+ # @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
152
+ # @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
153
+ # @example Check if a member has the Manage Channels permission defined in the server.
154
+ # has_manage_channels = member.defined_permission?(:manage_channels)
155
+ # @return [true, false] whether or not this user has the permission defined.
156
+ def defined_permission?(action, channel = nil)
157
+ # Get the permission the user's roles have
158
+ role_permission = defined_role_permission?(action, channel)
159
+
160
+ # Once we have checked the role permission, we have to check the channel overrides for the
161
+ # specific user
162
+ user_specific_override = permission_overwrite(action, channel, id) # Use the ID reader as members have no ID instance variable
163
+
164
+ # Merge the two permissions - if an override is defined, it has to be allow, otherwise we only care about the role
165
+ return role_permission unless user_specific_override
166
+
167
+ user_specific_override == :allow
168
+ end
169
+
170
+ # Define methods for querying permissions
171
+ Discordrb::Permissions::FLAGS.each_value do |flag|
172
+ define_method "can_#{flag}?" do |channel = nil|
173
+ permission? flag, channel
174
+ end
175
+ end
176
+
177
+ alias_method :can_administrate?, :can_administrator?
178
+
179
+ private
180
+
181
+ def defined_role_permission?(action, channel)
182
+ roles_to_check = [@server.everyone_role] + @roles
183
+
184
+ # For each role, check if
185
+ # (1) the channel explicitly allows or permits an action for the role and
186
+ # (2) if the user is allowed to do the action if the channel doesn't specify
187
+ roles_to_check.sort_by(&:position).reduce(false) do |can_act, role|
188
+ # Get the override defined for the role on the channel
189
+ channel_allow = permission_overwrite(action, channel, role.id)
190
+ if channel_allow
191
+ # If the channel has an override, check whether it is an allow - if yes,
192
+ # the user can act, if not, it can't
193
+ break true if channel_allow == :allow
194
+
195
+ false
196
+ else
197
+ # Otherwise defer to the role
198
+ role.permissions.instance_variable_get("@#{action}") || can_act
199
+ end
200
+ end
201
+ end
202
+
203
+ def permission_overwrite(action, channel, id)
204
+ # If no overwrites are defined, or no channel is set, no overwrite will be present
205
+ return nil unless channel && channel.permission_overwrites[id]
206
+
207
+ # Otherwise, check the allow and deny objects
208
+ allow = channel.permission_overwrites[id].allow
209
+ deny = channel.permission_overwrites[id].deny
210
+ if allow.instance_variable_get("@#{action}")
211
+ :allow
212
+ elsif deny.instance_variable_get("@#{action}")
213
+ :deny
214
+ end
215
+
216
+ # If there's no variable defined, nil will implicitly be returned
217
+ end
218
+ end
124
219
  end
@@ -3,5 +3,5 @@
3
3
  # Discordrb and all its functionality, in this case only the version.
4
4
  module Discordrb
5
5
  # The current version of discordrb.
6
- VERSION = '3.3.0'.freeze
6
+ VERSION = '3.4.0'
7
7
  end
@@ -30,6 +30,7 @@ module Discordrb::Voice
30
30
  @filter_volume = 1
31
31
 
32
32
  raise LoadError, 'Opus unavailable - voice not supported! Please install opus for voice support to work.' unless OPUS_AVAILABLE
33
+
33
34
  @opus = Opus::Encoder.new(sample_rate, frame_size, channels)
34
35
  end
35
36
 
@@ -86,10 +87,8 @@ module Discordrb::Voice
86
87
  # @param options [String] ffmpeg options to pass after the -i flag
87
88
  # @return [IO] the audio, encoded as s16le PCM
88
89
  def encode_io(io, options = '')
89
- ret_io, writer = IO.pipe
90
90
  command = "#{ffmpeg_command} -loglevel 0 -i - #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
91
- spawn(command, in: io, out: writer)
92
- ret_io
91
+ IO.popen(command, in: io)
93
92
  end
94
93
 
95
94
  private
@@ -100,6 +99,7 @@ module Discordrb::Voice
100
99
 
101
100
  def filter_volume_argument
102
101
  return '' if @filter_volume == 1
102
+
103
103
  @use_avconv ? "-vol #{(@filter_volume * 256).ceil}" : "-af volume=#{@filter_volume}"
104
104
  end
105
105
  end
@@ -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