rubycord 1.0.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/lib/rubycord/allowed_mentions.rb +34 -0
- data/lib/rubycord/api/application.rb +200 -0
- data/lib/rubycord/api/channel.rb +597 -0
- data/lib/rubycord/api/interaction.rb +52 -0
- data/lib/rubycord/api/invite.rb +42 -0
- data/lib/rubycord/api/server.rb +557 -0
- data/lib/rubycord/api/user.rb +153 -0
- data/lib/rubycord/api/webhook.rb +138 -0
- data/lib/rubycord/api.rb +356 -0
- data/lib/rubycord/await.rb +49 -0
- data/lib/rubycord/bot.rb +1757 -0
- data/lib/rubycord/cache.rb +259 -0
- data/lib/rubycord/colour_rgb.rb +41 -0
- data/lib/rubycord/commands/command_bot.rb +519 -0
- data/lib/rubycord/commands/container.rb +110 -0
- data/lib/rubycord/commands/events.rb +9 -0
- data/lib/rubycord/commands/parser.rb +325 -0
- data/lib/rubycord/commands/rate_limiter.rb +142 -0
- data/lib/rubycord/container.rb +753 -0
- data/lib/rubycord/data/activity.rb +269 -0
- data/lib/rubycord/data/application.rb +48 -0
- data/lib/rubycord/data/attachment.rb +109 -0
- data/lib/rubycord/data/audit_logs.rb +343 -0
- data/lib/rubycord/data/channel.rb +996 -0
- data/lib/rubycord/data/component.rb +227 -0
- data/lib/rubycord/data/embed.rb +249 -0
- data/lib/rubycord/data/emoji.rb +80 -0
- data/lib/rubycord/data/integration.rb +120 -0
- data/lib/rubycord/data/interaction.rb +798 -0
- data/lib/rubycord/data/invite.rb +135 -0
- data/lib/rubycord/data/member.rb +370 -0
- data/lib/rubycord/data/message.rb +412 -0
- data/lib/rubycord/data/overwrite.rb +106 -0
- data/lib/rubycord/data/profile.rb +89 -0
- data/lib/rubycord/data/reaction.rb +31 -0
- data/lib/rubycord/data/recipient.rb +32 -0
- data/lib/rubycord/data/role.rb +246 -0
- data/lib/rubycord/data/server.rb +1002 -0
- data/lib/rubycord/data/user.rb +261 -0
- data/lib/rubycord/data/voice_region.rb +43 -0
- data/lib/rubycord/data/voice_state.rb +39 -0
- data/lib/rubycord/data/webhook.rb +232 -0
- data/lib/rubycord/data.rb +40 -0
- data/lib/rubycord/errors.rb +737 -0
- data/lib/rubycord/events/await.rb +46 -0
- data/lib/rubycord/events/bans.rb +58 -0
- data/lib/rubycord/events/channels.rb +186 -0
- data/lib/rubycord/events/generic.rb +126 -0
- data/lib/rubycord/events/guilds.rb +191 -0
- data/lib/rubycord/events/interactions.rb +480 -0
- data/lib/rubycord/events/invites.rb +123 -0
- data/lib/rubycord/events/lifetime.rb +29 -0
- data/lib/rubycord/events/members.rb +91 -0
- data/lib/rubycord/events/message.rb +337 -0
- data/lib/rubycord/events/presence.rb +127 -0
- data/lib/rubycord/events/raw.rb +45 -0
- data/lib/rubycord/events/reactions.rb +156 -0
- data/lib/rubycord/events/roles.rb +86 -0
- data/lib/rubycord/events/threads.rb +94 -0
- data/lib/rubycord/events/typing.rb +70 -0
- data/lib/rubycord/events/voice_server_update.rb +45 -0
- data/lib/rubycord/events/voice_state_update.rb +103 -0
- data/lib/rubycord/events/webhooks.rb +62 -0
- data/lib/rubycord/gateway.rb +867 -0
- data/lib/rubycord/id_object.rb +37 -0
- data/lib/rubycord/light/data.rb +60 -0
- data/lib/rubycord/light/integrations.rb +71 -0
- data/lib/rubycord/light/light_bot.rb +56 -0
- data/lib/rubycord/light.rb +6 -0
- data/lib/rubycord/logger.rb +118 -0
- data/lib/rubycord/paginator.rb +55 -0
- data/lib/rubycord/permissions.rb +251 -0
- data/lib/rubycord/version.rb +5 -0
- data/lib/rubycord/voice/encoder.rb +113 -0
- data/lib/rubycord/voice/network.rb +366 -0
- data/lib/rubycord/voice/sodium.rb +96 -0
- data/lib/rubycord/voice/voice_bot.rb +408 -0
- data/lib/rubycord/webhooks/builder.rb +100 -0
- data/lib/rubycord/webhooks/client.rb +132 -0
- data/lib/rubycord/webhooks/embeds.rb +248 -0
- data/lib/rubycord/webhooks/modal.rb +78 -0
- data/lib/rubycord/webhooks/version.rb +7 -0
- data/lib/rubycord/webhooks/view.rb +192 -0
- data/lib/rubycord/webhooks.rb +12 -0
- data/lib/rubycord/websocket.rb +70 -0
- data/lib/rubycord.rb +140 -0
- metadata +231 -0
@@ -0,0 +1,325 @@
|
|
1
|
+
module Rubycord::Commands
|
2
|
+
# Command that can be called in a chain
|
3
|
+
class Command
|
4
|
+
# @return [Hash] the attributes the command was initialized with
|
5
|
+
attr_reader :attributes
|
6
|
+
|
7
|
+
# @return [Symbol] the name of this command
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
def initialize(name, attributes = {}, &block)
|
12
|
+
@name = name
|
13
|
+
@attributes = {
|
14
|
+
# The lowest permission level that can use the command
|
15
|
+
permission_level: attributes[:permission_level] || 0,
|
16
|
+
|
17
|
+
# Message to display when a user does not have sufficient permissions to execute a command
|
18
|
+
permission_message: attributes[:permission_message].is_a?(FalseClass) ? nil : (attributes[:permission_message] || "You don't have permission to execute command %name%!"),
|
19
|
+
|
20
|
+
# Discord action permissions required to use this command
|
21
|
+
required_permissions: attributes[:required_permissions] || [],
|
22
|
+
|
23
|
+
# Roles required to use this command (all? comparison)
|
24
|
+
required_roles: attributes[:required_roles] || [],
|
25
|
+
|
26
|
+
# Roles allowed to use this command (any? comparison)
|
27
|
+
allowed_roles: attributes[:allowed_roles] || [],
|
28
|
+
|
29
|
+
# Channels this command can be used on
|
30
|
+
channels: attributes[:channels] || nil,
|
31
|
+
|
32
|
+
# Whether this command is usable in a command chain
|
33
|
+
chain_usable: attributes[:chain_usable].nil? ? true : attributes[:chain_usable],
|
34
|
+
|
35
|
+
# Whether this command should show up in the help command
|
36
|
+
help_available: attributes[:help_available].nil? ? true : attributes[:help_available],
|
37
|
+
|
38
|
+
# Description (for help command)
|
39
|
+
description: attributes[:description] || nil,
|
40
|
+
|
41
|
+
# Usage description (for help command and error messages)
|
42
|
+
usage: attributes[:usage] || nil,
|
43
|
+
|
44
|
+
# Array of arguments (for type-checking)
|
45
|
+
arg_types: attributes[:arg_types] || nil,
|
46
|
+
|
47
|
+
# Parameter list (for help command and error messages)
|
48
|
+
parameters: attributes[:parameters] || nil,
|
49
|
+
|
50
|
+
# Minimum number of arguments
|
51
|
+
min_args: attributes[:min_args] || 0,
|
52
|
+
|
53
|
+
# Maximum number of arguments (-1 for no limit)
|
54
|
+
max_args: attributes[:max_args] || -1,
|
55
|
+
|
56
|
+
# Message to display upon rate limiting (%time% in the message for the remaining time until the next possible
|
57
|
+
# request, nil for no message)
|
58
|
+
rate_limit_message: attributes[:rate_limit_message],
|
59
|
+
|
60
|
+
# Rate limiting bucket (nil for no rate limiting)
|
61
|
+
bucket: attributes[:bucket],
|
62
|
+
|
63
|
+
# Block for handling internal exceptions, or a string to respond with
|
64
|
+
rescue: attributes[:rescue],
|
65
|
+
|
66
|
+
# A list of aliases that reference this command
|
67
|
+
aliases: attributes[:aliases] || []
|
68
|
+
}
|
69
|
+
|
70
|
+
@block = block
|
71
|
+
end
|
72
|
+
|
73
|
+
# Calls this command and executes the code inside.
|
74
|
+
# @param event [CommandEvent] The event to call the command with.
|
75
|
+
# @param arguments [Array<String>] The attributes for the command.
|
76
|
+
# @param chained [true, false] Whether or not this command is part of a command chain.
|
77
|
+
# @param check_permissions [true, false] Whether the user's permission to execute the command (i.e. rate limits)
|
78
|
+
# should be checked.
|
79
|
+
# @return [String] the result of the execution.
|
80
|
+
def call(event, arguments, chained = false, check_permissions = true)
|
81
|
+
if arguments.length < @attributes[:min_args]
|
82
|
+
response = "Too few arguments for command `#{name}`!"
|
83
|
+
response += "\nUsage: `#{@attributes[:usage]}`" if @attributes[:usage]
|
84
|
+
event.respond(response)
|
85
|
+
return
|
86
|
+
end
|
87
|
+
if @attributes[:max_args] >= 0 && arguments.length > @attributes[:max_args]
|
88
|
+
response = "Too many arguments for command `#{name}`!"
|
89
|
+
response += "\nUsage: `#{@attributes[:usage]}`" if @attributes[:usage]
|
90
|
+
event.respond(response)
|
91
|
+
return
|
92
|
+
end
|
93
|
+
unless @attributes[:chain_usable] && !chained
|
94
|
+
event.respond "Command `#{name}` cannot be used in a command chain!"
|
95
|
+
return
|
96
|
+
end
|
97
|
+
|
98
|
+
if check_permissions
|
99
|
+
rate_limited = event.bot.rate_limited?(@attributes[:bucket], event.author)
|
100
|
+
if @attributes[:bucket] && rate_limited
|
101
|
+
event.respond @attributes[:rate_limit_message].gsub("%time%", rate_limited.round(2).to_s) if @attributes[:rate_limit_message]
|
102
|
+
return
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
result = @block.call(event, *arguments)
|
107
|
+
event.drain_into(result)
|
108
|
+
rescue LocalJumpError => e # occurs when breaking
|
109
|
+
result = e.exit_value
|
110
|
+
event.drain_into(result)
|
111
|
+
rescue => e # Something went wrong inside our @block!
|
112
|
+
rescue_value = @attributes[:rescue] || event.bot.attributes[:rescue]
|
113
|
+
if rescue_value
|
114
|
+
event.respond(rescue_value.gsub("%exception%", e.message)) if rescue_value.is_a?(String)
|
115
|
+
rescue_value.call(event, e) if rescue_value.respond_to?(:call)
|
116
|
+
end
|
117
|
+
|
118
|
+
raise e
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# A command that references another command
|
123
|
+
class CommandAlias
|
124
|
+
# @return [Symbol] the name of this alias
|
125
|
+
attr_reader :name
|
126
|
+
|
127
|
+
# @return [Command] the command this alias points to
|
128
|
+
attr_reader :aliased_command
|
129
|
+
|
130
|
+
def initialize(name, aliased_command)
|
131
|
+
@name = name
|
132
|
+
@aliased_command = aliased_command
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Command chain, may have multiple commands, nested and commands
|
137
|
+
class CommandChain
|
138
|
+
# @param chain [String] The string the chain should be parsed from.
|
139
|
+
# @param bot [CommandBot] The bot that executes this command chain.
|
140
|
+
# @param subchain [true, false] Whether this chain is a sub chain of another chain.
|
141
|
+
def initialize(chain, bot, subchain = false)
|
142
|
+
@attributes = bot.attributes
|
143
|
+
@chain = chain
|
144
|
+
@bot = bot
|
145
|
+
@subchain = subchain
|
146
|
+
end
|
147
|
+
|
148
|
+
# Parses the command chain itself, including sub-chains, and executes it. Executes only the command chain, without
|
149
|
+
# its chain arguments.
|
150
|
+
# @param event [CommandEvent] The event to execute the chain with.
|
151
|
+
# @return [String] the result of the execution.
|
152
|
+
def execute_bare(event)
|
153
|
+
b_start = -1
|
154
|
+
b_level = 0
|
155
|
+
result = ""
|
156
|
+
quoted = false
|
157
|
+
escaped = false
|
158
|
+
hacky_delim, hacky_space, hacky_prev, hacky_newline = [0xe001, 0xe002, 0xe003, 0xe004].pack("U*").chars
|
159
|
+
|
160
|
+
@chain.each_char.with_index do |char, index|
|
161
|
+
# Escape character
|
162
|
+
if char == "\\" && !escaped
|
163
|
+
escaped = true
|
164
|
+
next
|
165
|
+
elsif escaped && b_level <= 0
|
166
|
+
result += char
|
167
|
+
escaped = false
|
168
|
+
next
|
169
|
+
end
|
170
|
+
|
171
|
+
if quoted
|
172
|
+
# Quote end
|
173
|
+
if char == @attributes[:quote_end]
|
174
|
+
quoted = false
|
175
|
+
next
|
176
|
+
end
|
177
|
+
|
178
|
+
if b_level <= 0
|
179
|
+
case char
|
180
|
+
when @attributes[:chain_delimiter]
|
181
|
+
result += hacky_delim
|
182
|
+
next
|
183
|
+
when @attributes[:previous]
|
184
|
+
result += hacky_prev
|
185
|
+
next
|
186
|
+
when " "
|
187
|
+
result += hacky_space
|
188
|
+
next
|
189
|
+
when "\n"
|
190
|
+
result += hacky_newline
|
191
|
+
next
|
192
|
+
end
|
193
|
+
end
|
194
|
+
else
|
195
|
+
case char
|
196
|
+
when @attributes[:quote_start] # Quote begin
|
197
|
+
quoted = true
|
198
|
+
next
|
199
|
+
when @attributes[:sub_chain_start]
|
200
|
+
b_start = index if b_level.zero?
|
201
|
+
b_level += 1
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
result += char if b_level <= 0
|
206
|
+
|
207
|
+
next unless char == @attributes[:sub_chain_end] && !quoted
|
208
|
+
|
209
|
+
b_level -= 1
|
210
|
+
next unless b_level.zero?
|
211
|
+
|
212
|
+
nested = @chain[b_start + 1..index - 1]
|
213
|
+
subchain = CommandChain.new(nested, @bot, true)
|
214
|
+
result += subchain.execute(event)
|
215
|
+
end
|
216
|
+
|
217
|
+
event.respond("Your subchains are mismatched! Make sure you don't have any extra #{@attributes[:sub_chain_start]}'s or #{@attributes[:sub_chain_end]}'s") unless b_level.zero?
|
218
|
+
|
219
|
+
@chain = result
|
220
|
+
|
221
|
+
@chain_args, @chain = divide_chain(@chain)
|
222
|
+
|
223
|
+
prev = ""
|
224
|
+
|
225
|
+
chain_to_split = @chain
|
226
|
+
|
227
|
+
# Don't break if a command is called the same thing as the chain delimiter
|
228
|
+
chain_to_split = chain_to_split.slice(1..-1) if !@attributes[:chain_delimiter].empty? && chain_to_split.start_with?(@attributes[:chain_delimiter])
|
229
|
+
|
230
|
+
first = true
|
231
|
+
split_chain = if @attributes[:chain_delimiter].empty?
|
232
|
+
[chain_to_split]
|
233
|
+
else
|
234
|
+
chain_to_split.split(@attributes[:chain_delimiter])
|
235
|
+
end
|
236
|
+
split_chain.each do |command|
|
237
|
+
command = @attributes[:chain_delimiter] + command if first && @chain.start_with?(@attributes[:chain_delimiter])
|
238
|
+
first = false
|
239
|
+
|
240
|
+
command = command.strip
|
241
|
+
|
242
|
+
# Replace the hacky delimiter that was used inside quotes with actual delimiters
|
243
|
+
command = command.gsub hacky_delim, @attributes[:chain_delimiter]
|
244
|
+
|
245
|
+
first_space = command.index " "
|
246
|
+
command_name = first_space ? command[0..first_space - 1] : command
|
247
|
+
arguments = first_space ? command[first_space + 1..] : ""
|
248
|
+
|
249
|
+
# Append a previous sign if none is present
|
250
|
+
arguments += @attributes[:previous] unless arguments.include? @attributes[:previous]
|
251
|
+
arguments = arguments.gsub @attributes[:previous], prev
|
252
|
+
|
253
|
+
# Replace hacky previous signs with actual ones
|
254
|
+
arguments = arguments.gsub hacky_prev, @attributes[:previous]
|
255
|
+
|
256
|
+
arguments = arguments.split " "
|
257
|
+
|
258
|
+
# Replace the hacky spaces/newlines with actual ones
|
259
|
+
arguments.map! do |elem|
|
260
|
+
elem.gsub(hacky_space, " ").gsub(hacky_newline, "\n")
|
261
|
+
end
|
262
|
+
|
263
|
+
# Finally execute the command
|
264
|
+
prev = @bot.execute_command(command_name.to_sym, event, arguments, split_chain.length > 1 || @subchain)
|
265
|
+
|
266
|
+
# Stop chain if execute_command failed (maybe the command doesn't exist, or permissions failed, etc.)
|
267
|
+
break unless prev
|
268
|
+
end
|
269
|
+
|
270
|
+
prev
|
271
|
+
end
|
272
|
+
|
273
|
+
# Divides the command chain into chain arguments and command chain, then executes them both.
|
274
|
+
# @param event [CommandEvent] The event to execute the command with.
|
275
|
+
# @return [String] the result of the command chain execution.
|
276
|
+
def execute(event)
|
277
|
+
old_chain = @chain
|
278
|
+
@bot.debug "Executing bare chain"
|
279
|
+
result = execute_bare(event)
|
280
|
+
|
281
|
+
@chain_args ||= []
|
282
|
+
|
283
|
+
@bot.debug "Found chain args #{@chain_args}, preliminary result #{result}"
|
284
|
+
|
285
|
+
@chain_args.each do |arg|
|
286
|
+
case arg.first
|
287
|
+
when "repeat"
|
288
|
+
new_result = ""
|
289
|
+
executed_chain = divide_chain(old_chain).last
|
290
|
+
|
291
|
+
arg[1].to_i.times do
|
292
|
+
chain_result = CommandChain.new(executed_chain, @bot).execute(event)
|
293
|
+
new_result += chain_result if chain_result
|
294
|
+
end
|
295
|
+
|
296
|
+
result = new_result
|
297
|
+
# TODO: more chain arguments
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
result
|
302
|
+
end
|
303
|
+
|
304
|
+
private
|
305
|
+
|
306
|
+
def divide_chain(chain)
|
307
|
+
chain_args_index = chain.index(@attributes[:chain_args_delim]) unless @attributes[:chain_args_delim].empty?
|
308
|
+
chain_args = []
|
309
|
+
|
310
|
+
if chain_args_index
|
311
|
+
chain_args = chain[0..chain_args_index].split ","
|
312
|
+
|
313
|
+
# Split up the arguments
|
314
|
+
|
315
|
+
chain_args.map! do |arg|
|
316
|
+
arg.split " "
|
317
|
+
end
|
318
|
+
|
319
|
+
chain = chain[chain_args_index + 1..]
|
320
|
+
end
|
321
|
+
|
322
|
+
[chain_args, chain]
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Rubycord::Commands
|
2
|
+
# This class represents a bucket for rate limiting - it keeps track of how many requests have been made and when
|
3
|
+
# exactly the user should be rate limited.
|
4
|
+
class Bucket
|
5
|
+
# Makes a new bucket
|
6
|
+
# @param limit [Integer, nil] How many requests the user may perform in the given time_span, or nil if there should be no limit.
|
7
|
+
# @param time_span [Integer, nil] The time span after which the request count is reset, in seconds, or nil if the bucket should never be reset. (If this is nil, limit should be nil too)
|
8
|
+
# @param delay [Integer, nil] The delay for which the user has to wait after performing a request, in seconds, or nil if the user shouldn't have to wait.
|
9
|
+
def initialize(limit, time_span, delay)
|
10
|
+
raise ArgumentError, "`limit` and `time_span` have to either both be set or both be nil!" if !limit != !time_span
|
11
|
+
|
12
|
+
@limit = limit
|
13
|
+
@time_span = time_span
|
14
|
+
@delay = delay
|
15
|
+
|
16
|
+
@bucket = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Cleans the bucket, removing all elements that aren't necessary anymore
|
20
|
+
# @param rate_limit_time [Time] The time to base the cleaning on, only useful for testing.
|
21
|
+
def clean(rate_limit_time = nil)
|
22
|
+
rate_limit_time ||= Time.now
|
23
|
+
|
24
|
+
@bucket.delete_if do |_, limit_hash|
|
25
|
+
# Time limit has not run out
|
26
|
+
return false if @time_span && rate_limit_time < (limit_hash[:set_time] + @time_span)
|
27
|
+
|
28
|
+
# Delay has not run out
|
29
|
+
return false if @delay && rate_limit_time < (limit_hash[:last_time] + @delay)
|
30
|
+
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Performs a rate limiting request
|
36
|
+
# @param thing [String, Integer, Symbol] The particular thing that should be rate-limited (usually a user/channel, but you can also choose arbitrary integers or symbols)
|
37
|
+
# @param rate_limit_time [Time] The time to base the rate limiting on, only useful for testing.
|
38
|
+
# @param increment [Integer] How much to increment the rate-limit counter. Default is 1.
|
39
|
+
# @return [Integer, false] the waiting time until the next request, in seconds, or false if the request succeeded
|
40
|
+
def rate_limited?(thing, rate_limit_time = nil, increment: 1)
|
41
|
+
key = resolve_key thing
|
42
|
+
limit_hash = @bucket[key]
|
43
|
+
|
44
|
+
# First case: limit_hash doesn't exist yet
|
45
|
+
unless limit_hash
|
46
|
+
@bucket[key] = {
|
47
|
+
last_time: Time.now,
|
48
|
+
set_time: Time.now,
|
49
|
+
count: increment
|
50
|
+
}
|
51
|
+
|
52
|
+
return false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Define the time at which we're being rate limited once so it doesn't get inaccurate
|
56
|
+
rate_limit_time ||= Time.now
|
57
|
+
|
58
|
+
if @limit && (limit_hash[:count] + increment) > @limit
|
59
|
+
# Second case: Count is over the limit and the time has not run out yet
|
60
|
+
return (limit_hash[:set_time] + @time_span) - rate_limit_time if @time_span && rate_limit_time < (limit_hash[:set_time] + @time_span)
|
61
|
+
|
62
|
+
# Third case: Count is over the limit but the time has run out
|
63
|
+
# Don't return anything here because there may still be delay-based limiting
|
64
|
+
limit_hash[:set_time] = rate_limit_time
|
65
|
+
limit_hash[:count] = 0
|
66
|
+
end
|
67
|
+
|
68
|
+
if @delay && rate_limit_time < (limit_hash[:last_time] + @delay)
|
69
|
+
# Fourth case: we're being delayed
|
70
|
+
(limit_hash[:last_time] + @delay) - rate_limit_time
|
71
|
+
else
|
72
|
+
# Fifth case: no rate limiting at all! Increment the count, set the last_time, and return false
|
73
|
+
limit_hash[:last_time] = rate_limit_time
|
74
|
+
limit_hash[:count] += increment
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def resolve_key(thing)
|
82
|
+
return thing.resolve_id if thing.respond_to?(:resolve_id) && !thing.is_a?(String)
|
83
|
+
return thing if thing.is_a?(Integer) || thing.is_a?(Symbol)
|
84
|
+
|
85
|
+
raise ArgumentError, "Cannot use a #{thing.class} as a rate limiting key!"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Represents a collection of {Bucket}s.
|
90
|
+
module RateLimiter
|
91
|
+
# Defines a new bucket for this rate limiter.
|
92
|
+
# @param key [Symbol] The name for this new bucket.
|
93
|
+
# @param attributes [Hash] The attributes to initialize the bucket with.
|
94
|
+
# @option attributes [Integer] :limit The limit of requests to perform in the given time span.
|
95
|
+
# @option attributes [Integer] :time_span How many seconds until the limit should be reset.
|
96
|
+
# @option attributes [Integer] :delay How many seconds the user has to wait after each request.
|
97
|
+
# @see Bucket#initialize
|
98
|
+
# @return [Bucket] the created bucket.
|
99
|
+
def bucket(key, attributes)
|
100
|
+
@buckets ||= {}
|
101
|
+
@buckets[key] = Bucket.new(attributes[:limit], attributes[:time_span], attributes[:delay])
|
102
|
+
end
|
103
|
+
|
104
|
+
# Performs a rate limit request.
|
105
|
+
# @param key [Symbol] Which bucket to perform the request for.
|
106
|
+
# @param thing [String, Integer, Symbol] What should be rate-limited.
|
107
|
+
# @param increment (see Bucket#rate_limited?)
|
108
|
+
# @see Bucket#rate_limited?
|
109
|
+
# @return [Integer, false] How much time to wait or false if the request succeeded.
|
110
|
+
def rate_limited?(key, thing, increment: 1)
|
111
|
+
# Check whether the bucket actually exists
|
112
|
+
return false unless @buckets && @buckets[key]
|
113
|
+
|
114
|
+
@buckets[key].rate_limited?(thing, increment: increment)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Cleans all buckets
|
118
|
+
# @see Bucket#clean
|
119
|
+
def clean
|
120
|
+
@buckets.each(&:clean)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Adds all the buckets from another RateLimiter onto this one.
|
124
|
+
# @param limiter [Module] Another {RateLimiter} module
|
125
|
+
def include_buckets(limiter)
|
126
|
+
buckets = limiter.instance_variable_get(:@buckets) || {}
|
127
|
+
@buckets ||= {}
|
128
|
+
@buckets.merge! buckets
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# This class provides a convenient way to do rate-limiting on non-command events.
|
133
|
+
# @see RateLimiter
|
134
|
+
class SimpleRateLimiter
|
135
|
+
include RateLimiter
|
136
|
+
|
137
|
+
# Makes a new rate limiter
|
138
|
+
def initialize
|
139
|
+
@buckets = {}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|