discordrb 3.3.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +152 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  5. data/.github/pull_request_template.md +37 -0
  6. data/.github/workflows/codeql.yml +65 -0
  7. data/.markdownlint.json +4 -0
  8. data/.rubocop.yml +39 -36
  9. data/CHANGELOG.md +874 -552
  10. data/Gemfile +2 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +80 -86
  13. data/Rakefile +2 -0
  14. data/bin/console +1 -0
  15. data/discordrb-webhooks.gemspec +9 -6
  16. data/discordrb.gemspec +21 -18
  17. data/lib/discordrb/allowed_mentions.rb +36 -0
  18. data/lib/discordrb/api/application.rb +202 -0
  19. data/lib/discordrb/api/channel.rb +236 -47
  20. data/lib/discordrb/api/interaction.rb +54 -0
  21. data/lib/discordrb/api/invite.rb +5 -5
  22. data/lib/discordrb/api/server.rb +94 -66
  23. data/lib/discordrb/api/user.rb +17 -11
  24. data/lib/discordrb/api/webhook.rb +63 -6
  25. data/lib/discordrb/api.rb +55 -16
  26. data/lib/discordrb/await.rb +0 -1
  27. data/lib/discordrb/bot.rb +480 -93
  28. data/lib/discordrb/cache.rb +31 -24
  29. data/lib/discordrb/colour_rgb.rb +43 -0
  30. data/lib/discordrb/commands/command_bot.rb +35 -12
  31. data/lib/discordrb/commands/container.rb +21 -24
  32. data/lib/discordrb/commands/parser.rb +20 -20
  33. data/lib/discordrb/commands/rate_limiter.rb +4 -3
  34. data/lib/discordrb/container.rb +209 -20
  35. data/lib/discordrb/data/activity.rb +271 -0
  36. data/lib/discordrb/data/application.rb +50 -0
  37. data/lib/discordrb/data/attachment.rb +71 -0
  38. data/lib/discordrb/data/audit_logs.rb +345 -0
  39. data/lib/discordrb/data/channel.rb +993 -0
  40. data/lib/discordrb/data/component.rb +229 -0
  41. data/lib/discordrb/data/embed.rb +251 -0
  42. data/lib/discordrb/data/emoji.rb +82 -0
  43. data/lib/discordrb/data/integration.rb +122 -0
  44. data/lib/discordrb/data/interaction.rb +800 -0
  45. data/lib/discordrb/data/invite.rb +137 -0
  46. data/lib/discordrb/data/member.rb +372 -0
  47. data/lib/discordrb/data/message.rb +414 -0
  48. data/lib/discordrb/data/overwrite.rb +108 -0
  49. data/lib/discordrb/data/profile.rb +91 -0
  50. data/lib/discordrb/data/reaction.rb +33 -0
  51. data/lib/discordrb/data/recipient.rb +34 -0
  52. data/lib/discordrb/data/role.rb +248 -0
  53. data/lib/discordrb/data/server.rb +1004 -0
  54. data/lib/discordrb/data/user.rb +264 -0
  55. data/lib/discordrb/data/voice_region.rb +45 -0
  56. data/lib/discordrb/data/voice_state.rb +41 -0
  57. data/lib/discordrb/data/webhook.rb +238 -0
  58. data/lib/discordrb/data.rb +28 -4180
  59. data/lib/discordrb/errors.rb +46 -4
  60. data/lib/discordrb/events/bans.rb +7 -5
  61. data/lib/discordrb/events/channels.rb +3 -1
  62. data/lib/discordrb/events/guilds.rb +16 -9
  63. data/lib/discordrb/events/interactions.rb +482 -0
  64. data/lib/discordrb/events/invites.rb +125 -0
  65. data/lib/discordrb/events/members.rb +6 -2
  66. data/lib/discordrb/events/message.rb +72 -27
  67. data/lib/discordrb/events/presence.rb +35 -18
  68. data/lib/discordrb/events/raw.rb +1 -3
  69. data/lib/discordrb/events/reactions.rb +49 -4
  70. data/lib/discordrb/events/threads.rb +96 -0
  71. data/lib/discordrb/events/typing.rb +6 -4
  72. data/lib/discordrb/events/voice_server_update.rb +47 -0
  73. data/lib/discordrb/events/voice_state_update.rb +15 -10
  74. data/lib/discordrb/events/webhooks.rb +9 -6
  75. data/lib/discordrb/gateway.rb +99 -71
  76. data/lib/discordrb/id_object.rb +39 -0
  77. data/lib/discordrb/light/integrations.rb +1 -1
  78. data/lib/discordrb/light/light_bot.rb +1 -1
  79. data/lib/discordrb/logger.rb +4 -4
  80. data/lib/discordrb/paginator.rb +57 -0
  81. data/lib/discordrb/permissions.rb +159 -39
  82. data/lib/discordrb/version.rb +1 -1
  83. data/lib/discordrb/voice/encoder.rb +16 -7
  84. data/lib/discordrb/voice/network.rb +99 -47
  85. data/lib/discordrb/voice/sodium.rb +98 -0
  86. data/lib/discordrb/voice/voice_bot.rb +33 -25
  87. data/lib/discordrb/webhooks.rb +2 -0
  88. data/lib/discordrb.rb +107 -1
  89. metadata +126 -54
  90. data/.codeclimate.yml +0 -16
  91. data/.travis.yml +0 -33
  92. data/bin/travis_build_docs.sh +0 -17
  93. /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
