rubycord 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,519 @@
|
|
1
|
+
require "rubycord/bot"
|
2
|
+
require "rubycord/data"
|
3
|
+
require "rubycord/commands/parser"
|
4
|
+
require "rubycord/commands/events"
|
5
|
+
require "rubycord/commands/container"
|
6
|
+
require "rubycord/commands/rate_limiter"
|
7
|
+
require "time"
|
8
|
+
|
9
|
+
# Specialized bot to run commands
|
10
|
+
|
11
|
+
module Rubycord::Commands
|
12
|
+
# Bot that supports commands and command chains
|
13
|
+
class CommandBot < Rubycord::Bot
|
14
|
+
# @return [Hash] this bot's attributes.
|
15
|
+
attr_reader :attributes
|
16
|
+
|
17
|
+
# @return [String, Array<String>, #call] the prefix commands are triggered with.
|
18
|
+
# @see #initialize
|
19
|
+
attr_reader :prefix
|
20
|
+
|
21
|
+
include CommandContainer
|
22
|
+
|
23
|
+
# Creates a new CommandBot and logs in to Discord.
|
24
|
+
# @param attributes [Hash] The attributes to initialize the CommandBot with.
|
25
|
+
# @see Rubycord::Bot#initialize Rubycord::Bot#initialize for other attributes that should be used to create the underlying regular bot.
|
26
|
+
# @option attributes [String, Array<String>, #call] :prefix The prefix that should trigger this bot's commands. It
|
27
|
+
# can be:
|
28
|
+
#
|
29
|
+
# * Any string (including the empty string). This has the effect that if a message starts with the prefix, the
|
30
|
+
# prefix will be stripped and the rest of the chain will be parsed as a command chain. Note that it will be
|
31
|
+
# literal - if the prefix is "hi" then the corresponding trigger string for a command called "test" would be
|
32
|
+
# "hitest". Don't forget to put spaces in if you need them!
|
33
|
+
# * An array of prefixes. Those will behave similarly to setting one string as a prefix, but instead of only one
|
34
|
+
# string, any of the strings in the array can be used.
|
35
|
+
# * Something Proc-like (responds to :call) that takes a {Message} object as an argument and returns either
|
36
|
+
# the command chain in raw form or `nil` if the given message shouldn't be parsed. This can be used to make more
|
37
|
+
# complicated dynamic prefixes (e. g. based on server), or even something else entirely (suffixes, or most
|
38
|
+
# adventurous, infixes).
|
39
|
+
# @option attributes [true, false] :advanced_functionality Whether to enable advanced functionality (very powerful
|
40
|
+
# way to nest commands into chains, see https://github.com/dakurei-gems/rubycord/wiki/Commands#command-chain-syntax
|
41
|
+
# for info. Default is false.
|
42
|
+
# @option attributes [Symbol, Array<Symbol>, false] :help_command The name of the command that displays info for
|
43
|
+
# other commands. Use an array if you want to have aliases. Default is "help". If none should be created, use
|
44
|
+
# `false` as the value.
|
45
|
+
# @option attributes [String, #call] :command_doesnt_exist_message The message that should be displayed if a user attempts
|
46
|
+
# to use a command that does not exist. If none is specified, no message will be displayed. In the message, you
|
47
|
+
# can use the string '%command%' that will be replaced with the name of the command. Anything responding to call
|
48
|
+
# such as a proc will be called with the event, and is expected to return a String or nil.
|
49
|
+
# @option attributes [String] :no_permission_message The message to be displayed when `NoPermission` error is raised.
|
50
|
+
# @option attributes [true, false] :spaces_allowed Whether spaces are allowed to occur between the prefix and the
|
51
|
+
# command. Default is false.
|
52
|
+
# @option attributes [true, false] :webhook_commands Whether messages sent by webhooks are allowed to trigger
|
53
|
+
# commands. Default is true.
|
54
|
+
# @option attributes [Array<String, Integer, Channel>] :channels The channels this command bot accepts commands on.
|
55
|
+
# Superseded if a command has a 'channels' attribute.
|
56
|
+
# @option attributes [String] :previous Character that should designate the result of the previous command in
|
57
|
+
# a command chain (see :advanced_functionality). Default is '~'. Set to an empty string to disable.
|
58
|
+
# @option attributes [String] :chain_delimiter Character that should designate that a new command begins in the
|
59
|
+
# command chain (see :advanced_functionality). Default is '>'. Set to an empty string to disable.
|
60
|
+
# @option attributes [String] :chain_args_delim Character that should separate the command chain arguments from the
|
61
|
+
# chain itself (see :advanced_functionality). Default is ':'. Set to an empty string to disable.
|
62
|
+
# @option attributes [String] :sub_chain_start Character that should start a sub-chain (see
|
63
|
+
# :advanced_functionality). Default is '['. Set to an empty string to disable.
|
64
|
+
# @option attributes [String] :sub_chain_end Character that should end a sub-chain (see
|
65
|
+
# :advanced_functionality). Default is ']'. Set to an empty string to disable.
|
66
|
+
# @option attributes [String] :quote_start Character that should start a quoted string (see
|
67
|
+
# :advanced_functionality). Default is '"'. Set to an empty string to disable.
|
68
|
+
# @option attributes [String] :quote_end Character that should end a quoted string (see
|
69
|
+
# :advanced_functionality). Default is '"' or the same as :quote_start. Set to an empty string to disable.
|
70
|
+
# @option attributes [true, false] :ignore_bots Whether the bot should ignore bot accounts or not. Default is false.
|
71
|
+
def initialize(**attributes)
|
72
|
+
# TODO: This needs to be revisited. undefined attributes are treated
|
73
|
+
# as explicitly passed nils.
|
74
|
+
super(
|
75
|
+
log_mode: attributes[:log_mode],
|
76
|
+
token: attributes[:token],
|
77
|
+
client_id: attributes[:client_id],
|
78
|
+
type: attributes[:type],
|
79
|
+
name: attributes[:name],
|
80
|
+
fancy_log: attributes[:fancy_log],
|
81
|
+
suppress_ready: attributes[:suppress_ready],
|
82
|
+
parse_self: attributes[:parse_self],
|
83
|
+
shard_id: attributes[:shard_id],
|
84
|
+
num_shards: attributes[:num_shards],
|
85
|
+
redact_token: attributes.key?(:redact_token) ? attributes[:redact_token] : true,
|
86
|
+
ignore_bots: attributes[:ignore_bots],
|
87
|
+
compress_mode: attributes[:compress_mode],
|
88
|
+
intents: attributes[:intents] || :all
|
89
|
+
)
|
90
|
+
|
91
|
+
@prefix = attributes[:prefix]
|
92
|
+
@attributes = {
|
93
|
+
# Whether advanced functionality such as command chains are enabled
|
94
|
+
advanced_functionality: attributes[:advanced_functionality].nil? ? false : attributes[:advanced_functionality],
|
95
|
+
|
96
|
+
# The name of the help command (that displays information to other commands). False if none should exist
|
97
|
+
help_command: attributes[:help_command].is_a?(FalseClass) ? nil : (attributes[:help_command] || :help),
|
98
|
+
|
99
|
+
# The message to display for when a command doesn't exist, %command% to get the command name in question and nil for no message
|
100
|
+
# No default value here because it may not be desired behaviour
|
101
|
+
command_doesnt_exist_message: attributes[:command_doesnt_exist_message],
|
102
|
+
|
103
|
+
# The message to be displayed when `NoPermission` error is raised.
|
104
|
+
no_permission_message: attributes[:no_permission_message],
|
105
|
+
|
106
|
+
# Spaces allowed between prefix and command
|
107
|
+
spaces_allowed: attributes[:spaces_allowed].nil? ? false : attributes[:spaces_allowed],
|
108
|
+
|
109
|
+
# Webhooks allowed to trigger commands
|
110
|
+
webhook_commands: attributes[:webhook_commands].nil? ? true : attributes[:webhook_commands],
|
111
|
+
|
112
|
+
channels: attributes[:channels] || [],
|
113
|
+
|
114
|
+
# All of the following need to be one character
|
115
|
+
# String to designate previous result in command chain
|
116
|
+
previous: attributes[:previous] || "~",
|
117
|
+
|
118
|
+
# Command chain delimiter
|
119
|
+
chain_delimiter: attributes[:chain_delimiter] || ">",
|
120
|
+
|
121
|
+
# Chain argument delimiter
|
122
|
+
chain_args_delim: attributes[:chain_args_delim] || ":",
|
123
|
+
|
124
|
+
# Sub-chain starting character
|
125
|
+
sub_chain_start: attributes[:sub_chain_start] || "[",
|
126
|
+
|
127
|
+
# Sub-chain ending character
|
128
|
+
sub_chain_end: attributes[:sub_chain_end] || "]",
|
129
|
+
|
130
|
+
# Quoted mode starting character
|
131
|
+
quote_start: attributes[:quote_start] || '"',
|
132
|
+
|
133
|
+
# Quoted mode ending character
|
134
|
+
quote_end: attributes[:quote_end] || attributes[:quote_start] || '"',
|
135
|
+
|
136
|
+
# Default block for handling internal exceptions, or a string to respond with
|
137
|
+
rescue: attributes[:rescue]
|
138
|
+
}
|
139
|
+
|
140
|
+
@permissions = {
|
141
|
+
roles: {},
|
142
|
+
users: {}
|
143
|
+
}
|
144
|
+
|
145
|
+
return unless @attributes[:help_command]
|
146
|
+
|
147
|
+
command(@attributes[:help_command], max_args: 1, description: "Shows a list of all the commands available or displays help for a specific command.", usage: "help [command name]") do |event, command_name|
|
148
|
+
if command_name
|
149
|
+
command = @commands[command_name.to_sym]
|
150
|
+
if command.is_a?(CommandAlias)
|
151
|
+
command = command.aliased_command
|
152
|
+
command_name = command.name
|
153
|
+
end
|
154
|
+
return "The command `#{command_name}` does not exist!" unless command
|
155
|
+
|
156
|
+
desc = command.attributes[:description] || "*No description available*"
|
157
|
+
usage = command.attributes[:usage]
|
158
|
+
parameters = command.attributes[:parameters]
|
159
|
+
result = "**`#{command_name}`**: #{desc}"
|
160
|
+
aliases = command_aliases(command_name.to_sym)
|
161
|
+
unless aliases.empty?
|
162
|
+
result += "\nAliases: "
|
163
|
+
result += aliases.map { |a| "`#{a.name}`" }.join(", ")
|
164
|
+
end
|
165
|
+
result += "\nUsage: `#{usage}`" if usage
|
166
|
+
if parameters
|
167
|
+
result += "\nAccepted Parameters:\n```"
|
168
|
+
parameters.each { |p| result += "\n#{p}" }
|
169
|
+
result += "```"
|
170
|
+
end
|
171
|
+
result
|
172
|
+
else
|
173
|
+
available_commands = @commands.values.reject do |c|
|
174
|
+
c.is_a?(CommandAlias) || !c.attributes[:help_available] || !required_roles?(event.user, c.attributes[:required_roles]) || !allowed_roles?(event.user, c.attributes[:allowed_roles]) || !required_permissions?(event.user, c.attributes[:required_permissions], event.channel)
|
175
|
+
end
|
176
|
+
case available_commands.length
|
177
|
+
when 0..5
|
178
|
+
available_commands.reduce "**List of commands:**\n" do |memo, c|
|
179
|
+
memo + "**`#{c.name}`**: #{c.attributes[:description] || "*No description available*"}\n"
|
180
|
+
end
|
181
|
+
when 5..50
|
182
|
+
(available_commands.reduce "**List of commands:**\n" do |memo, c|
|
183
|
+
memo + "`#{c.name}`, "
|
184
|
+
end)[0..-3]
|
185
|
+
else
|
186
|
+
event.user.pm(available_commands.reduce("**List of commands:**\n") { |m, e| m + "`#{e.name}`, " }[0..-3])
|
187
|
+
event.channel.pm? ? "" : "Sending list in PM!"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns all aliases for the command with the given name
|
194
|
+
# @param name [Symbol] the name of the `Command`
|
195
|
+
# @return [Array<CommandAlias>]
|
196
|
+
def command_aliases(name)
|
197
|
+
commands.values.select do |command|
|
198
|
+
command.is_a?(CommandAlias) && command.aliased_command.name == name
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Executes a particular command on the bot. Mostly useful for internal stuff, but one can never know.
|
203
|
+
# @param name [Symbol] The command to execute.
|
204
|
+
# @param event [CommandEvent] The event to pass to the command.
|
205
|
+
# @param arguments [Array<String>] The arguments to pass to the command.
|
206
|
+
# @param chained [true, false] Whether or not it should be executed as part of a command chain. If this is false,
|
207
|
+
# commands that have chain_usable set to false will not work.
|
208
|
+
# @param check_permissions [true, false] Whether permission parameters such as `required_permission` or
|
209
|
+
# `permission_level` should be checked.
|
210
|
+
# @return [String, nil] the command's result, if there is any.
|
211
|
+
def execute_command(name, event, arguments, chained = false, check_permissions = true)
|
212
|
+
debug("Executing command #{name} with arguments #{arguments}")
|
213
|
+
return unless @commands
|
214
|
+
|
215
|
+
command = @commands[name]
|
216
|
+
command = command.aliased_command if command.is_a?(CommandAlias)
|
217
|
+
return if check_permissions && !channels?(event.channel, @attributes[:channels]) &&
|
218
|
+
(!command || command.attributes[:channels].nil?)
|
219
|
+
|
220
|
+
unless command
|
221
|
+
if @attributes[:command_doesnt_exist_message]
|
222
|
+
message = @attributes[:command_doesnt_exist_message]
|
223
|
+
message = message.call(event) if message.respond_to?(:call)
|
224
|
+
event.respond message.gsub("%command%", name.to_s) if message
|
225
|
+
end
|
226
|
+
return
|
227
|
+
end
|
228
|
+
return unless !check_permissions || channels?(event.channel, command.attributes[:channels])
|
229
|
+
|
230
|
+
arguments = arg_check(arguments, command.attributes[:arg_types], event.server) if check_permissions
|
231
|
+
if (check_permissions &&
|
232
|
+
permission?(event.author, command.attributes[:permission_level], event.server) &&
|
233
|
+
required_permissions?(event.author, command.attributes[:required_permissions], event.channel) &&
|
234
|
+
required_roles?(event.author, command.attributes[:required_roles]) &&
|
235
|
+
allowed_roles?(event.author, command.attributes[:allowed_roles])) ||
|
236
|
+
!check_permissions
|
237
|
+
event.command = command
|
238
|
+
result = command.call(event, arguments, chained, check_permissions)
|
239
|
+
stringify(result)
|
240
|
+
else
|
241
|
+
event.respond command.attributes[:permission_message].gsub("%name%", name.to_s) if command.attributes[:permission_message]
|
242
|
+
nil
|
243
|
+
end
|
244
|
+
rescue Rubycord::Errors::NoPermission
|
245
|
+
event.respond @attributes[:no_permission_message] unless @attributes[:no_permission_message].nil?
|
246
|
+
raise
|
247
|
+
end
|
248
|
+
|
249
|
+
# Transforms an array of string arguments based on types array.
|
250
|
+
# For example, `['1', '10..14']` with types `[Integer, Range]` would turn into `[1, 10..14]`.
|
251
|
+
def arg_check(args, types = nil, server = nil)
|
252
|
+
return args unless types
|
253
|
+
|
254
|
+
args.each_with_index.map do |arg, i|
|
255
|
+
next arg if types[i].nil? || types[i] == String
|
256
|
+
|
257
|
+
if types[i] == Integer
|
258
|
+
begin
|
259
|
+
Integer(arg, 10)
|
260
|
+
rescue ArgumentError
|
261
|
+
nil
|
262
|
+
end
|
263
|
+
elsif types[i] == Float
|
264
|
+
begin
|
265
|
+
Float(arg)
|
266
|
+
rescue ArgumentError
|
267
|
+
nil
|
268
|
+
end
|
269
|
+
elsif types[i] == Time
|
270
|
+
begin
|
271
|
+
Time.parse arg
|
272
|
+
rescue ArgumentError
|
273
|
+
nil
|
274
|
+
end
|
275
|
+
elsif types[i] == TrueClass || types[i] == FalseClass
|
276
|
+
if arg.casecmp("true").zero? || arg.downcase.start_with?("y")
|
277
|
+
true
|
278
|
+
elsif arg.casecmp("false").zero? || arg.downcase.start_with?("n")
|
279
|
+
false
|
280
|
+
end
|
281
|
+
elsif types[i] == Symbol
|
282
|
+
arg.to_sym
|
283
|
+
elsif types[i] == Encoding
|
284
|
+
begin
|
285
|
+
Encoding.find arg
|
286
|
+
rescue ArgumentError
|
287
|
+
nil
|
288
|
+
end
|
289
|
+
elsif types[i] == Regexp
|
290
|
+
begin
|
291
|
+
Regexp.new arg
|
292
|
+
rescue ArgumentError
|
293
|
+
nil
|
294
|
+
end
|
295
|
+
elsif types[i] == Rational
|
296
|
+
begin
|
297
|
+
Rational(arg)
|
298
|
+
rescue ArgumentError
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
elsif types[i] == Range
|
302
|
+
begin
|
303
|
+
if arg.include? "..."
|
304
|
+
Range.new(*arg.split("...").map(&:to_i), true)
|
305
|
+
elsif arg.include? ".."
|
306
|
+
Range.new(*arg.split("..").map(&:to_i))
|
307
|
+
end
|
308
|
+
rescue ArgumentError
|
309
|
+
nil
|
310
|
+
end
|
311
|
+
elsif types[i] == NilClass
|
312
|
+
nil
|
313
|
+
elsif [Rubycord::User, Rubycord::Role, Rubycord::Emoji].include? types[i]
|
314
|
+
result = parse_mention arg, server
|
315
|
+
result if result.instance_of? types[i]
|
316
|
+
elsif types[i] == Rubycord::Invite
|
317
|
+
resolve_invite_code arg
|
318
|
+
elsif types[i].respond_to?(:from_argument)
|
319
|
+
begin
|
320
|
+
types[i].from_argument arg
|
321
|
+
rescue
|
322
|
+
nil
|
323
|
+
end
|
324
|
+
else
|
325
|
+
raise ArgumentError, "#{types[i]} doesn't implement from_argument"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Executes a command in a simple manner, without command chains or permissions.
|
331
|
+
# @param chain [String] The command with its arguments separated by spaces.
|
332
|
+
# @param event [CommandEvent] The event to pass to the command.
|
333
|
+
# @return [String, nil] the command's result, if there is any.
|
334
|
+
def simple_execute(chain, event)
|
335
|
+
return nil if chain.empty?
|
336
|
+
|
337
|
+
args = chain.split(" ")
|
338
|
+
execute_command(args[0].to_sym, event, args[1..])
|
339
|
+
end
|
340
|
+
|
341
|
+
# Sets the permission level of a user
|
342
|
+
# @param id [Integer] the ID of the user whose level to set
|
343
|
+
# @param level [Integer] the level to set the permission to
|
344
|
+
def set_user_permission(id, level)
|
345
|
+
@permissions[:users][id] = level
|
346
|
+
end
|
347
|
+
|
348
|
+
# Sets the permission level of a role - this applies to all users in the role
|
349
|
+
# @param id [Integer] the ID of the role whose level to set
|
350
|
+
# @param level [Integer] the level to set the permission to
|
351
|
+
def set_role_permission(id, level)
|
352
|
+
@permissions[:roles][id] = level
|
353
|
+
end
|
354
|
+
|
355
|
+
# Check if a user has permission to do something
|
356
|
+
# @param user [User] The user to check
|
357
|
+
# @param level [Integer] The minimum permission level the user should have (inclusive)
|
358
|
+
# @param server [Server] The server on which to check
|
359
|
+
# @return [true, false] whether or not the user has the given permission
|
360
|
+
def permission?(user, level, server)
|
361
|
+
determined_level = if user.webhook? || server.nil?
|
362
|
+
0
|
363
|
+
else
|
364
|
+
user.roles.reduce(0) do |memo, role|
|
365
|
+
[@permissions[:roles][role.id] || 0, memo].max
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
[@permissions[:users][user.id] || 0, determined_level].max >= level
|
370
|
+
end
|
371
|
+
|
372
|
+
# @see CommandBot#update_channels
|
373
|
+
def channels=(channels)
|
374
|
+
update_channels(channels)
|
375
|
+
end
|
376
|
+
|
377
|
+
# Update the list of channels the bot accepts commands from.
|
378
|
+
# @param channels [Array<String, Integer, Channel>] The channels this command bot accepts commands on.
|
379
|
+
def update_channels(channels = [])
|
380
|
+
@attributes[:channels] = Array(channels)
|
381
|
+
end
|
382
|
+
|
383
|
+
# Add a channel to the list of channels the bot accepts commands from.
|
384
|
+
# @param channel [String, Integer, Channel] The channel name, integer ID, or `Channel` object to be added
|
385
|
+
def add_channel(channel)
|
386
|
+
return if @attributes[:channels].find { |c| channel.resolve_id == c.resolve_id }
|
387
|
+
|
388
|
+
@attributes[:channels] << channel
|
389
|
+
end
|
390
|
+
|
391
|
+
# Remove a channel from the list of channels the bot accepts commands from.
|
392
|
+
# @param channel [String, Integer, Channel] The channel name, integer ID, or `Channel` object to be removed
|
393
|
+
def remove_channel(channel)
|
394
|
+
@attributes[:channels].delete_if { |c| channel.resolve_id == c.resolve_id }
|
395
|
+
end
|
396
|
+
|
397
|
+
private
|
398
|
+
|
399
|
+
# Internal handler for MESSAGE_CREATE that is overwritten to allow for command handling
|
400
|
+
def create_message(data)
|
401
|
+
message = Rubycord::Message.new(data, self)
|
402
|
+
return message if message.from_bot? && !@should_parse_self
|
403
|
+
return message if message.webhook? && !@attributes[:webhook_commands]
|
404
|
+
|
405
|
+
unless message.author
|
406
|
+
Rubycord::LOGGER.warn("Received a message (#{message.inspect}) with nil author! Ignoring, please report this if you can")
|
407
|
+
return
|
408
|
+
end
|
409
|
+
|
410
|
+
event = CommandEvent.new(message, self)
|
411
|
+
|
412
|
+
chain = trigger?(message)
|
413
|
+
return message unless chain
|
414
|
+
|
415
|
+
# Don't allow spaces between the prefix and the command
|
416
|
+
if chain.start_with?(" ") && !@attributes[:spaces_allowed]
|
417
|
+
debug("Chain starts with a space")
|
418
|
+
return message
|
419
|
+
end
|
420
|
+
|
421
|
+
if chain.strip.empty?
|
422
|
+
debug("Chain is empty")
|
423
|
+
return message
|
424
|
+
end
|
425
|
+
|
426
|
+
execute_chain(chain, event)
|
427
|
+
|
428
|
+
# Return the message so it doesn't get parsed again during the rest of the dispatch handling
|
429
|
+
message
|
430
|
+
end
|
431
|
+
|
432
|
+
# Check whether a message should trigger command execution, and if it does, return the raw chain
|
433
|
+
def trigger?(message)
|
434
|
+
if @prefix.is_a? String
|
435
|
+
standard_prefix_trigger(message.content, @prefix)
|
436
|
+
elsif @prefix.is_a? Array
|
437
|
+
@prefix.map { |e| standard_prefix_trigger(message.content, e) }.reduce { |m, e| m || e }
|
438
|
+
elsif @prefix.respond_to? :call
|
439
|
+
@prefix.call(message)
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
def standard_prefix_trigger(message, prefix)
|
444
|
+
return nil unless message.start_with? prefix
|
445
|
+
|
446
|
+
message[prefix.length..]
|
447
|
+
end
|
448
|
+
|
449
|
+
def required_permissions?(member, required, channel = nil)
|
450
|
+
required.reduce(true) do |a, action|
|
451
|
+
a && !member.webhook? && !member.is_a?(Rubycord::Recipient) && member.permission?(action, channel)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
def required_roles?(member, required)
|
456
|
+
return true if member.webhook? || member.is_a?(Rubycord::Recipient) || required.nil? || required.empty?
|
457
|
+
|
458
|
+
required.is_a?(Array) ? check_multiple_roles(member, required) : member.role?(role)
|
459
|
+
end
|
460
|
+
|
461
|
+
def allowed_roles?(member, required)
|
462
|
+
return true if member.webhook? || member.is_a?(Rubycord::Recipient) || required.nil? || required.empty?
|
463
|
+
|
464
|
+
required.is_a?(Array) ? check_multiple_roles(member, required, false) : member.role?(role)
|
465
|
+
end
|
466
|
+
|
467
|
+
def check_multiple_roles(member, required, all_roles = true)
|
468
|
+
if all_roles
|
469
|
+
required.all? do |role|
|
470
|
+
member.role?(role)
|
471
|
+
end
|
472
|
+
else
|
473
|
+
required.any? do |role|
|
474
|
+
member.role?(role)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
def channels?(channel, channels)
|
480
|
+
return true if channels.nil? || channels.empty?
|
481
|
+
|
482
|
+
channels.any? do |c|
|
483
|
+
# if c is string, make sure to remove the "#" from channel names in case it was specified
|
484
|
+
return true if c.is_a?(String) && c.delete("#") == channel.name
|
485
|
+
|
486
|
+
c.resolve_id == channel.resolve_id
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
def execute_chain(chain, event)
|
491
|
+
t = Thread.new do
|
492
|
+
@event_threads << t
|
493
|
+
Thread.current[:rubycord_name] = "ct-#{@current_thread += 1}"
|
494
|
+
begin
|
495
|
+
debug("Parsing command chain #{chain}")
|
496
|
+
result = @attributes[:advanced_functionality] ? CommandChain.new(chain, self).execute(event) : simple_execute(chain, event)
|
497
|
+
result = event.drain_into(result)
|
498
|
+
|
499
|
+
if event.file
|
500
|
+
event.send_file(event.file, caption: result)
|
501
|
+
else
|
502
|
+
event.respond result unless result.nil? || result.empty?
|
503
|
+
end
|
504
|
+
rescue => e
|
505
|
+
log_exception(e)
|
506
|
+
ensure
|
507
|
+
@event_threads.delete(t)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# Turns the object into a string, using to_s by default
|
513
|
+
def stringify(object)
|
514
|
+
return "" if object.is_a? Rubycord::Message
|
515
|
+
|
516
|
+
object.to_s
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "rubycord/container"
|
2
|
+
require "rubycord/commands/rate_limiter"
|
3
|
+
|
4
|
+
module Rubycord::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
|
+
# @return [Hash<Symbol, Command, CommandAlias>] hash of command names and commands this container has.
|
11
|
+
attr_reader :commands
|
12
|
+
|
13
|
+
# Adds a new command to the container.
|
14
|
+
# @param name [Symbol] The name of the command to add.
|
15
|
+
# @param attributes [Hash] The attributes to initialize the command with.
|
16
|
+
# @option attributes [Array<Symbol>] :aliases A list of additional names for this command. This in effect
|
17
|
+
# creates {CommandAlias} objects in the container ({#commands}) that refer to the newly created command.
|
18
|
+
# Additionally, the default help command will identify these command names as an alias where applicable.
|
19
|
+
# @option attributes [Integer] :permission_level The minimum permission level that can use this command, inclusive.
|
20
|
+
# See {CommandBot#set_user_permission} and {CommandBot#set_role_permission}.
|
21
|
+
# @option attributes [String, false] :permission_message Message to display when a user does not have sufficient
|
22
|
+
# permissions to execute a command. %name% in the message will be replaced with the name of the command. Disable
|
23
|
+
# the message by setting this option to false.
|
24
|
+
# @option attributes [Array<Symbol>] :required_permissions Discord action permissions (e.g. `:kick_members`) that
|
25
|
+
# should be required to use this command. See {Rubycord::Permissions::FLAGS} for a list.
|
26
|
+
# @option attributes [Array<Role>, Array<String, Integer>] :required_roles Roles, or their IDs, that user must have to use this command
|
27
|
+
# (user must have all of them).
|
28
|
+
# @option attributes [Array<Role>, Array<String, Integer>] :allowed_roles Roles, or their IDs, that user should have to use this command
|
29
|
+
# (user should have at least one of them).
|
30
|
+
# @option attributes [Array<String, Integer, Channel>] :channels The channels that this command can be used on. An
|
31
|
+
# empty array indicates it can be used on any channel. Supersedes the command bot attribute.
|
32
|
+
# @option attributes [true, false] :chain_usable Whether this command is able to be used inside of a command chain
|
33
|
+
# or sub-chain. Typically used for administrative commands that shouldn't be done carelessly.
|
34
|
+
# @option attributes [true, false] :help_available Whether this command is visible in the help command. See the
|
35
|
+
# :help_command attribute of {CommandBot#initialize}.
|
36
|
+
# @option attributes [String] :description A short description of what this command does. Will be shown in the help
|
37
|
+
# command if the user asks for it.
|
38
|
+
# @option attributes [String] :usage A short description of how this command should be used. Will be displayed in
|
39
|
+
# the help command or if the user uses it wrong.
|
40
|
+
# @option attributes [Array<Class>] :arg_types An array of argument classes which will be used for type-checking.
|
41
|
+
# Hard-coded for some native classes, but can be used with any class that implements static
|
42
|
+
# method `from_argument`.
|
43
|
+
# @option attributes [Integer] :min_args The minimum number of arguments this command should have. If a user
|
44
|
+
# attempts to call the command with fewer arguments, the usage information will be displayed, if it exists.
|
45
|
+
# @option attributes [Integer] :max_args The maximum number of arguments the command should have.
|
46
|
+
# @option attributes [String] :rate_limit_message The message that should be displayed if the command hits a rate
|
47
|
+
# limit. None if unspecified or nil. %time% in the message will be replaced with the time in seconds when the
|
48
|
+
# command will be available again.
|
49
|
+
# @option attributes [Symbol] :bucket The rate limit bucket that should be used for rate limiting. No rate limiting
|
50
|
+
# will be done if unspecified or nil.
|
51
|
+
# @option attributes [String, #call] :rescue A string to respond with, or a block to be called in the event an exception
|
52
|
+
# is raised internally. If given a String, `%exception%` will be substituted with the exception's `#message`. If given
|
53
|
+
# a `Proc`, it will be passed the `CommandEvent` along with the `Exception`.
|
54
|
+
# @yield The block is executed when the command is executed.
|
55
|
+
# @yieldparam event [CommandEvent] The event of the message that contained the command.
|
56
|
+
# @note `LocalJumpError`s are rescued from internally, giving bots the opportunity to use `return` or `break` in
|
57
|
+
# their blocks without propagating an exception.
|
58
|
+
# @return [Command] The command that was added.
|
59
|
+
def command(name, attributes = {}, &)
|
60
|
+
@commands ||= {}
|
61
|
+
|
62
|
+
# TODO: Remove in 4.0
|
63
|
+
if name.is_a?(Array)
|
64
|
+
name, *aliases = name
|
65
|
+
attributes[:aliases] = aliases if attributes[:aliases].nil?
|
66
|
+
Rubycord::LOGGER.warn("While registering command #{name.inspect}")
|
67
|
+
Rubycord::LOGGER.warn("Arrays for command aliases is removed. Please use `aliases` argument instead.")
|
68
|
+
end
|
69
|
+
|
70
|
+
new_command = Command.new(name, attributes, &)
|
71
|
+
new_command.attributes[:aliases].each do |aliased_name|
|
72
|
+
@commands[aliased_name] = CommandAlias.new(aliased_name, new_command)
|
73
|
+
end
|
74
|
+
@commands[name] = new_command
|
75
|
+
end
|
76
|
+
|
77
|
+
# Removes a specific command from this container.
|
78
|
+
# @param name [Symbol] The command to remove.
|
79
|
+
def remove_command(name)
|
80
|
+
@commands ||= {}
|
81
|
+
@commands.delete name
|
82
|
+
end
|
83
|
+
|
84
|
+
# Adds all commands from another container into this one. Existing commands will be overwritten.
|
85
|
+
# @param container [Module] A module that `extend`s {CommandContainer} from which the commands will be added.
|
86
|
+
def include_commands(container)
|
87
|
+
handlers = container.instance_variable_get :@commands
|
88
|
+
return unless handlers
|
89
|
+
|
90
|
+
@commands ||= {}
|
91
|
+
@commands.merge! handlers
|
92
|
+
end
|
93
|
+
|
94
|
+
# Includes another container into this one.
|
95
|
+
# @param container [Module] An EventContainer or CommandContainer that will be included if it can.
|
96
|
+
def include!(container)
|
97
|
+
container_modules = container.singleton_class.included_modules
|
98
|
+
|
99
|
+
# If the container is an EventContainer and we can include it, then do that
|
100
|
+
include_events(container) if container_modules.include?(Rubycord::EventContainer) && respond_to?(:include_events)
|
101
|
+
|
102
|
+
if container_modules.include? Rubycord::Commands::CommandContainer
|
103
|
+
include_commands(container)
|
104
|
+
include_buckets(container)
|
105
|
+
elsif !container_modules.include? Rubycord::EventContainer
|
106
|
+
raise "Could not include! this particular container - ancestors: #{container_modules}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require "rubycord/events/message"
|
2
|
+
|
3
|
+
module Rubycord::Commands
|
4
|
+
# Extension of MessageEvent for commands that contains the command called and makes the bot readable
|
5
|
+
class CommandEvent < Rubycord::Events::MessageEvent
|
6
|
+
attr_reader :bot
|
7
|
+
attr_accessor :command
|
8
|
+
end
|
9
|
+
end
|