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,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
|