rubycord 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/lib/rubycord/allowed_mentions.rb +34 -0
  3. data/lib/rubycord/api/application.rb +200 -0
  4. data/lib/rubycord/api/channel.rb +597 -0
  5. data/lib/rubycord/api/interaction.rb +52 -0
  6. data/lib/rubycord/api/invite.rb +42 -0
  7. data/lib/rubycord/api/server.rb +557 -0
  8. data/lib/rubycord/api/user.rb +153 -0
  9. data/lib/rubycord/api/webhook.rb +138 -0
  10. data/lib/rubycord/api.rb +356 -0
  11. data/lib/rubycord/await.rb +49 -0
  12. data/lib/rubycord/bot.rb +1757 -0
  13. data/lib/rubycord/cache.rb +259 -0
  14. data/lib/rubycord/colour_rgb.rb +41 -0
  15. data/lib/rubycord/commands/command_bot.rb +519 -0
  16. data/lib/rubycord/commands/container.rb +110 -0
  17. data/lib/rubycord/commands/events.rb +9 -0
  18. data/lib/rubycord/commands/parser.rb +325 -0
  19. data/lib/rubycord/commands/rate_limiter.rb +142 -0
  20. data/lib/rubycord/container.rb +753 -0
  21. data/lib/rubycord/data/activity.rb +269 -0
  22. data/lib/rubycord/data/application.rb +48 -0
  23. data/lib/rubycord/data/attachment.rb +109 -0
  24. data/lib/rubycord/data/audit_logs.rb +343 -0
  25. data/lib/rubycord/data/channel.rb +996 -0
  26. data/lib/rubycord/data/component.rb +227 -0
  27. data/lib/rubycord/data/embed.rb +249 -0
  28. data/lib/rubycord/data/emoji.rb +80 -0
  29. data/lib/rubycord/data/integration.rb +120 -0
  30. data/lib/rubycord/data/interaction.rb +798 -0
  31. data/lib/rubycord/data/invite.rb +135 -0
  32. data/lib/rubycord/data/member.rb +370 -0
  33. data/lib/rubycord/data/message.rb +412 -0
  34. data/lib/rubycord/data/overwrite.rb +106 -0
  35. data/lib/rubycord/data/profile.rb +89 -0
  36. data/lib/rubycord/data/reaction.rb +31 -0
  37. data/lib/rubycord/data/recipient.rb +32 -0
  38. data/lib/rubycord/data/role.rb +246 -0
  39. data/lib/rubycord/data/server.rb +1002 -0
  40. data/lib/rubycord/data/user.rb +261 -0
  41. data/lib/rubycord/data/voice_region.rb +43 -0
  42. data/lib/rubycord/data/voice_state.rb +39 -0
  43. data/lib/rubycord/data/webhook.rb +232 -0
  44. data/lib/rubycord/data.rb +40 -0
  45. data/lib/rubycord/errors.rb +737 -0
  46. data/lib/rubycord/events/await.rb +46 -0
  47. data/lib/rubycord/events/bans.rb +58 -0
  48. data/lib/rubycord/events/channels.rb +186 -0
  49. data/lib/rubycord/events/generic.rb +126 -0
  50. data/lib/rubycord/events/guilds.rb +191 -0
  51. data/lib/rubycord/events/interactions.rb +480 -0
  52. data/lib/rubycord/events/invites.rb +123 -0
  53. data/lib/rubycord/events/lifetime.rb +29 -0
  54. data/lib/rubycord/events/members.rb +91 -0
  55. data/lib/rubycord/events/message.rb +337 -0
  56. data/lib/rubycord/events/presence.rb +127 -0
  57. data/lib/rubycord/events/raw.rb +45 -0
  58. data/lib/rubycord/events/reactions.rb +156 -0
  59. data/lib/rubycord/events/roles.rb +86 -0
  60. data/lib/rubycord/events/threads.rb +94 -0
  61. data/lib/rubycord/events/typing.rb +70 -0
  62. data/lib/rubycord/events/voice_server_update.rb +45 -0
  63. data/lib/rubycord/events/voice_state_update.rb +103 -0
  64. data/lib/rubycord/events/webhooks.rb +62 -0
  65. data/lib/rubycord/gateway.rb +867 -0
  66. data/lib/rubycord/id_object.rb +37 -0
  67. data/lib/rubycord/light/data.rb +60 -0
  68. data/lib/rubycord/light/integrations.rb +71 -0
  69. data/lib/rubycord/light/light_bot.rb +56 -0
  70. data/lib/rubycord/light.rb +6 -0
  71. data/lib/rubycord/logger.rb +118 -0
  72. data/lib/rubycord/paginator.rb +55 -0
  73. data/lib/rubycord/permissions.rb +251 -0
  74. data/lib/rubycord/version.rb +5 -0
  75. data/lib/rubycord/voice/encoder.rb +113 -0
  76. data/lib/rubycord/voice/network.rb +366 -0
  77. data/lib/rubycord/voice/sodium.rb +96 -0
  78. data/lib/rubycord/voice/voice_bot.rb +408 -0
  79. data/lib/rubycord/webhooks/builder.rb +100 -0
  80. data/lib/rubycord/webhooks/client.rb +132 -0
  81. data/lib/rubycord/webhooks/embeds.rb +248 -0
  82. data/lib/rubycord/webhooks/modal.rb +78 -0
  83. data/lib/rubycord/webhooks/version.rb +7 -0
  84. data/lib/rubycord/webhooks/view.rb +192 -0
  85. data/lib/rubycord/webhooks.rb +12 -0
  86. data/lib/rubycord/websocket.rb +70 -0
  87. data/lib/rubycord.rb +140 -0
  88. 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