discordrb 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +152 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- data/.github/pull_request_template.md +37 -0
- data/.github/workflows/codeql.yml +65 -0
- data/.markdownlint.json +4 -0
- data/.rubocop.yml +39 -36
- data/CHANGELOG.md +874 -552
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +80 -86
- data/Rakefile +2 -0
- data/bin/console +1 -0
- data/discordrb-webhooks.gemspec +9 -6
- data/discordrb.gemspec +21 -18
- data/lib/discordrb/allowed_mentions.rb +36 -0
- data/lib/discordrb/api/application.rb +202 -0
- data/lib/discordrb/api/channel.rb +236 -47
- data/lib/discordrb/api/interaction.rb +54 -0
- data/lib/discordrb/api/invite.rb +5 -5
- data/lib/discordrb/api/server.rb +94 -66
- data/lib/discordrb/api/user.rb +17 -11
- data/lib/discordrb/api/webhook.rb +63 -6
- data/lib/discordrb/api.rb +55 -16
- data/lib/discordrb/await.rb +0 -1
- data/lib/discordrb/bot.rb +480 -93
- data/lib/discordrb/cache.rb +31 -24
- data/lib/discordrb/colour_rgb.rb +43 -0
- data/lib/discordrb/commands/command_bot.rb +35 -12
- data/lib/discordrb/commands/container.rb +21 -24
- data/lib/discordrb/commands/parser.rb +20 -20
- data/lib/discordrb/commands/rate_limiter.rb +4 -3
- data/lib/discordrb/container.rb +209 -20
- data/lib/discordrb/data/activity.rb +271 -0
- data/lib/discordrb/data/application.rb +50 -0
- data/lib/discordrb/data/attachment.rb +71 -0
- data/lib/discordrb/data/audit_logs.rb +345 -0
- data/lib/discordrb/data/channel.rb +993 -0
- data/lib/discordrb/data/component.rb +229 -0
- data/lib/discordrb/data/embed.rb +251 -0
- data/lib/discordrb/data/emoji.rb +82 -0
- data/lib/discordrb/data/integration.rb +122 -0
- data/lib/discordrb/data/interaction.rb +800 -0
- data/lib/discordrb/data/invite.rb +137 -0
- data/lib/discordrb/data/member.rb +372 -0
- data/lib/discordrb/data/message.rb +414 -0
- data/lib/discordrb/data/overwrite.rb +108 -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 +248 -0
- data/lib/discordrb/data/server.rb +1004 -0
- data/lib/discordrb/data/user.rb +264 -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 +238 -0
- data/lib/discordrb/data.rb +28 -4180
- data/lib/discordrb/errors.rb +46 -4
- data/lib/discordrb/events/bans.rb +7 -5
- data/lib/discordrb/events/channels.rb +3 -1
- data/lib/discordrb/events/guilds.rb +16 -9
- data/lib/discordrb/events/interactions.rb +482 -0
- data/lib/discordrb/events/invites.rb +125 -0
- data/lib/discordrb/events/members.rb +6 -2
- data/lib/discordrb/events/message.rb +72 -27
- data/lib/discordrb/events/presence.rb +35 -18
- data/lib/discordrb/events/raw.rb +1 -3
- data/lib/discordrb/events/reactions.rb +49 -4
- data/lib/discordrb/events/threads.rb +96 -0
- data/lib/discordrb/events/typing.rb +6 -4
- data/lib/discordrb/events/voice_server_update.rb +47 -0
- data/lib/discordrb/events/voice_state_update.rb +15 -10
- data/lib/discordrb/events/webhooks.rb +9 -6
- data/lib/discordrb/gateway.rb +99 -71
- data/lib/discordrb/id_object.rb +39 -0
- data/lib/discordrb/light/integrations.rb +1 -1
- data/lib/discordrb/light/light_bot.rb +1 -1
- data/lib/discordrb/logger.rb +4 -4
- data/lib/discordrb/paginator.rb +57 -0
- data/lib/discordrb/permissions.rb +159 -39
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/encoder.rb +16 -7
- data/lib/discordrb/voice/network.rb +99 -47
- data/lib/discordrb/voice/sodium.rb +98 -0
- data/lib/discordrb/voice/voice_bot.rb +33 -25
- data/lib/discordrb/webhooks.rb +2 -0
- data/lib/discordrb.rb +107 -1
- metadata +126 -54
- data/.codeclimate.yml +0 -16
- data/.travis.yml +0 -33
- data/bin/travis_build_docs.sh +0 -17
- /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
|
-
|
8
|
-
Flags = {
|
7
|
+
FLAGS = {
|
9
8
|
# Bit => Permission # Value
|
10
|
-
0 => :create_instant_invite,
|
11
|
-
1 => :kick_members,
|
12
|
-
2 => :ban_members,
|
13
|
-
3 => :administrator,
|
14
|
-
4 => :manage_channels,
|
15
|
-
5 => :manage_server,
|
16
|
-
6 => :add_reactions,
|
17
|
-
7 => :view_audit_log,
|
18
|
-
8 => :priority_speaker,
|
19
|
-
|
20
|
-
10 => :read_messages,
|
21
|
-
11 => :send_messages,
|
22
|
-
12 => :send_tts_messages,
|
23
|
-
13 => :manage_messages,
|
24
|
-
14 => :embed_links,
|
25
|
-
15 => :attach_files,
|
26
|
-
16 => :read_message_history,
|
27
|
-
17 => :mention_everyone,
|
28
|
-
18 => :use_external_emoji,
|
29
|
-
|
30
|
-
20 => :connect,
|
31
|
-
21 => :speak,
|
32
|
-
22 => :mute_members,
|
33
|
-
23 => :deafen_members,
|
34
|
-
24 => :move_members,
|
35
|
-
25 => :use_voice_activity,
|
36
|
-
26 => :change_nickname,
|
37
|
-
27 => :manage_nicknames,
|
38
|
-
28 => :manage_roles,
|
39
|
-
29 => :manage_webhooks,
|
40
|
-
30 => :manage_emojis
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
data/lib/discordrb/version.rb
CHANGED
@@ -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 =
|
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
|
-
|
90
|
-
command
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
@@ -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(
|
65
|
-
ip = message[
|
66
|
-
port = message[-2
|
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
|
-
|
80
|
-
buf = encrypt_audio(
|
87
|
+
nonce = generate_nonce(header)
|
88
|
+
buf = encrypt_audio(buf, nonce)
|
89
|
+
|
90
|
+
data = header + buf
|
81
91
|
|
82
|
-
|
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
|
-
|
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
|
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
|
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(
|
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
|
-
|
106
|
-
nonce = header + ([0] * 12).pack('C*')
|
128
|
+
secret_box = Discordrb::Voice::SecretBox.new(@secret_key)
|
107
129
|
|
108
|
-
|
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, @
|
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 '
|
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.
|
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:
|
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
|
-
@
|
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
|