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