@@ -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,44 +4,54 @@ 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
- 0 => :create_instant_invite, # 1
11
- 1 => :kick_members, # 2
12
- 2 => :ban_members, # 4
13
- 3 => :administrator, # 8
14
- 4 => :manage_channels, # 16
15
- 5 => :manage_server, # 32
16
- 6 => :add_reactions, # 64
17
- 7 => :view_audit_log, # 128
18
- 8 => :priority_speaker, # 256
19
- # 9 # 512
20
- 10 => :read_messages, # 1024
21
- 11 => :send_messages, # 2048
22
- 12 => :send_tts_messages, # 4096
23
- 13 => :manage_messages, # 8192
24
- 14 => :embed_links, # 16384
25
- 15 => :attach_files, # 32768
26
- 16 => :read_message_history, # 65536
27
- 17 => :mention_everyone, # 131072
28
- 18 => :use_external_emoji, # 262144
29
- # 19 # 524288
30
- 20 => :connect, # 1048576
31
- 21 => :speak, # 2097152
32
- 22 => :mute_members, # 4194304
33
- 23 => :deafen_members, # 8388608
34
- 24 => :move_members, # 16777216
35
- 25 => :use_voice_activity, # 33554432
36
- 26 => :change_nickname, # 67108864
37
- 27 => :manage_nicknames, # 134217728
38
- 28 => :manage_roles, # 268435456, also Manage Permissions
39
- 29 => :manage_webhooks, # 536870912
40
- 30 => :manage_emojis # 1073741824
9
+ 0 => :create_instant_invite, # 1
10
+ 1 => :kick_members, # 2
11
+ 2 => :ban_members, # 4
12
+ 3 => :administrator, # 8
13
+ 4 => :manage_channels, # 16
14
+ 5 => :manage_server, # 32
15
+ 6 => :add_reactions, # 64
16
+ 7 => :view_audit_log, # 128
17
+ 8 => :priority_speaker, # 256
18
+ 9 => :stream, # 512
19
+ 10 => :read_messages, # 1024
20
+ 11 => :send_messages, # 2048
21
+ 12 => :send_tts_messages, # 4096
22
+ 13 => :manage_messages, # 8192
23
+ 14 => :embed_links, # 16384
24
+ 15 => :attach_files, # 32768
25
+ 16 => :read_message_history, # 65536
26
+ 17 => :mention_everyone, # 131072
27
+ 18 => :use_external_emoji, # 262144
28
+ 19 => :view_server_insights, # 524288
29
+ 20 => :connect, # 1048576
30
+ 21 => :speak, # 2097152
31
+ 22 => :mute_members, # 4194304
32
+ 23 => :deafen_members, # 8388608
33
+ 24 => :move_members, # 16777216
34
+ 25 => :use_voice_activity, # 33554432
35
+ 26 => :change_nickname, # 67108864
36
+ 27 => :manage_nicknames, # 134217728
37
+ 28 => :manage_roles, # 268435456, also Manage Permissions
38
+ 29 => :manage_webhooks, # 536870912
39
+ 30 => :manage_emojis, # 1073741824, also Manage Stickers
40
+ 31 => :use_slash_commands, # 2147483648
41
+ 32 => :request_to_speak, # 4294967296
42
+ 33 => :manage_events, # 8589934592
43
+ 34 => :manage_threads, # 17179869184
44
+ 35 => :use_public_threads, # 34359738368
45
+ 36 => :use_private_threads, # 68719476736
46
+ 37 => :use_external_stickers, # 137438953472
47
+ 38 => :send_messages_in_threads, # 274877906944
48
+ 39 => :use_embedded_activities, # 549755813888
49
+ 40 => :moderate_members # 1099511627776
41
50
  }.freeze
