discordrb 3.3.0 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of discordrb might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.circleci/config.yml +126 -0
- data/{CONTRIBUTING.md → .github/CONTRIBUTING.md} +0 -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 +472 -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 +17 -17
- data/lib/discordrb.rb +73 -0
- data/lib/discordrb/allowed_mentions.rb +36 -0
- data/lib/discordrb/api.rb +40 -15
- data/lib/discordrb/api/channel.rb +57 -39
- data/lib/discordrb/api/invite.rb +3 -3
- data/lib/discordrb/api/server.rb +55 -50
- data/lib/discordrb/api/user.rb +8 -8
- data/lib/discordrb/api/webhook.rb +6 -6
- data/lib/discordrb/await.rb +0 -1
- data/lib/discordrb/bot.rb +164 -72
- data/lib/discordrb/cache.rb +4 -2
- data/lib/discordrb/colour_rgb.rb +43 -0
- data/lib/discordrb/commands/command_bot.rb +22 -6
- 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.rb +25 -4180
- data/lib/discordrb/data/activity.rb +264 -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/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 +3 -3
- 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
- metadata +93 -55
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Discordrb
|
4
|
+
# Mixin for objects that have IDs
|
5
|
+
module IDObject
|
6
|
+
# @return [Integer] the ID which uniquely identifies this object across Discord.
|
7
|
+
attr_reader :id
|
8
|
+
alias_method :resolve_id, :id
|
9
|
+
alias_method :hash, :id
|
10
|
+
|
11
|
+
# ID based comparison
|
12
|
+
def ==(other)
|
13
|
+
Discordrb.id_compare(@id, other)
|
14
|
+
end
|
15
|
+
|
16
|
+
alias_method :eql?, :==
|
17
|
+
|
18
|
+
# Estimates the time this object was generated on based on the beginning of the ID. This is fairly accurate but
|
19
|
+
# shouldn't be relied on as Discord might change its algorithm at any time
|
20
|
+
# @return [Time] when this object was created at
|
21
|
+
def creation_time
|
22
|
+
# Milliseconds
|
23
|
+
ms = (@id >> 22) + DISCORD_EPOCH
|
24
|
+
Time.at(ms / 1000.0)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Creates an artificial snowflake at the given point in time. Useful for comparing against.
|
28
|
+
# @param time [Time] The time the snowflake should represent.
|
29
|
+
# @return [Integer] a snowflake with the timestamp data as the given time
|
30
|
+
def self.synthesise(time)
|
31
|
+
ms = (time.to_f * 1000).to_i
|
32
|
+
(ms - DISCORD_EPOCH) << 22
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
alias_method :synthesize, :synthesise
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -49,7 +49,7 @@ module Discordrb::Light
|
|
49
49
|
# Twitch account connection of the server owner).
|
50
50
|
attr_reader :server_connection
|
51
51
|
|
52
|
-
# @return [Connection] the connection integrated with the server (i.
|
52
|
+
# @return [Connection] the connection integrated with the server (i.e. your connection)
|
53
53
|
attr_reader :integrated_connection
|
54
54
|
|
55
55
|
# @!visibility private
|
@@ -6,7 +6,7 @@ require 'discordrb/api/user'
|
|
6
6
|
require 'discordrb/light/data'
|
7
7
|
require 'discordrb/light/integrations'
|
8
8
|
|
9
|
-
# This module contains classes to allow connections to bots without a connection to the gateway socket, i.
|
9
|
+
# This module contains classes to allow connections to bots without a connection to the gateway socket, i.e. bots
|
10
10
|
# that only use the REST part of the API.
|
11
11
|
module Discordrb::Light
|
12
12
|
# A bot that only uses the REST part of the API. Hierarchically unrelated to the regular {Discordrb::Bot}. Useful to
|
data/lib/discordrb/logger.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Discordrb
|
4
4
|
# The format log timestamps should be in, in strftime format
|
5
|
-
LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'
|
5
|
+
LOG_TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%L'
|
6
6
|
|
7
7
|
# Logs debug messages
|
8
8
|
class Logger
|
@@ -18,7 +18,7 @@ module Discordrb
|
|
18
18
|
# Creates a new logger.
|
19
19
|
# @param fancy [true, false] Whether this logger uses fancy mode (ANSI escape codes to make the output colourful)
|
20
20
|
# @param streams [Array<IO>, Array<#puts & #flush>] the streams the logger should write to.
|
21
|
-
def initialize(fancy = false, streams = [
|
21
|
+
def initialize(fancy = false, streams = [$stdout])
|
22
22
|
@fancy = fancy
|
23
23
|
self.mode = :normal
|
24
24
|
|
@@ -38,10 +38,10 @@ module Discordrb
|
|
38
38
|
}.freeze
|
39
39
|
|
40
40
|
# The ANSI format code that resets formatting
|
41
|
-
FORMAT_RESET = "\u001B[0m"
|
41
|
+
FORMAT_RESET = "\u001B[0m"
|
42
42
|
|
43
43
|
# The ANSI format code that makes something bold
|
44
|
-
FORMAT_BOLD = "\u001B[1m"
|
44
|
+
FORMAT_BOLD = "\u001B[1m"
|
45
45
|
|
46
46
|
MODES.each do |mode, hash|
|
47
47
|
define_method(mode) do |message|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Discordrb
|
4
|
+
# Utility class for wrapping paginated endpoints. It is [Enumerable](https://ruby-doc.org/core-2.5.1/Enumerable.html),
|
5
|
+
# similar to an `Array`, so most of the same methods can be used to filter the results of the request
|
6
|
+
# that it wraps. If you simply want an array of all of the results, `#to_a` can be called.
|
7
|
+
class Paginator
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# Creates a new {Paginator}
|
11
|
+
# @param limit [Integer] the maximum number of items to request before stopping
|
12
|
+
# @param direction [:up, :down] the order in which results are returned in
|
13
|
+
# @yield [Array, nil] the last page of results, or nil if this is the first iteration.
|
14
|
+
# This should be used to request the next page of results.
|
15
|
+
# @yieldreturn [Array] the next page of results
|
16
|
+
def initialize(limit, direction, &block)
|
17
|
+
@count = 0
|
18
|
+
@limit = limit
|
19
|
+
@direction = direction
|
20
|
+
@block = block
|
21
|
+
end
|
22
|
+
|
23
|
+
# Yields every item produced by the wrapped request, until it returns
|
24
|
+
# no more results or the configured `limit` is reached.
|
25
|
+
def each
|
26
|
+
last_page = nil
|
27
|
+
until limit_check
|
28
|
+
page = @block.call(last_page)
|
29
|
+
return if page.empty?
|
30
|
+
|
31
|
+
enumerator = case @direction
|
32
|
+
when :down
|
33
|
+
page.each
|
34
|
+
when :up
|
35
|
+
page.reverse_each
|
36
|
+
end
|
37
|
+
|
38
|
+
enumerator.each do |item|
|
39
|
+
yield item
|
40
|
+
@count += 1
|
41
|
+
break if limit_check
|
42
|
+
end
|
43
|
+
|
44
|
+
last_page = page
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Whether the paginator limit has been exceeded
|
51
|
+
def limit_check
|
52
|
+
return false if @limit.nil?
|
53
|
+
|
54
|
+
@count >= @limit
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -4,8 +4,7 @@ module Discordrb
|
|
4
4
|
# List of permissions Discord uses
|
5
5
|
class Permissions
|
6
6
|
# This hash maps bit positions to logical permissions.
|
7
|
-
|
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
|
|
@@ -86,10 +87,8 @@ module Discordrb::Voice
|
|
86
87
|
# @param options [String] ffmpeg options to pass after the -i flag
|
87
88
|
# @return [IO] the audio, encoded as s16le PCM
|
88
89
|
def encode_io(io, options = '')
|
89
|
-
ret_io, writer = IO.pipe
|
90
90
|
command = "#{ffmpeg_command} -loglevel 0 -i - #{options} -f s16le -ar 48000 -ac 2 #{filter_volume_argument} pipe:1"
|
91
|
-
|
92
|
-
ret_io
|
91
|
+
IO.popen(command, in: io)
|
93
92
|
end
|
94
93
|
|
95
94
|
private
|
@@ -100,6 +99,7 @@ module Discordrb::Voice
|
|
100
99
|
|
101
100
|
def filter_volume_argument
|
102
101
|
return '' if @filter_volume == 1
|
102
|
+
|
103
103
|
@use_avconv ? "-vol #{(@filter_volume * 256).ceil}" : "-af volume=#{@filter_volume}"
|
104
104
|
end
|
105
105
|
end
|
@@ -1,58 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'websocket-client-simple'
|
4
|
-
require 'resolv'
|
5
4
|
require 'socket'
|
6
5
|
require 'json'
|
7
6
|
|
8
7
|
require 'discordrb/websocket'
|
9
8
|
|
10
9
|
begin
|
11
|
-
|
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
|