discordrb 3.3.0 → 3.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|