42
51
 
43
- Flags.each do |position, flag|
52
+ FLAGS.each do |position, flag|
44
53
  attr_reader flag
54
+
45
55
  define_method "can_#{flag}=" do |value|
46
56
  new_bits = @bits
47
57
  if value
@@ -49,7 +59,7 @@ module Discordrb
49
59
  else
50
60
  new_bits &= ~(1 << position)
51
61
  end
52
- @writer.write(new_bits) if @writer
62
+ @writer&.write(new_bits)
53
63
  @bits = new_bits
54
64
  init_vars
55
65
  end
@@ -69,7 +79,7 @@ module Discordrb
69
79
 
70
80
  # Initialize the instance variables based on the bitset.
71
81
  def init_vars
72
- Flags.each do |position, flag|
82
+ FLAGS.each do |position, flag|
73
83
  flag_set = ((@bits >> position) & 0x1) == 1
74
84
  instance_variable_set "@#{flag}", flag_set
75
85
  end
@@ -85,7 +95,7 @@ module Discordrb
85
95
  def self.bits(list)
86
96
  value = 0
87
97
 
88
- Flags.each do |position, flag|
98
+ FLAGS.each do |position, flag|
89
99
  value += 2**position if list.include? flag
90
100
  end
91
101
 
@@ -101,7 +111,7 @@ module Discordrb
101
111
  # permission.can_speak = true
102
112
  # @example Create a permissions object that could allow/deny read messages, connect, and speak by an array of symbols
103
113
  # Permissions.new [:read_messages, :connect, :speak]
104
- # @param bits [Integer, Array<Symbol>] The permission bits that should be set from the beginning, or an array of permission flag symbols
114
+ # @param bits [String, Integer, Array<Symbol>] The permission bits that should be set from the beginning, or an array of permission flag symbols
105
115
  # @param writer [RoleWriter] The writer that should be used to update data when a permission is set.
106
116
  def initialize(bits = 0, writer = nil)
107
117
  @writer = writer
@@ -109,16 +119,126 @@ module Discordrb
109
119
  @bits = if bits.is_a? Array
110
120
  self.class.bits(bits)
111
121
  else
112
- bits
122
+ bits.to_i
113
123
  end
114
124
 
115
125
  init_vars
116
126
  end
117
127
 
128
+ # Return an array of permission flag symbols for this class's permissions
129
+ # @example Get the permissions for the bits "9"
130
+ # permissions = Permissions.new(9)
131
+ # permissions.defined_permissions #=> [:create_instant_invite, :administrator]
132
+ # @return [Array<Symbol>] the permissions
133
+ def defined_permissions
134
+ FLAGS.filter_map { |value, name| (@bits & (1 << value)).positive? ? name : nil }
135
+ end
136
+
118
137
  # Comparison based on permission bits
119
138
  def ==(other)
120
139
  false unless other.is_a? Discordrb::Permissions
121
140
  bits == other.bits
122
141
  end
123
142
  end
