discordrb 1.6.6 → 1.7.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/.travis.yml +3 -0
- data/CHANGELOG.md +26 -0
- data/README.md +1 -1
- data/discordrb.gemspec +2 -0
- data/lib/discordrb.rb +1 -1
- data/lib/discordrb/api.rb +36 -5
- data/lib/discordrb/await.rb +3 -1
- data/lib/discordrb/bot.rb +70 -275
- data/lib/discordrb/commands/command_bot.rb +81 -28
- data/lib/discordrb/commands/container.rb +78 -0
- data/lib/discordrb/commands/events.rb +2 -12
- data/lib/discordrb/commands/parser.rb +40 -4
- data/lib/discordrb/commands/rate_limiter.rb +141 -0
- data/lib/discordrb/container.rb +379 -0
- data/lib/discordrb/data.rb +52 -4
- data/lib/discordrb/errors.rb +24 -0
- data/lib/discordrb/events/bans.rb +10 -8
- data/lib/discordrb/events/generic.rb +38 -2
- data/lib/discordrb/events/guilds.rb +11 -1
- data/lib/discordrb/events/lifetime.rb +6 -0
- data/lib/discordrb/events/members.rb +9 -1
- data/lib/discordrb/events/message.rb +33 -1
- data/lib/discordrb/logger.rb +7 -1
- data/lib/discordrb/permissions.rb +3 -0
- data/lib/discordrb/token_cache.rb +38 -4
- data/lib/discordrb/version.rb +1 -1
- data/lib/discordrb/voice/encoder.rb +29 -19
- data/lib/discordrb/voice/network.rb +6 -11
- data/lib/discordrb/voice/voice_bot.rb +112 -43
- metadata +34 -3
- data/lib/discordrb/exceptions.rb +0 -12
@@ -2,18 +2,56 @@ require 'discordrb/bot'
|
|
2
2
|
require 'discordrb/data'
|
3
3
|
require 'discordrb/commands/parser'
|
4
4
|
require 'discordrb/commands/events'
|
5
|
+
require 'discordrb/commands/container'
|
6
|
+
require 'discordrb/commands/rate_limiter'
|
5
7
|
|
6
8
|
# Specialized bot to run commands
|
7
9
|
|
8
10
|
module Discordrb::Commands
|
9
11
|
# Bot that supports commands and command chains
|
10
12
|
class CommandBot < Discordrb::Bot
|
11
|
-
|
12
|
-
|
13
|
+
# @return [Hash] this bot's attributes.
|
14
|
+
attr_reader :attributes
|
15
|
+
|
16
|
+
# @return [String] the prefix commands are triggered with.
|
17
|
+
attr_reader :prefix
|
18
|
+
|
19
|
+
include CommandContainer
|
20
|
+
|
21
|
+
# Creates a new CommandBot and logs in to Discord.
|
22
|
+
# @param email [String] The email to use to log in.
|
23
|
+
# @param password [String] The password corresponding to the email.
|
24
|
+
# @param prefix [String] The prefix that should trigger this bot's commands. Can be any string (including the empty
|
25
|
+
# string), but note that it will be literal - if the prefix is "hi" then the corresponding trigger string for
|
26
|
+
# a command called "test" would be "hitest". Don't forget to put spaces in if you need them!
|
27
|
+
# @param attributes [Hash] The attributes to initialize the CommandBot with.
|
28
|
+
# @param debug [true, false] Whether or not debug mode should be used - debug mode logs tons of extra stuff to the
|
29
|
+
# console that may be useful in development.
|
30
|
+
# @option attributes [true, false] :advanced_functionality Whether to enable advanced functionality (very powerful
|
31
|
+
# way to nest commands into chains, see https://github.com/meew0/discordrb/wiki/Commands#command-chain-syntax
|
32
|
+
# for info. Default is true.
|
33
|
+
# @option attributes [Symbol, Array<Symbol>] :help_command The name of the command that displays info for other
|
34
|
+
# commands. Use an array if you want to have aliases. Default is "help".
|
35
|
+
# @option attributes [String] :command_doesnt_exist_message The message that should be displayed if a user attempts
|
36
|
+
# to use a command that does not exist. If none is specified, no message will be displayed. In the message, you
|
37
|
+
# can use the string '%command%' that will be replaced with the name of the command.
|
38
|
+
# @option attributes [String] :previous Character that should designate the result of the previous command in
|
39
|
+
# a command chain (see :advanced_functionality). Default is '~'.
|
40
|
+
# @option attributes [String] :chain_delimiter Character that should designate that a new command begins in the
|
41
|
+
# command chain (see :advanced_functionality). Default is '>'.
|
42
|
+
# @option attributes [String] :chain_args_delim Character that should separate the command chain arguments from the
|
43
|
+
# chain itself (see :advanced_functionality). Default is ':'.
|
44
|
+
# @option attributes [String] :sub_chain_start Character that should start a sub-chain (see
|
45
|
+
# :advanced_functionality). Default is '['.
|
46
|
+
# @option attributes [String] :sub_chain_end Character that should end a sub-chain (see
|
47
|
+
# :advanced_functionality). Default is ']'.
|
48
|
+
# @option attributes [String] :quote_start Character that should start a quoted string (see
|
49
|
+
# :advanced_functionality). Default is '"'.
|
50
|
+
# @option attributes [String] :quote_end Character that should end a quoted string (see
|
51
|
+
# :advanced_functionality). Default is '"'.
|
13
52
|
def initialize(email, password, prefix, attributes = {}, debug = false)
|
14
53
|
super(email, password, debug)
|
15
54
|
@prefix = prefix
|
16
|
-
@commands = {}
|
17
55
|
@attributes = {
|
18
56
|
# Whether advanced functionality such as command chains are enabled
|
19
57
|
advanced_functionality: attributes[:advanced_functionality].nil? ? true : attributes[:advanced_functionality],
|
@@ -81,16 +119,13 @@ module Discordrb::Commands
|
|
81
119
|
end
|
82
120
|
end
|
83
121
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
122
|
+
# Executes a particular command on the bot. Mostly useful for internal stuff, but one can never know.
|
123
|
+
# @param name [Symbol] The command to execute.
|
124
|
+
# @param event [CommandEvent] The event to pass to the command.
|
125
|
+
# @param arguments [Array<String>] The arguments to pass to the command.
|
126
|
+
# @param chained [true, false] Whether or not it should be executed as part of a command chain. If this is false,
|
127
|
+
# commands that have chain_usable set to false will not work.
|
128
|
+
# @return [String, nil] the command's result, if there is any.
|
94
129
|
def execute_command(name, event, arguments, chained = false)
|
95
130
|
debug("Executing command #{name} with arguments #{arguments}")
|
96
131
|
command = @commands[name]
|
@@ -107,12 +142,45 @@ module Discordrb::Commands
|
|
107
142
|
end
|
108
143
|
end
|
109
144
|
|
145
|
+
# Executes a command in a simple manner, without command chains or permissions.
|
146
|
+
# @param chain [String] The command with its arguments separated by spaces.
|
147
|
+
# @param event [CommandEvent] The event to pass to the command.
|
148
|
+
# @return [String, nil] the command's result, if there is any.
|
110
149
|
def simple_execute(chain, event)
|
111
150
|
return nil if chain.empty?
|
112
151
|
args = chain.split(' ')
|
113
152
|
execute_command(args[0].to_sym, event, args[1..-1])
|
114
153
|
end
|
115
154
|
|
155
|
+
# Sets the permission level of a user
|
156
|
+
# @param id [Integer] the ID of the user whose level to set
|
157
|
+
# @param level [Integer] the level to set the permission to
|
158
|
+
def set_user_permission(id, level)
|
159
|
+
@permissions[:users][id] = level
|
160
|
+
end
|
161
|
+
|
162
|
+
# Sets the permission level of a role - this applies to all users in the role
|
163
|
+
# @param id [Integer] the ID of the role whose level to set
|
164
|
+
# @param level [Integer] the level to set the permission to
|
165
|
+
def set_role_permission(id, level)
|
166
|
+
@permissions[:roles][id] = level
|
167
|
+
end
|
168
|
+
|
169
|
+
# Check if a user has permission to do something
|
170
|
+
# @param user [User] The user to check
|
171
|
+
# @param level [Integer] The minimum permission level the user should have (inclusive)
|
172
|
+
# @param server [Server] The server on which to check
|
173
|
+
# @return [true, false] whether or not the user has the given permission
|
174
|
+
def permission?(user, level, server)
|
175
|
+
determined_level = server.nil? ? 0 : user.roles[server.id].each.reduce(0) do |memo, role|
|
176
|
+
[@permissions[:roles][role.id] || 0, memo].max
|
177
|
+
end
|
178
|
+
[@permissions[:users][user.id] || 0, determined_level].max >= level
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
# Internal handler for MESSAGE_CREATE that is overwritten to allow for command handling
|
116
184
|
def create_message(data)
|
117
185
|
message = Discordrb::Message.new(data, self)
|
118
186
|
event = CommandEvent.new(message, self)
|
@@ -144,20 +212,5 @@ module Discordrb::Commands
|
|
144
212
|
end
|
145
213
|
end
|
146
214
|
end
|
147
|
-
|
148
|
-
def set_user_permission(id, level)
|
149
|
-
@permissions[:users][id] = level
|
150
|
-
end
|
151
|
-
|
152
|
-
def set_role_permission(id, level)
|
153
|
-
@permissions[:roles][id] = level
|
154
|
-
end
|
155
|
-
|
156
|
-
def permission?(user, level, server)
|
157
|
-
determined_level = server.nil? ? 0 : user.roles[server.id].each.reduce(0) do |memo, role|
|
158
|
-
[@permissions[:roles][role.id] || 0, memo].max
|
159
|
-
end
|
160
|
-
[@permissions[:users][user.id] || 0, determined_level].max >= level
|
161
|
-
end
|
162
215
|
end
|
163
216
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'discordrb/container'
|
2
|
+
require 'discordrb/commands/rate_limiter'
|
3
|
+
|
4
|
+
module Discordrb::Commands
|
5
|
+
# This module holds a collection of commands that can be easily added to by calling the {CommandContainer#command}
|
6
|
+
# function. Other containers can be included into it as well. This allows for modularization of command bots.
|
7
|
+
module CommandContainer
|
8
|
+
include RateLimiter
|
9
|
+
|
10
|
+
# Adds a new command to the container.
|
11
|
+
# @param name [Symbol] The name of the command to add.
|
12
|
+
# @param attributes [Hash] The attributes to initialize the command with.
|
13
|
+
# @option attributes [Integer] :permission_level The minimum permission level that can use this command, inclusive.
|
14
|
+
# See {CommandBot#set_user_permission} and {CommandBot#set_role_permission}.
|
15
|
+
# @option attributes [true, false] :chain_usable Whether this command is able to be used inside of a command chain
|
16
|
+
# or sub-chain. Typically used for administrative commands that shouldn't be done carelessly.
|
17
|
+
# @option attributes [true, false] :help_available Whether this command is visible in the help command. See the
|
18
|
+
# :help_command attribute of {CommandBot#initialize}.
|
19
|
+
# @option attributes [String] :description A short description of what this command does. Will be shown in the help
|
20
|
+
# command if the user asks for it.
|
21
|
+
# @option attributes [String] :usage A short description of how this command should be used. Will be displayed in
|
22
|
+
# the help command or if the user uses it wrong.
|
23
|
+
# @option attributes [Integer] :min_args The minimum number of arguments this command should have. If a user
|
24
|
+
# attempts to call the command with fewer arguments, the usage information will be displayed, if it exists.
|
25
|
+
# @option attributes [Integer] :max_args The maximum number of arguments the command should have.
|
26
|
+
# @option attributes [String] :rate_limit_message The message that should be displayed if the command hits a rate
|
27
|
+
# limit. None if unspecified or nil. %time% in the message will be replaced with the time in seconds when the
|
28
|
+
# command will be available again.
|
29
|
+
# @option attributes [Symbol] :bucket The rate limit bucket that should be used for rate limiting. No rate limiting
|
30
|
+
# will be done if unspecified or nil.
|
31
|
+
# @yield The block is executed when the command is executed.
|
32
|
+
# @yieldparam event [CommandEvent] The event of the message that contained the command.
|
33
|
+
# @return [Command] The command that was added.
|
34
|
+
def command(name, attributes = {}, &block)
|
35
|
+
@commands ||= {}
|
36
|
+
if name.is_a? Array
|
37
|
+
new_command = Command.new(name[0], attributes, &block)
|
38
|
+
name.each { |n| @commands[n] = new_command }
|
39
|
+
new_command
|
40
|
+
else
|
41
|
+
@commands[name] = Command.new(name, attributes, &block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Removes a specific command from this container.
|
46
|
+
# @param name [Symbol] The command to remove.
|
47
|
+
def remove_command(name)
|
48
|
+
@commands ||= {}
|
49
|
+
@commands.delete name
|
50
|
+
end
|
51
|
+
|
52
|
+
# Adds all commands from another container into this one. Existing commands will be overwritten.
|
53
|
+
# @param container [Module] A module that `extend`s {CommandContainer} from which the commands will be added.
|
54
|
+
def include_commands(container)
|
55
|
+
handlers = container.instance_variable_get '@commands'
|
56
|
+
@commands ||= {}
|
57
|
+
@commands.merge! handlers
|
58
|
+
end
|
59
|
+
|
60
|
+
# Includes another container into this one.
|
61
|
+
# @param container [Module] An EventContainer or CommandContainer that will be included if it can.
|
62
|
+
def include!(container)
|
63
|
+
container_modules = container.singleton_class.included_modules
|
64
|
+
|
65
|
+
# If the container is an EventContainer and we can include it, then do that
|
66
|
+
if container_modules.include?(Discordrb::EventContainer) && respond_to?(:include_events)
|
67
|
+
include_events(container)
|
68
|
+
end
|
69
|
+
|
70
|
+
if container_modules.include? Discordrb::Commands::CommandContainer
|
71
|
+
include_commands(container)
|
72
|
+
include_buckets(container)
|
73
|
+
elsif !container_modules.include? Discordrb::EventContainer
|
74
|
+
fail "Could not include! this particular container - ancestors: #{container_modules}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -1,19 +1,9 @@
|
|
1
1
|
require 'discordrb/events/message'
|
2
2
|
|
3
3
|
module Discordrb::Commands
|
4
|
-
# Extension of MessageEvent for commands that contains the command called
|
4
|
+
# Extension of MessageEvent for commands that contains the command called and makes the bot readable
|
5
5
|
class CommandEvent < Discordrb::Events::MessageEvent
|
6
|
-
attr_reader :bot
|
6
|
+
attr_reader :bot
|
7
7
|
attr_accessor :command
|
8
|
-
|
9
|
-
def initialize(message, bot)
|
10
|
-
super(message, bot)
|
11
|
-
@saved_message = ''
|
12
|
-
end
|
13
|
-
|
14
|
-
def <<(message)
|
15
|
-
@saved_message += "#{message}\n"
|
16
|
-
nil
|
17
|
-
end
|
18
8
|
end
|
19
9
|
end
|
@@ -1,8 +1,13 @@
|
|
1
1
|
module Discordrb::Commands
|
2
2
|
# Command that can be called in a chain
|
3
3
|
class Command
|
4
|
-
|
4
|
+
# @return [Hash] the attributes the command was initialized with
|
5
|
+
attr_reader :attributes
|
5
6
|
|
7
|
+
# @return [Symbol] the name of this command
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# @!visibility private
|
6
11
|
def initialize(name, attributes = {}, &block)
|
7
12
|
@name = name
|
8
13
|
@attributes = {
|
@@ -25,12 +30,24 @@ module Discordrb::Commands
|
|
25
30
|
min_args: attributes[:min_args] || 0,
|
26
31
|
|
27
32
|
# Maximum number of arguments (-1 for no limit)
|
28
|
-
max_args: attributes[:max_args] || -1
|
33
|
+
max_args: attributes[:max_args] || -1,
|
34
|
+
|
35
|
+
# Message to display upon rate limiting (%time% in the message for the remaining time until the next possible
|
36
|
+
# request, nil for no message)
|
37
|
+
rate_limit_message: attributes[:rate_limit_message],
|
38
|
+
|
39
|
+
# Rate limiting bucket (nil for no rate limiting)
|
40
|
+
bucket: attributes[:bucket]
|
29
41
|
}
|
30
42
|
|
31
43
|
@block = block
|
32
44
|
end
|
33
45
|
|
46
|
+
# Calls this command and executes the code inside.
|
47
|
+
# @param event [CommandEvent] The event to call the command with.
|
48
|
+
# @param arguments [Array<String>] The attributes for the command.
|
49
|
+
# @param chained [true, false] Whether or not this command is part of a command chain.
|
50
|
+
# @return [String] the result of the execution.
|
34
51
|
def call(event, arguments, chained = false)
|
35
52
|
if arguments.length < @attributes[:min_args]
|
36
53
|
event.respond "Too few arguments for command `#{name}`!"
|
@@ -48,12 +65,26 @@ module Discordrb::Commands
|
|
48
65
|
return
|
49
66
|
end
|
50
67
|
end
|
68
|
+
|
69
|
+
rate_limited = event.bot.rate_limited?(@attributes[:bucket], event.author)
|
70
|
+
if @attributes[:bucket] && rate_limited
|
71
|
+
if @attributes[:rate_limit_message]
|
72
|
+
event.respond @attributes[:rate_limit_message].gsub('%time%', rate_limited.round(2).to_s)
|
73
|
+
end
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
51
77
|
@block.call(event, *arguments)
|
78
|
+
rescue LocalJumpError # occurs when breaking
|
79
|
+
nil
|
52
80
|
end
|
53
81
|
end
|
54
82
|
|
55
83
|
# Command chain, may have multiple commands, nested and commands
|
56
84
|
class CommandChain
|
85
|
+
# @param chain [String] The string the chain should be parsed from.
|
86
|
+
# @param bot [CommandBot] The bot that executes this command chain.
|
87
|
+
# @param subchain [true, false] Whether this chain is a sub chain of another chain.
|
57
88
|
def initialize(chain, bot, subchain = false)
|
58
89
|
@attributes = bot.attributes
|
59
90
|
@chain = chain
|
@@ -61,6 +92,10 @@ module Discordrb::Commands
|
|
61
92
|
@subchain = subchain
|
62
93
|
end
|
63
94
|
|
95
|
+
# Parses the command chain itself, including sub-chains, and executes it. Executes only the command chain, without
|
96
|
+
# its chain arguments.
|
97
|
+
# @param event [CommandEvent] The event to execute the chain with.
|
98
|
+
# @return [String] the result of the execution.
|
64
99
|
def execute_bare(event)
|
65
100
|
b_start = -1
|
66
101
|
b_level = 0
|
@@ -160,6 +195,9 @@ module Discordrb::Commands
|
|
160
195
|
prev
|
161
196
|
end
|
162
197
|
|
198
|
+
# Divides the command chain into chain arguments and command chain, then executes them both.
|
199
|
+
# @param event [CommandEvent] The event to execute the command with.
|
200
|
+
# @return [String] the result of the command chain execution.
|
163
201
|
def execute(event)
|
164
202
|
old_chain = @chain
|
165
203
|
@bot.debug 'Executing bare chain'
|
@@ -181,8 +219,6 @@ module Discordrb::Commands
|
|
181
219
|
|
182
220
|
result = new_result
|
183
221
|
# TODO: more chain arguments
|
184
|
-
else
|
185
|
-
# ignore
|
186
222
|
end
|
187
223
|
end
|
188
224
|
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Discordrb::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
|
+
fail 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 [#resolve_id, 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
|
+
# @return [Integer, false] the waiting time until the next request, in seconds, or false if the request succeeded
|
39
|
+
def rate_limited?(thing, rate_limit_time = nil)
|
40
|
+
key = resolve_key thing
|
41
|
+
limit_hash = @bucket[key]
|
42
|
+
|
43
|
+
# First case: limit_hash doesn't exist yet
|
44
|
+
unless limit_hash
|
45
|
+
@bucket[key] = {
|
46
|
+
last_time: Time.now,
|
47
|
+
set_time: Time.now,
|
48
|
+
count: 1
|
49
|
+
}
|
50
|
+
|
51
|
+
return false
|
52
|
+
end
|
53
|
+
|
54
|
+
# Define the time at which we're being rate limited once so it doesn't get inaccurate
|
55
|
+
rate_limit_time ||= Time.now
|
56
|
+
|
57
|
+
if @limit && (limit_hash[:count] + 1) > @limit
|
58
|
+
if @time_span && rate_limit_time < (limit_hash[:set_time] + @time_span)
|
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
|
61
|
+
else
|
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
|
+
end
|
68
|
+
|
69
|
+
if @delay && rate_limit_time < (limit_hash[:last_time] + @delay)
|
70
|
+
# Fourth case: we're being delayed
|
71
|
+
(limit_hash[:last_time] + @delay) - rate_limit_time
|
72
|
+
else
|
73
|
+
# Fifth case: no rate limiting at all! Increment the count, set the last_time, and return false
|
74
|
+
limit_hash[:last_time] = rate_limit_time
|
75
|
+
limit_hash[:count] += 1
|
76
|
+
false
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def resolve_key(thing)
|
83
|
+
return thing.resolve_id if thing.respond_to?(:resolve_id) && !thing.is_a?(String)
|
84
|
+
return thing if thing.is_a?(Integer) || thing.is_a?(Symbol)
|
85
|
+
fail 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 [#resolve_id, Integer, Symbol] What should be rate-limited.
|
107
|
+
# @see Bucket#rate_limited?
|
108
|
+
# @return [Integer, false] How much time to wait or false if the request succeeded.
|
109
|
+
def rate_limited?(key, thing)
|
110
|
+
# Check whether the bucket actually exists
|
111
|
+
return false unless @buckets && @buckets[key]
|
112
|
+
|
113
|
+
@buckets[key].rate_limited?(thing)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Cleans all buckets
|
117
|
+
# @see Bucket#clean
|
118
|
+
def clean
|
119
|
+
@buckets.each(&:clean)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Adds all the buckets from another RateLimiter onto this one.
|
123
|
+
# @param limiter [Module] Another {RateLimiter} module
|
124
|
+
def include_buckets(limiter)
|
125
|
+
buckets = limiter.instance_variable_get('@buckets') || {}
|
126
|
+
@buckets ||= {}
|
127
|
+
@buckets.merge! buckets
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# This class provides a convenient way to do rate-limiting on non-command events.
|
132
|
+
# @see RateLimiter
|
133
|
+
class SimpleRateLimiter
|
134
|
+
include RateLimiter
|
135
|
+
|
136
|
+
# Makes a new rate limiter
|
137
|
+
def initialize
|
138
|
+
@buckets = {}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|