discordrb 3.3.0 → 3.4.3
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 +126 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
- data/.github/pull_request_template.md +37 -0
- data/.rubocop.yml +34 -37
- data/.travis.yml +5 -6
- data/CHANGELOG.md +504 -347
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +61 -79
- data/Rakefile +2 -0
- data/bin/console +1 -0
- data/discordrb-webhooks.gemspec +6 -6
- data/discordrb.gemspec +18 -18
- data/lib/discordrb/allowed_mentions.rb +36 -0
- data/lib/discordrb/api/channel.rb +62 -39
- data/lib/discordrb/api/invite.rb +3 -3
- data/lib/discordrb/api/server.rb +57 -50
- data/lib/discordrb/api/user.rb +9 -8
- data/lib/discordrb/api/webhook.rb +6 -6
- data/lib/discordrb/api.rb +40 -15
- data/lib/discordrb/await.rb +0 -1
- data/lib/discordrb/bot.rb +175 -73
- data/lib/discordrb/cache.rb +4 -2
- data/lib/discordrb/colour_rgb.rb +43 -0
- data/lib/discordrb/commands/command_bot.rb +30 -9
- data/lib/discordrb/commands/container.rb +20 -23
- data/lib/discordrb/commands/parser.rb +18 -18
- data/lib/discordrb/commands/rate_limiter.rb +3 -2
- data/lib/discordrb/container.rb +77 -17
- data/lib/discordrb/data/activity.rb +271 -0
- data/lib/discordrb/data/application.rb +50 -0
- data/lib/discordrb/data/attachment.rb +56 -0
- data/lib/discordrb/data/audit_logs.rb +345 -0
- data/lib/discordrb/data/channel.rb +849 -0
- data/lib/discordrb/data/embed.rb +251 -0
- data/lib/discordrb/data/emoji.rb +82 -0
- data/lib/discordrb/data/integration.rb +83 -0
- data/lib/discordrb/data/invite.rb +137 -0
- data/lib/discordrb/data/member.rb +297 -0
- data/lib/discordrb/data/message.rb +334 -0
- data/lib/discordrb/data/overwrite.rb +102 -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 +191 -0
- data/lib/discordrb/data/server.rb +1002 -0
- data/lib/discordrb/data/user.rb +204 -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 +145 -0
- data/lib/discordrb/data.rb +25 -4180
- data/lib/discordrb/errors.rb +2 -1
- data/lib/discordrb/events/bans.rb +7 -5
- data/lib/discordrb/events/channels.rb +2 -0
- data/lib/discordrb/events/guilds.rb +16 -9
- data/lib/discordrb/events/invites.rb +125 -0
- data/lib/discordrb/events/members.rb +6 -2
- data/lib/discordrb/events/message.rb +69 -27
- data/lib/discordrb/events/presence.rb +14 -4
- data/lib/discordrb/events/raw.rb +1 -3
- data/lib/discordrb/events/reactions.rb +49 -3
- 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 +72 -57
- 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 +103 -8
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/encoder.rb +16 -7
- data/lib/discordrb/voice/network.rb +84 -43
- data/lib/discordrb/voice/sodium.rb +96 -0
- data/lib/discordrb/voice/voice_bot.rb +34 -26
- data/lib/discordrb.rb +73 -0
- metadata +98 -60
- /data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -0
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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 # 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
|
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
|
@@ -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].
|
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
|
-
|
80
|
-
buf = encrypt_audio(
|
87
|
+
nonce = generate_nonce(header)
|
88
|
+
buf = encrypt_audio(buf, nonce)
|
81
89
|
|
82
|
-
|
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
|
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(
|
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
|
-
|
106
|
-
nonce = header + ([0] * 12).pack('C*')
|
117
|
+
secret_box = Discordrb::Voice::SecretBox.new(@secret_key)
|
107
118
|
|
108
|
-
|
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, @
|
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 '
|
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.
|
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:
|
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
|
-
@
|
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
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Discordrb::Voice
|
4
|
+
# @!visibility private
|
5
|
+
module Sodium
|
6
|
+
extend FFI::Library
|
7
|
+
|
8
|
+
ffi_lib(['sodium', 'libsodium.so.18', 'libsodium.so.23'])
|
9
|
+
|
10
|
+
# Encryption & decryption
|
11
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305, %i[pointer pointer ulong_long pointer pointer], :int)
|
12
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_open, %i[pointer pointer ulong_long pointer pointer], :int)
|
13
|
+
|
14
|
+
# Constants
|
15
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_keybytes, [], :size_t)
|
16
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_noncebytes, [], :size_t)
|
17
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_zerobytes, [], :size_t)
|
18
|
+
attach_function(:crypto_secretbox_xsalsa20poly1305_boxzerobytes, [], :size_t)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Utility class for interacting with required `xsalsa20poly1305` functions for voice transmission
|
22
|
+
# @!visibility private
|
23
|
+
class SecretBox
|
24
|
+
# Exception raised when a key or nonce with invalid length is used
|
25
|
+
class LengthError < RuntimeError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Exception raised when encryption or decryption fails
|
29
|
+
class CryptoError < RuntimeError
|
30
|
+
end
|
31
|
+
|
32
|
+
# Required key length
|
33
|
+
KEY_LENGTH = Sodium.crypto_secretbox_xsalsa20poly1305_keybytes
|
34
|
+
|
35
|
+
# Required nonce length
|
36
|
+
NONCE_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_noncebytes
|
37
|
+
|
38
|
+
# Zero byte padding for encryption
|
39
|
+
ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_zerobytes
|
40
|
+
|
41
|
+
# Zero byte padding for decryption
|
42
|
+
BOX_ZERO_BYTES = Sodium.crypto_secretbox_xsalsa20poly1305_boxzerobytes
|
43
|
+
|
44
|
+
# @param key [String] Crypto key of length {KEY_LENGTH}
|
45
|
+
def initialize(key)
|
46
|
+
raise(LengthError, 'Key length') if key.bytesize != KEY_LENGTH
|
47
|
+
|
48
|
+
@key = key
|
49
|
+
end
|
50
|
+
|
51
|
+
# Encrypts a message using this box's key
|
52
|
+
# @param nonce [String] encryption nonce for this message
|
53
|
+
# @param message [String] message to be encrypted
|
54
|
+
def box(nonce, message)
|
55
|
+
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
56
|
+
|
57
|
+
message_padded = prepend_zeroes(ZERO_BYTES, message)
|
58
|
+
buffer = zero_string(message_padded.bytesize)
|
59
|
+
|
60
|
+
success = Sodium.crypto_secretbox_xsalsa20poly1305(buffer, message_padded, message_padded.bytesize, nonce, @key)
|
61
|
+
raise(CryptoError, "Encryption failed (#{success})") unless success.zero?
|
62
|
+
|
63
|
+
remove_zeroes(BOX_ZERO_BYTES, buffer)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Decrypts the given ciphertext using this box's key
|
67
|
+
# @param nonce [String] encryption nonce for this ciphertext
|
68
|
+
# @param ciphertext [String] ciphertext to decrypt
|
69
|
+
def open(nonce, ciphertext)
|
70
|
+
raise(LengthError, 'Nonce length') if nonce.bytesize != NONCE_BYTES
|
71
|
+
|
72
|
+
ct_padded = prepend_zeroes(BOX_ZERO_BYTES, ciphertext)
|
73
|
+
buffer = zero_string(ct_padded.bytesize)
|
74
|
+
|
75
|
+
success = Sodium.crypto_secretbox_xsalsa20poly1305_open(buffer, ct_padded, ct_padded.bytesize, nonce, @key)
|
76
|
+
raise(CryptoError, "Decryption failed (#{success})") unless success.zero?
|
77
|
+
|
78
|
+
remove_zeroes(ZERO_BYTES, buffer)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def zero_string(size)
|
84
|
+
str = "\0" * size
|
85
|
+
str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding)
|
86
|
+
end
|
87
|
+
|
88
|
+
def prepend_zeroes(size, string)
|
89
|
+
zero_string(size) + string
|
90
|
+
end
|
91
|
+
|
92
|
+
def remove_zeroes(size, string)
|
93
|
+
string.slice!(size, string.bytesize - size)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|