143
+
144
+ # Mixin to calculate resulting permissions from overrides etc.
145
+ module PermissionCalculator
146
+ # Checks whether this user can do the particular action, regardless of whether it has the permission defined,
147
+ # through for example being the server owner or having the Manage Roles permission
148
+ # @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
149
+ # @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
150
+ # @example Check if the bot can send messages to a specific channel in a server.
151
+ # bot_profile = bot.profile.on(event.server)
152
+ # can_send_messages = bot_profile.permission?(:send_messages, channel)
153
+ # @return [true, false] whether or not this user has the permission.
154
+ def permission?(action, channel = nil)
155
+ # If the member is the server owner, it irrevocably has all permissions.
156
+ return true if owner?
157
+
158
+ # First, check whether the user has Manage Roles defined.
159
+ # (Coincidentally, Manage Permissions is the same permission as Manage Roles, and a
160
+ # Manage Permissions deny overwrite will override Manage Roles, so we can just check for
161
+ # Manage Roles once and call it a day.)
162
+ return true if defined_permission?(:administrator, channel)
163
+
164
+ # Otherwise, defer to defined_permission
165
+ defined_permission?(action, channel)
166
+ end
167
+
168
+ # Checks whether this user has a particular permission defined (i.e. not implicit, through for example
169
+ # Manage Roles)
170
+ # @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
171
+ # @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
172
+ # @example Check if a member has the Manage Channels permission defined in the server.
173
+ # has_manage_channels = member.defined_permission?(:manage_channels)
174
+ # @return [true, false] whether or not this user has the permission defined.
175
+ def defined_permission?(action, channel = nil)
176
+ # For slash commands we may not have access to the server or role
177
+ # permissions. In this case we use the permissions given to us by the
178
+ # interaction. If attempting to check against a specific channel the check
179
+ # is skipped.
180
+ return @permissions.__send__(action) if @permissions && channel.nil?
181
+
182
+ # Get the permission the user's roles have
183
+ role_permission = defined_role_permission?(action, channel)
184
+
185
+ # Once we have checked the role permission, we have to check the channel overrides for the
186
+ # specific user
187
+ user_specific_override = permission_overwrite(action, channel, id) # Use the ID reader as members have no ID instance variable
188
+
189
+ # Merge the two permissions - if an override is defined, it has to be allow, otherwise we only care about the role
190
+ return role_permission unless user_specific_override
191
+
192
+ user_specific_override == :allow
193
+ end
194
+
195
+ # Define methods for querying permissions
196
+ Discordrb::Permissions::FLAGS.each_value do |flag|
197
+ define_method "can_#{flag}?" do |channel = nil|
198
+ permission? flag, channel
199
+ end
200
+ end
201
+
202
+ alias_method :can_administrate?, :can_administrator?
203
+
204
+ private
205
+
206
+ def defined_role_permission?(action, channel)
207
+ roles_to_check = [@server.everyone_role] + roles
208
+
209
+ # For each role, check if
210
+ # (1) the channel explicitly allows or permits an action for the role and
211
+ # (2) if the user is allowed to do the action if the channel doesn't specify
212
+ roles_to_check.sort_by(&:position).reduce(false) do |can_act, role|
213
+ # Get the override defined for the role on the channel
214
+ channel_allow = permission_overwrite(action, channel, role.id)
215
+ if channel_allow
216
+ # If the channel has an override, check whether it is an allow - if yes,
217
+ # the user can act, if not, it can't
218
+ break true if channel_allow == :allow
219
+
220
+ false
221
+ else
222
+ # Otherwise defer to the role
223
+ role.permissions.instance_variable_get("@#{action}") || can_act
224
+ end
225
+ end
226
+ end
227
+
228
+ def permission_overwrite(action, channel, id)
229
+ # If no overwrites are defined, or no channel is set, no overwrite will be present
230
+ return nil unless channel && channel.permission_overwrites[id]
231
+
232
+ # Otherwise, check the allow and deny objects
233
+ allow = channel.permission_overwrites[id].allow
234
+ deny = channel.permission_overwrites[id].deny
235
+ if allow.instance_variable_get("@#{action}")
236
+ :allow
237
+ elsif deny.instance_variable_get("@#{action}")
238
+ :deny
239
+ end
240
+
241
+ # If there's no variable defined, nil will implicitly be returned
242
+ end
243
+ end
124
244
  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.5.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
 
@@ -76,7 +77,7 @@ module Discordrb::Voice
76
77
  # @param options [String] ffmpeg options to pass after the -i flag
77
78
  # @return [IO] the audio, encoded as s16le PCM
78
79
  def encode_file(file, options = '')
79
- command = "#{ffmpeg_command} -loglevel 0 -i \"#{file}\" #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
80
+ command = ffmpeg_command(input: file, options: options)
80
81
  IO.popen(command)
81
82
  end
82
83
 
@@ -86,20 +87,28 @@ 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
- 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
90
+ command = ffmpeg_command(options: options)
91
+ IO.popen(command, in: io)
93
92
  end
94
93
 
95
94
  private
96
95
 
