onyxcord 1.1.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 +7 -0
- data/.devcontainer/Dockerfile +13 -0
- data/.devcontainer/devcontainer.json +29 -0
- data/.devcontainer/postcreate.sh +4 -0
- data/.github/CONTRIBUTING.md +13 -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/ci.yml +78 -0
- data/.github/workflows/codeql.yml +65 -0
- data/.github/workflows/deploy.yml +54 -0
- data/.github/workflows/release.yml +51 -0
- data/.gitignore +16 -0
- data/.markdownlint.json +4 -0
- data/.overcommit.yml +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +129 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +0 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +305 -0
- data/Rakefile +17 -0
- data/bin/console +15 -0
- data/bin/setup +7 -0
- data/lib/onyxcord/allowed_mentions.rb +43 -0
- data/lib/onyxcord/api/application.rb +316 -0
- data/lib/onyxcord/api/channel.rb +700 -0
- data/lib/onyxcord/api/interaction.rb +67 -0
- data/lib/onyxcord/api/invite.rb +44 -0
- data/lib/onyxcord/api/server.rb +775 -0
- data/lib/onyxcord/api/user.rb +158 -0
- data/lib/onyxcord/api/webhook.rb +163 -0
- data/lib/onyxcord/api.rb +335 -0
- data/lib/onyxcord/await.rb +51 -0
- data/lib/onyxcord/bot.rb +1971 -0
- data/lib/onyxcord/cache.rb +326 -0
- data/lib/onyxcord/colour_rgb.rb +43 -0
- data/lib/onyxcord/commands/command_bot.rb +511 -0
- data/lib/onyxcord/commands/container.rb +112 -0
- data/lib/onyxcord/commands/events.rb +11 -0
- data/lib/onyxcord/commands/parser.rb +327 -0
- data/lib/onyxcord/commands/rate_limiter.rb +144 -0
- data/lib/onyxcord/configuration.rb +125 -0
- data/lib/onyxcord/container.rb +988 -0
- data/lib/onyxcord/data/activity.rb +271 -0
- data/lib/onyxcord/data/application.rb +341 -0
- data/lib/onyxcord/data/attachment.rb +91 -0
- data/lib/onyxcord/data/audit_logs.rb +438 -0
- data/lib/onyxcord/data/avatar_decoration.rb +26 -0
- data/lib/onyxcord/data/call.rb +22 -0
- data/lib/onyxcord/data/channel.rb +1355 -0
- data/lib/onyxcord/data/channel_tag.rb +69 -0
- data/lib/onyxcord/data/collectibles.rb +47 -0
- data/lib/onyxcord/data/component.rb +583 -0
- data/lib/onyxcord/data/embed.rb +258 -0
- data/lib/onyxcord/data/emoji.rb +123 -0
- data/lib/onyxcord/data/install_params.rb +24 -0
- data/lib/onyxcord/data/integration.rb +144 -0
- data/lib/onyxcord/data/interaction.rb +1141 -0
- data/lib/onyxcord/data/invite.rb +137 -0
- data/lib/onyxcord/data/member.rb +528 -0
- data/lib/onyxcord/data/message.rb +612 -0
- data/lib/onyxcord/data/message_activity.rb +41 -0
- data/lib/onyxcord/data/overwrite.rb +109 -0
- data/lib/onyxcord/data/poll.rb +365 -0
- data/lib/onyxcord/data/primary_server.rb +60 -0
- data/lib/onyxcord/data/profile.rb +79 -0
- data/lib/onyxcord/data/reaction.rb +64 -0
- data/lib/onyxcord/data/recipient.rb +34 -0
- data/lib/onyxcord/data/role.rb +449 -0
- data/lib/onyxcord/data/role_connection_data.rb +69 -0
- data/lib/onyxcord/data/role_subscription.rb +41 -0
- data/lib/onyxcord/data/scheduled_event.rb +513 -0
- data/lib/onyxcord/data/server.rb +1614 -0
- data/lib/onyxcord/data/server_preview.rb +68 -0
- data/lib/onyxcord/data/snapshot.rb +112 -0
- data/lib/onyxcord/data/team.rb +98 -0
- data/lib/onyxcord/data/timestamp.rb +69 -0
- data/lib/onyxcord/data/user.rb +324 -0
- data/lib/onyxcord/data/voice_region.rb +46 -0
- data/lib/onyxcord/data/voice_state.rb +41 -0
- data/lib/onyxcord/data/webhook.rb +238 -0
- data/lib/onyxcord/data.rb +57 -0
- data/lib/onyxcord/errors.rb +246 -0
- data/lib/onyxcord/event_executor.rb +80 -0
- data/lib/onyxcord/events/await.rb +48 -0
- data/lib/onyxcord/events/bans.rb +60 -0
- data/lib/onyxcord/events/channels.rb +225 -0
- data/lib/onyxcord/events/generic.rb +129 -0
- data/lib/onyxcord/events/guilds.rb +269 -0
- data/lib/onyxcord/events/integrations.rb +100 -0
- data/lib/onyxcord/events/interactions.rb +624 -0
- data/lib/onyxcord/events/invites.rb +127 -0
- data/lib/onyxcord/events/lifetime.rb +31 -0
- data/lib/onyxcord/events/members.rb +110 -0
- data/lib/onyxcord/events/message.rb +399 -0
- data/lib/onyxcord/events/polls.rb +118 -0
- data/lib/onyxcord/events/presence.rb +131 -0
- data/lib/onyxcord/events/raw.rb +74 -0
- data/lib/onyxcord/events/reactions.rb +218 -0
- data/lib/onyxcord/events/roles.rb +87 -0
- data/lib/onyxcord/events/scheduled_events.rb +171 -0
- data/lib/onyxcord/events/threads.rb +100 -0
- data/lib/onyxcord/events/typing.rb +73 -0
- data/lib/onyxcord/events/voice_server_update.rb +48 -0
- data/lib/onyxcord/events/voice_state_update.rb +106 -0
- data/lib/onyxcord/events/webhooks.rb +65 -0
- data/lib/onyxcord/gateway.rb +890 -0
- data/lib/onyxcord/id_object.rb +39 -0
- data/lib/onyxcord/light/data.rb +62 -0
- data/lib/onyxcord/light/integrations.rb +73 -0
- data/lib/onyxcord/light/light_bot.rb +58 -0
- data/lib/onyxcord/light.rb +8 -0
- data/lib/onyxcord/logger.rb +120 -0
- data/lib/onyxcord/message_components.rb +70 -0
- data/lib/onyxcord/paginator.rb +60 -0
- data/lib/onyxcord/permissions.rb +255 -0
- data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
- data/lib/onyxcord/rate_limiter/rest.rb +89 -0
- data/lib/onyxcord/version.rb +7 -0
- data/lib/onyxcord/voice/encoder.rb +115 -0
- data/lib/onyxcord/voice/network.rb +380 -0
- data/lib/onyxcord/voice/opcodes.rb +29 -0
- data/lib/onyxcord/voice/sodium.rb +157 -0
- data/lib/onyxcord/voice/timer.rb +19 -0
- data/lib/onyxcord/voice/voice_bot.rb +386 -0
- data/lib/onyxcord/webhooks.rb +14 -0
- data/lib/onyxcord/websocket.rb +62 -0
- data/lib/onyxcord.rb +180 -0
- data/onyxcord-webhooks.gemspec +30 -0
- data/onyxcord.gemspec +50 -0
- metadata +421 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OnyxCord
|
|
4
|
+
# List of permissions Discord uses
|
|
5
|
+
class Permissions
|
|
6
|
+
# This hash maps bit positions to logical permissions.
|
|
7
|
+
FLAGS = {
|
|
8
|
+
# Bit => Permission # Value
|
|
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
|
|
50
|
+
41 => :view_monetization_analytics, # 2199023255552
|
|
51
|
+
42 => :use_soundboard, # 4398046511104
|
|
52
|
+
43 => :create_server_expressions, # 8796093022208
|
|
53
|
+
44 => :create_scheduled_events, # 17592186044416
|
|
54
|
+
45 => :use_external_sounds, # 35184372088832
|
|
55
|
+
46 => :send_voice_messages, # 70368744177664
|
|
56
|
+
49 => :send_polls, # 562949953421312
|
|
57
|
+
50 => :use_external_apps, # 1125899906842624
|
|
58
|
+
51 => :pin_messages, # 2251799813685248
|
|
59
|
+
52 => :bypass_slowmode # 4503599627370496
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
FLAGS.each do |position, flag|
|
|
63
|
+
attr_reader flag
|
|
64
|
+
|
|
65
|
+
define_method "can_#{flag}=" do |value|
|
|
66
|
+
new_bits = @bits
|
|
67
|
+
if value
|
|
68
|
+
new_bits |= (1 << position)
|
|
69
|
+
else
|
|
70
|
+
new_bits &= ~(1 << position)
|
|
71
|
+
end
|
|
72
|
+
@writer&.write(new_bits)
|
|
73
|
+
@bits = new_bits
|
|
74
|
+
init_vars
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
alias_method :can_administrate=, :can_administrator=
|
|
79
|
+
alias_method :administrate, :administrator
|
|
80
|
+
|
|
81
|
+
attr_reader :bits
|
|
82
|
+
|
|
83
|
+
# Set the raw bitset of this permission object
|
|
84
|
+
# @param bits [Integer] A number whose binary representation is the desired bitset.
|
|
85
|
+
def bits=(bits)
|
|
86
|
+
@bits = bits
|
|
87
|
+
init_vars
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Initialize the instance variables based on the bitset.
|
|
91
|
+
def init_vars
|
|
92
|
+
FLAGS.each do |position, flag|
|
|
93
|
+
flag_set = (@bits >> position).allbits?(0x1)
|
|
94
|
+
instance_variable_set "@#{flag}", flag_set
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Return the corresponding bits for an array of permission flag symbols.
|
|
99
|
+
# This is a class method that can be used to calculate bits instead
|
|
100
|
+
# of instancing a new Permissions object.
|
|
101
|
+
# @example Get the bits for permissions that could allow/deny read messages, connect, and speak
|
|
102
|
+
# Permissions.bits [:read_messages, :connect, :speak] #=> 3146752
|
|
103
|
+
# @param list [Array<Symbol>]
|
|
104
|
+
# @return [Integer] the computed permissions integer
|
|
105
|
+
def self.bits(list)
|
|
106
|
+
value = 0
|
|
107
|
+
|
|
108
|
+
FLAGS.each do |position, flag|
|
|
109
|
+
value += 2**position if list.include? flag
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
value
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Create a new Permissions object either as a blank slate to add permissions to (for example for
|
|
116
|
+
# {Channel#define_overwrite}) or from existing bit data to read out.
|
|
117
|
+
# @example Create a permissions object that could allow/deny read messages, connect, and speak by setting flags
|
|
118
|
+
# permission = Permissions.new
|
|
119
|
+
# permission.can_read_messages = true
|
|
120
|
+
# permission.can_connect = true
|
|
121
|
+
# permission.can_speak = true
|
|
122
|
+
# @example Create a permissions object that could allow/deny read messages, connect, and speak by an array of symbols
|
|
123
|
+
# Permissions.new [:read_messages, :connect, :speak]
|
|
124
|
+
# @param bits [String, Integer, Array<Symbol>] The permission bits that should be set from the beginning, or an array of permission flag symbols
|
|
125
|
+
# @param writer [RoleWriter] The writer that should be used to update data when a permission is set.
|
|
126
|
+
def initialize(bits = 0, writer = nil)
|
|
127
|
+
@writer = writer
|
|
128
|
+
|
|
129
|
+
@bits = if bits.is_a? Array
|
|
130
|
+
self.class.bits(bits)
|
|
131
|
+
else
|
|
132
|
+
bits.to_i
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
init_vars
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Return an array of permission flag symbols for this class's permissions
|
|
139
|
+
# @example Get the permissions for the bits "9"
|
|
140
|
+
# permissions = Permissions.new(9)
|
|
141
|
+
# permissions.defined_permissions #=> [:create_instant_invite, :administrator]
|
|
142
|
+
# @return [Array<Symbol>] the permissions
|
|
143
|
+
def defined_permissions
|
|
144
|
+
FLAGS.filter_map { |value, name| @bits.anybits?((1 << value)) ? name : nil }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Comparison based on permission bits
|
|
148
|
+
def ==(other)
|
|
149
|
+
return false unless other.is_a?(OnyxCord::Permissions)
|
|
150
|
+
|
|
151
|
+
bits == other.bits
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Mixin to calculate resulting permissions from overrides etc.
|
|
156
|
+
module PermissionCalculator
|
|
157
|
+
# Checks whether this user can do the particular action, regardless of whether it has the permission defined,
|
|
158
|
+
# through for example being the server owner or having the Manage Roles permission
|
|
159
|
+
# @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
|
|
160
|
+
# @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
|
|
161
|
+
# @example Check if the bot can send messages to a specific channel in a server.
|
|
162
|
+
# bot_profile = bot.profile.on(event.server)
|
|
163
|
+
# can_send_messages = bot_profile.permission?(:send_messages, channel)
|
|
164
|
+
# @return [true, false] whether or not this user has the permission.
|
|
165
|
+
def permission?(action, channel = nil)
|
|
166
|
+
# If the member is the server owner, it irrevocably has all permissions.
|
|
167
|
+
return true if owner?
|
|
168
|
+
|
|
169
|
+
# First, check whether the user has Manage Roles defined.
|
|
170
|
+
# (Coincidentally, Manage Permissions is the same permission as Manage Roles, and a
|
|
171
|
+
# Manage Permissions deny overwrite will override Manage Roles, so we can just check for
|
|
172
|
+
# Manage Roles once and call it a day.)
|
|
173
|
+
return true if defined_permission?(:administrator, channel)
|
|
174
|
+
|
|
175
|
+
# Otherwise, defer to defined_permission
|
|
176
|
+
defined_permission?(action, channel)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Checks whether this user has a particular permission defined (i.e. not implicit, through for example
|
|
180
|
+
# Manage Roles)
|
|
181
|
+
# @param action [Symbol] The permission that should be checked. See also {Permissions::FLAGS} for a list.
|
|
182
|
+
# @param channel [Channel, nil] If channel overrides should be checked too, this channel specifies where the overrides should be checked.
|
|
183
|
+
# @example Check if a member has the Manage Channels permission defined in the server.
|
|
184
|
+
# has_manage_channels = member.defined_permission?(:manage_channels)
|
|
185
|
+
# @return [true, false] whether or not this user has the permission defined.
|
|
186
|
+
def defined_permission?(action, channel = nil)
|
|
187
|
+
# For slash commands we may not have access to the server or role
|
|
188
|
+
# permissions. In this case we use the permissions given to us by the
|
|
189
|
+
# interaction. If attempting to check against a specific channel the check
|
|
190
|
+
# is skipped.
|
|
191
|
+
return @permissions.__send__(action) if @permissions && channel.nil?
|
|
192
|
+
|
|
193
|
+
# Get the permission the user's roles have
|
|
194
|
+
role_permission = defined_role_permission?(action, channel)
|
|
195
|
+
|
|
196
|
+
# Once we have checked the role permission, we have to check the channel overrides for the
|
|
197
|
+
# specific user
|
|
198
|
+
user_specific_override = permission_overwrite(action, channel, id) # Use the ID reader as members have no ID instance variable
|
|
199
|
+
|
|
200
|
+
# Merge the two permissions - if an override is defined, it has to be allow, otherwise we only care about the role
|
|
201
|
+
return role_permission unless user_specific_override
|
|
202
|
+
|
|
203
|
+
user_specific_override == :allow
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Define methods for querying permissions
|
|
207
|
+
OnyxCord::Permissions::FLAGS.each_value do |flag|
|
|
208
|
+
define_method "can_#{flag}?" do |channel = nil|
|
|
209
|
+
permission? flag, channel
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
alias_method :can_administrate?, :can_administrator?
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def defined_role_permission?(action, channel)
|
|
218
|
+
roles_to_check = [@server.everyone_role] + roles
|
|
219
|
+
|
|
220
|
+
# For each role, check if
|
|
221
|
+
# (1) the channel explicitly allows or permits an action for the role and
|
|
222
|
+
# (2) if the user is allowed to do the action if the channel doesn't specify
|
|
223
|
+
roles_to_check.sort_by(&:position).reduce(false) do |can_act, role|
|
|
224
|
+
# Get the override defined for the role on the channel
|
|
225
|
+
channel_allow = permission_overwrite(action, channel, role.id)
|
|
226
|
+
if channel_allow
|
|
227
|
+
# If the channel has an override, check whether it is an allow - if yes,
|
|
228
|
+
# the user can act, if not, it can't
|
|
229
|
+
break true if channel_allow == :allow
|
|
230
|
+
|
|
231
|
+
false
|
|
232
|
+
else
|
|
233
|
+
# Otherwise defer to the role
|
|
234
|
+
role.permissions.instance_variable_get("@#{action}") || can_act
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def permission_overwrite(action, channel, id)
|
|
240
|
+
# If no overwrites are defined, or no channel is set, no overwrite will be present
|
|
241
|
+
return nil unless channel && channel.permission_overwrites[id]
|
|
242
|
+
|
|
243
|
+
# Otherwise, check the allow and deny objects
|
|
244
|
+
allow = channel.permission_overwrites[id].allow
|
|
245
|
+
deny = channel.permission_overwrites[id].deny
|
|
246
|
+
if allow.instance_variable_get("@#{action}")
|
|
247
|
+
:allow
|
|
248
|
+
elsif deny.instance_variable_get("@#{action}")
|
|
249
|
+
:deny
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# If there's no variable defined, nil will implicitly be returned
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OnyxCord
|
|
4
|
+
module RateLimiter
|
|
5
|
+
# Sliding-window limiter for Gateway sends.
|
|
6
|
+
class Gateway
|
|
7
|
+
DEFAULT_LIMIT = 120
|
|
8
|
+
DEFAULT_INTERVAL = 60
|
|
9
|
+
|
|
10
|
+
def initialize(limit: DEFAULT_LIMIT, interval: DEFAULT_INTERVAL, clock: -> { Time.now }, sleeper: ->(duration) { sleep(duration) })
|
|
11
|
+
@limit = limit
|
|
12
|
+
@interval = interval
|
|
13
|
+
@clock = clock
|
|
14
|
+
@sleeper = sleeper
|
|
15
|
+
@sent_at = []
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def wait
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
now = @clock.call
|
|
22
|
+
prune(now)
|
|
23
|
+
|
|
24
|
+
if @sent_at.length >= @limit
|
|
25
|
+
sleep_for = @interval - (now - @sent_at.first)
|
|
26
|
+
@sleeper.call(sleep_for) if sleep_for.positive?
|
|
27
|
+
now = @clock.call
|
|
28
|
+
prune(now)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@sent_at << now
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def prune(now)
|
|
38
|
+
@sent_at.shift while @sent_at.any? && (now - @sent_at.first) >= @interval
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module OnyxCord
|
|
6
|
+
module RateLimiter
|
|
7
|
+
# Discord REST rate limiter keyed by route/major parameter and remapped to
|
|
8
|
+
# X-RateLimit-Bucket whenever Discord returns a concrete bucket id.
|
|
9
|
+
class Rest
|
|
10
|
+
def initialize
|
|
11
|
+
@route_buckets = {}
|
|
12
|
+
@bucket_mutexes = {}
|
|
13
|
+
@global_mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def before_request(route, major_parameter)
|
|
17
|
+
mutex_wait(mutex_for(route, major_parameter))
|
|
18
|
+
mutex_wait(@global_mutex) if @global_mutex.locked?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def record_response(route, major_parameter, headers)
|
|
22
|
+
headers = normalize_headers(headers)
|
|
23
|
+
bucket = headers[:x_ratelimit_bucket]
|
|
24
|
+
|
|
25
|
+
@route_buckets[route_key(route, major_parameter)] = bucket_key(bucket, major_parameter) if bucket
|
|
26
|
+
|
|
27
|
+
return unless headers[:x_ratelimit_remaining] == '0'
|
|
28
|
+
|
|
29
|
+
wait_seconds = headers[:x_ratelimit_reset_after].to_f
|
|
30
|
+
return unless wait_seconds.positive?
|
|
31
|
+
|
|
32
|
+
sync_wait(wait_seconds, mutex_for(route, major_parameter))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def handle_rate_limit(route, major_parameter, response)
|
|
36
|
+
headers = normalize_headers(response.headers)
|
|
37
|
+
mutex = headers[:x_ratelimit_global] == 'true' || headers[:x_ratelimit_scope] == 'global' ? @global_mutex : mutex_for(route, major_parameter)
|
|
38
|
+
wait_seconds = retry_after(response, headers)
|
|
39
|
+
|
|
40
|
+
sync_wait(wait_seconds, mutex) if wait_seconds.positive?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def mutex_for(route, major_parameter)
|
|
46
|
+
@bucket_mutexes[resolved_key(route, major_parameter)] ||= Mutex.new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def resolved_key(route, major_parameter)
|
|
50
|
+
@route_buckets[route_key(route, major_parameter)] || route_key(route, major_parameter)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def route_key(route, major_parameter)
|
|
54
|
+
[route, major_parameter].freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def bucket_key(bucket, major_parameter)
|
|
58
|
+
[:bucket, bucket, major_parameter].freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def retry_after(response, headers)
|
|
62
|
+
body = response.respond_to?(:body) ? response.body : response.to_s
|
|
63
|
+
if body && !body.empty?
|
|
64
|
+
data = JSON.parse(body)
|
|
65
|
+
return data['retry_after'].to_f if data['retry_after']
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
(headers[:retry_after] || 0).to_f
|
|
69
|
+
rescue JSON::ParserError
|
|
70
|
+
(headers[:retry_after] || 0).to_f
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def normalize_headers(headers)
|
|
74
|
+
headers.each_with_object({}) do |(key, value), memo|
|
|
75
|
+
memo[key.to_s.tr('-', '_').downcase.to_sym] = value.to_s
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sync_wait(time, mutex)
|
|
80
|
+
mutex.synchronize { sleep time }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def mutex_wait(mutex)
|
|
84
|
+
mutex.lock
|
|
85
|
+
mutex.unlock
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This makes opus an optional dependency
|
|
4
|
+
begin
|
|
5
|
+
require 'opus-ruby'
|
|
6
|
+
OPUS_AVAILABLE = true
|
|
7
|
+
rescue LoadError
|
|
8
|
+
OPUS_AVAILABLE = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Discord voice chat support
|
|
12
|
+
module OnyxCord::Voice
|
|
13
|
+
# This class conveniently abstracts opus and ffmpeg/avconv, for easy implementation of voice sending. It's not very
|
|
14
|
+
# useful for most users, but I guess it can be useful sometimes.
|
|
15
|
+
class Encoder
|
|
16
|
+
# Whether or not avconv should be used instead of ffmpeg. If possible, it is recommended to use ffmpeg instead,
|
|
17
|
+
# as it is better supported.
|
|
18
|
+
# @return [true, false] whether avconv should be used instead of ffmpeg.
|
|
19
|
+
attr_accessor :use_avconv
|
|
20
|
+
|
|
21
|
+
# @see VoiceBot#filter_volume=
|
|
22
|
+
# @return [Integer] the volume used as a filter to ffmpeg/avconv.
|
|
23
|
+
attr_accessor :filter_volume
|
|
24
|
+
|
|
25
|
+
# Create a new encoder
|
|
26
|
+
def initialize
|
|
27
|
+
sample_rate = 48_000
|
|
28
|
+
frame_size = 960
|
|
29
|
+
channels = 2
|
|
30
|
+
@filter_volume = 1
|
|
31
|
+
|
|
32
|
+
raise LoadError, 'Opus unavailable - voice not supported! Please install opus for voice support to work.' unless OPUS_AVAILABLE
|
|
33
|
+
|
|
34
|
+
@opus = Opus::Encoder.new(sample_rate, frame_size, channels)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Set the opus encoding bitrate
|
|
38
|
+
# @param value [Integer] The new bitrate to use, in bits per second (so 64000 if you want 64 kbps)
|
|
39
|
+
def bitrate=(value)
|
|
40
|
+
@opus.bitrate = value
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Encodes the given buffer using opus.
|
|
44
|
+
# @param buffer [String] An unencoded PCM (s16le) buffer.
|
|
45
|
+
# @return [String] A buffer encoded using opus.
|
|
46
|
+
def encode(buffer)
|
|
47
|
+
@opus.encode(buffer, 1920)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# One frame of complete silence Opus encoded
|
|
51
|
+
OPUS_SILENCE = [0xF8, 0xFF, 0xFE].pack('C*').freeze
|
|
52
|
+
|
|
53
|
+
# Adjusts the volume of a given buffer of s16le PCM data.
|
|
54
|
+
# @param buf [String] An unencoded PCM (s16le) buffer.
|
|
55
|
+
# @param mult [Float] The volume multiplier, 1 for same volume.
|
|
56
|
+
# @return [String] The buffer with adjusted volume, s16le again
|
|
57
|
+
def adjust_volume(buf, mult)
|
|
58
|
+
# We don't need to adjust anything if the buf is nil so just return in that case
|
|
59
|
+
return unless buf
|
|
60
|
+
|
|
61
|
+
# buf is s16le so use 's<' for signed, 16 bit, LE
|
|
62
|
+
result = buf.unpack('s<*').map do |sample|
|
|
63
|
+
sample *= mult
|
|
64
|
+
|
|
65
|
+
# clamp to s16 range
|
|
66
|
+
[32_767, [-32_768, sample].max].min
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# After modification, make it s16le again
|
|
70
|
+
result.pack('s<*')
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Encodes a given file (or rather, decodes it) using ffmpeg. This accepts pretty much any format, even videos with
|
|
74
|
+
# an audio track. For a list of supported formats, see https://ffmpeg.org/general.html#Audio-Codecs. It even accepts
|
|
75
|
+
# URLs, though encoding them is pretty slow - I recommend to make a stream of it and then use {#encode_io} instead.
|
|
76
|
+
# @param file [String] The path or URL to encode.
|
|
77
|
+
# @param options [String] ffmpeg options to pass after the -i flag
|
|
78
|
+
# @return [IO] the audio, encoded as s16le PCM
|
|
79
|
+
def encode_file(file, options = '')
|
|
80
|
+
command = ffmpeg_command(input: file, options: options)
|
|
81
|
+
IO.popen(command)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Encodes an arbitrary IO audio stream using ffmpeg. Accepts pretty much any media format, even videos with audio
|
|
85
|
+
# tracks. For a list of supported audio formats, see https://ffmpeg.org/general.html#Audio-Codecs.
|
|
86
|
+
# @param io [IO] The stream to encode.
|
|
87
|
+
# @param options [String] ffmpeg options to pass after the -i flag
|
|
88
|
+
# @return [IO] the audio, encoded as s16le PCM
|
|
89
|
+
def encode_io(io, options = '')
|
|
90
|
+
command = ffmpeg_command(options: options)
|
|
91
|
+
IO.popen(command, in: io)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
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 == '' }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def filter_volume_argument
|
|
110
|
+
return '' if @filter_volume == 1
|
|
111
|
+
|
|
112
|
+
@use_avconv ? "-vol #{(@filter_volume * 256).ceil}" : "-af volume=#{@filter_volume}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|