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