97
- def ffmpeg_command
98
- @use_avconv ? 'avconv' : 'ffmpeg'
96
+ def ffmpeg_command(input: '-', options: null)
97
+ [
98
+ @use_avconv ? 'avconv' : 'ffmpeg',
99
+ '-loglevel', '0',
100
+ '-i', input,
101
+ '-f', 's16le',
102
+ '-ar', '48000',
103
+ '-ac', '2',
104
+ 'pipe:1',
105
+ filter_volume_argument
106
+ ].concat(options.split).reject { |segment| segment.nil? || segment == '' }
99
107
  end
100
108
 
101
109
  def filter_volume_argument
102
110
  return '' if @filter_volume == 1
111
+
103
112
  @use_avconv ? "-vol #{(@filter_volume * 256).ceil}" : "-af volume=#{@filter_volume}"
104
113
  end
105
114
  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
46
+
47
+ # @!visibility private
48
+ attr_writer :mode
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
@@ -61,9 +69,9 @@ module Discordrb::Voice
61
69
  # @return [Array(String, Integer)] the IP and port received from the discovery reply.
62
70
  def receive_discovery_reply
63
71
  # Wait for a UDP message
64
- message = @socket.recv(70)
65
- ip = message[4..-3].delete("\0")
66
- port = message[-2..-1].to_i
72
+ message = @socket.recv(74)
73
+ ip = message[8..-3].delete("\0")
74
+ port = message[-2..].unpack1('n')
67
75
  [ip, port]
68
76
  end
69
77
 
@@ -76,40 +84,81 @@ 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)
89
+
90
+ data = header + buf
81
91
 
82
- send_packet(header + buf)
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
86
99
  # be received using {#receive_discovery_reply}
87
100
  def send_discovery
88
- discovery_packet = [@ssrc].pack('N')
101
+ # Create empty packet
102
+ discovery_packet = ''
103
+
104
+ # Add Type request (0x1 = request, 0x2 = response)
105
+ discovery_packet += [0x1].pack('n')
106
+
107
+ # Add Length (excluding Type and itself = 70)
108
+ discovery_packet += [70].pack('n')
89
109
 
90
- # Add 66 zeroes so the packet is 70 bytes long
110
+ # Add SSRC
111
+ discovery_packet += [@ssrc].pack('N')
112
+
113
+ # Add 66 zeroes so the packet is 74 bytes long
91
114
  discovery_packet += "\0" * 66
115
+
92
116
  send_packet(discovery_packet)
93
117
  end
94
118
 
95
119
  private
96
120
 
97
- # Encrypts audio data using RbNaCl
98
- # @param header [String] The header of the packet, to be used as the nonce
121
+ # Encrypts audio data using libsodium
99
122
  # @param buf [String] The encoded audio data to be encrypted
123
+ # @param nonce [String] The nonce to be used to encrypt the data
100
124
  # @return [String] the audio data, encrypted
101
- def encrypt_audio(header, buf)
125
+ def encrypt_audio(buf, nonce)
102
126
  raise 'No secret key found, despite encryption being enabled!' unless @secret_key
103
- box = RbNaCl::SecretBox.new(@secret_key)
104
127
 
105
- # The nonce is the header of the voice packet with 12 null bytes appended
106
- nonce = header + ([0] * 12).pack('C*')
128
+ secret_box = Discordrb::Voice::SecretBox.new(@secret_key)
107
129
 
108
- box.encrypt(nonce, buf)
130
+ # Nonces must be 24 bytes in length. We right pad with null bytes for poly1305 and poly1305_lite
131
+ secret_box.box(nonce.ljust(24, "\0"), buf)
109
132
  end
110
133
 
111
134
  def send_packet(packet)
112
- @socket.send(packet, 0, @endpoint, @port)
135
+ @socket.send(packet, 0, @ip, @port)
136
+ end
137
+
138
+ # @param header [String] The header of the packet, to be used as the nonce
139
+ # @return [String]
140
+ # @note
141
+ # The nonce generated depends on the encryption mode.
142
+ # In xsalsa20_poly1305 the nonce is the header plus twelve null bytes for padding.
143
+ # In xsalsa20_poly1305_suffix, the nonce is 24 random bytes
144
+ # In xsalsa20_poly1305_lite, the nonce is an incremental 4 byte int.
145
+ def generate_nonce(header)
146
+ case @mode
147
+ when 'xsalsa20_poly1305'
148
+ header
149
+ when 'xsalsa20_poly1305_suffix'
150
+ Random.urandom(24)
151
+ when 'xsalsa20_poly1305_lite'
152
+ case @lite_nonce
153
+ when nil, 0xff_ff_ff_ff
154
+ @lite_nonce = 0
155
+ else
156
+ @lite_nonce += 1
157
+ end
158
+ [@lite_nonce].pack('N')
159
+ else
160
+ raise "`#{@mode}' is not a supported encryption mode"
161
+ end
113
162
  end
114
163
  end
115
164
 
@@ -117,6 +166,9 @@ module Discordrb::Voice
117
166
  # used to manage general data about the connection, such as sending the speaking packet, which determines the green
118
167
  # circle around users on Discord, and obtaining UDP connection info.
119
168
  class VoiceWS
169
+ # The version of the voice gateway that's supposed to be used.
170
+ VOICE_GATEWAY_VERSION = 4
171
+
120
172
  # @return [VoiceUDP] the UDP voice connection over which the actual audio data is sent.
121
173
  attr_reader :udp
122
174
 
@@ -127,14 +179,14 @@ module Discordrb::Voice
127
179
  # @param session [String] The voice session ID Discord sends over the regular websocket
128
180
  # @param endpoint [String] The endpoint URL to connect to
129
181
  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
182
+ raise 'libsodium is unavailable - unable to create voice bot! Please read https://github.com/shardlab/discordrb/wiki/Installing-libsodium' unless LIBSODIUM_AVAILABLE
131
183
 
132
184
  @channel = channel
133
185
  @bot = bot
134
186
  @token = token
135
187
  @session = session
136
188
 
137
- @endpoint = endpoint.gsub(':80', '')
189
+ @endpoint = endpoint.split(':').first
138
190
 
139
191
  @udp = VoiceUDP.new
140
192
  end
@@ -181,12 +233,12 @@ module Discordrb::Voice
181
233
 
182
234
  @client.send({
183
235
  op: 3,
184
- d: nil
236
+ d: millis
185
237
  }.to_json)
186
238
  end
187
239
 
188
240
  # 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
241
+ # @param value [true, false, Integer] Whether or not the bot should be speaking, can also be a bitmask denoting audio type.
190
242
  def send_speaking(value)
191
243
  @bot.debug("Speaking: #{value}")
192
244
  @client.send({
@@ -218,18 +270,23 @@ module Discordrb::Voice
218
270
  # Opcode 2 contains data to initialize the UDP connection
219
271
  @ws_data = packet['d']
220
272
 
221
- @heartbeat_interval = @ws_data['heartbeat_interval']
222
273
  @ssrc = @ws_data['ssrc']
223
274
  @port = @ws_data['port']
224
- @udp_mode = mode
225
275
 
226
- @udp.connect(@endpoint, @port, @ssrc)
276
+ @udp_mode = (ENCRYPTION_MODES & @ws_data['modes']).first
277
+
278
+ @udp.connect(@ws_data['ip'], @port, @ssrc)
227
279
  @udp.send_discovery
228
280
  when 4
229
281
  # Opcode 4 sends the secret key used for encryption
230
282
  @ws_data = packet['d']
283
+
231
284
  @ready = true
232
285
  @udp.secret_key = @ws_data['secret_key'].pack('C*')
286
+ @udp.mode = @ws_data['mode']
287
+ when 8
288
+ # Opcode 8 contains the heartbeat interval.
289
+ @heartbeat_interval = packet['d']['heartbeat_interval']
233
290
  end
234
291
  end
235
292
 
@@ -276,11 +333,6 @@ module Discordrb::Voice
276
333
 
277
334
  private
278
335
 
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
336
  def heartbeat_loop
285
337
  @heartbeat_running = true
286
338
  while @heartbeat_running
@@ -295,7 +347,7 @@ module Discordrb::Voice
295
347
  end
296
348
 
297
349
  def init_ws
298
- host = "wss://#{@endpoint}:443"
350
+ host = "wss://#{@endpoint}:443/?v=#{VOICE_GATEWAY_VERSION}"
299
351
  @bot.debug("Connecting VWS to host: #{host}")
300
352
 
301
353
  # Connect the WS