Class: Discordrb::Commands::CommandBot

Inherits:
Bot
  • Object
show all
Includes:
CommandContainer
Defined in:
lib/discordrb/commands/command_bot.rb

Overview

Bot that supports commands and command chains

Instance Attribute Summary collapse

Attributes included from CommandContainer

#commands

Attributes inherited from Bot

#awaits, #event_threads, #gateway, #name, #shard_key, #should_parse_self, #voices

Instance Method Summary collapse

Methods included from CommandContainer

#command, #include!, #include_commands, #remove_command

Methods included from RateLimiter

#bucket, #clean, #include_buckets, #rate_limited?

Methods inherited from Bot

#accept_invite, #add_await, #add_await!, #bot_application, #competing=, #connected?, #create_oauth_application, #create_server, #debug, #debug=, #delete_application_command, #delete_invite, #dispatch, #dnd, #edit_application_command, #edit_application_command_permissions, #emoji, #find_emoji, #game=, #get_application_command, #get_application_commands, #idle, #ignore_user, #ignored?, #invisible, #invite_url, #join, #listening=, #log_exception, #mode=, #online, #parse_mention, #parse_mentions, #profile, #prune_empty_groups, #raise_heartbeat_event, #raw_token, #register_application_command, #run, #send_file, #send_message, #send_temporary_message, #servers, #stop, #stream, #suppress_ready_debug, #token, #unignore_user, #update_oauth_application, #update_status, #users, #voice, #voice_connect, #voice_destroy, #watching=

Methods included from Discordrb::Cache

#channel, #ensure_channel, #ensure_server, #ensure_user, #find_channel, #find_user, #init_cache, #invite, #member, #pm_channel, #request_chunks, #resolve_invite_code, #server, #user, #voice_regions

Methods included from EventContainer

#add_handler, #application_command, #await, #button, #channel_create, #channel_delete, #channel_recipient_add, #channel_recipient_remove, #channel_update, class_from_string, #clear!, #disconnected, event_class, handler_class, #heartbeat, #include_events, #interaction_create, #invite_create, #invite_delete, #member_join, #member_leave, #member_update, #mention, #message, #message_delete, #message_edit, #message_update, #playing, #pm, #presence, #raw, #reaction_add, #reaction_remove, #reaction_remove_all, #ready, #remove_application_command_handler, #remove_handler, #select_menu, #server_create, #server_delete, #server_emoji, #server_emoji_create, #server_emoji_delete, #server_emoji_update, #server_role_create, #server_role_delete, #server_role_update, #server_update, #typing, #unknown, #user_ban, #user_unban, #voice_server_update, #voice_state_update, #webhook_update

Methods included from Events

matches_all

Constructor Details

#initialize(**attributes) ⇒ CommandBot

Creates a new CommandBot and logs in to Discord.

Parameters:

  • attributes (Hash)

    The attributes to initialize the CommandBot with.

Options Hash (**attributes):

  • :prefix (String, Array<String>, #call)

    The prefix that should trigger this bot's commands. It can be:

    • Any string (including the empty string). This has the effect that if a message starts with the prefix, the prefix will be stripped and the rest of the chain will be parsed as a command chain. Note that it will be literal - if the prefix is "hi" then the corresponding trigger string for a command called "test" would be "hitest". Don't forget to put spaces in if you need them!
    • An array of prefixes. Those will behave similarly to setting one string as a prefix, but instead of only one string, any of the strings in the array can be used.
    • Something Proc-like (responds to :call) that takes a Message object as an argument and returns either the command chain in raw form or nil if the given message shouldn't be parsed. This can be used to make more complicated dynamic prefixes (e. g. based on server), or even something else entirely (suffixes, or most adventurous, infixes).
  • :advanced_functionality (true, false)

    Whether to enable advanced functionality (very powerful way to nest commands into chains, see https://github.com/shardlab/discordrb/wiki/Commands#command-chain-syntax for info. Default is false.

  • :help_command (Symbol, Array<Symbol>, false)

    The name of the command that displays info for other commands. Use an array if you want to have aliases. Default is "help". If none should be created, use false as the value.

  • :command_doesnt_exist_message (String, #call)

    The message that should be displayed if a user attempts to use a command that does not exist. If none is specified, no message will be displayed. In the message, you can use the string '%command%' that will be replaced with the name of the command. Anything responding to call such as a proc will be called with the event, and is expected to return a String or nil.

  • :no_permission_message (String)

    The message to be displayed when NoPermission error is raised.

  • :spaces_allowed (true, false)

    Whether spaces are allowed to occur between the prefix and the command. Default is false.

  • :webhook_commands (true, false)

    Whether messages sent by webhooks are allowed to trigger commands. Default is true.

  • :channels (Array<String, Integer, Channel>)

    The channels this command bot accepts commands on. Superseded if a command has a 'channels' attribute.

  • :previous (String)

    Character that should designate the result of the previous command in a command chain (see :advanced_functionality). Default is '~'. Set to an empty string to disable.

  • :chain_delimiter (String)

    Character that should designate that a new command begins in the command chain (see :advanced_functionality). Default is '>'. Set to an empty string to disable.

  • :chain_args_delim (String)

    Character that should separate the command chain arguments from the chain itself (see :advanced_functionality). Default is ':'. Set to an empty string to disable.

  • :sub_chain_start (String)

    Character that should start a sub-chain (see :advanced_functionality). Default is '['. Set to an empty string to disable.

  • :sub_chain_end (String)

    Character that should end a sub-chain (see :advanced_functionality). Default is ']'. Set to an empty string to disable.

  • :quote_start (String)

    Character that should start a quoted string (see :advanced_functionality). Default is '"'. Set to an empty string to disable.

  • :quote_end (String)

    Character that should end a quoted string (see :advanced_functionality). Default is '"' or the same as :quote_start. Set to an empty string to disable.

  • :ignore_bots (true, false)

    Whether the bot should ignore bot accounts or not. Default is false.

See Also:



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/discordrb/commands/command_bot.rb', line 73

def initialize(**attributes)
  # TODO: This needs to be revisited. undefined attributes are treated
  # as explicitly passed nils.
  super(
    log_mode: attributes[:log_mode],
    token: attributes[:token],
    client_id: attributes[:client_id],
    type: attributes[:type],
    name: attributes[:name],
    fancy_log: attributes[:fancy_log],
    suppress_ready: attributes[:suppress_ready],
    parse_self: attributes[:parse_self],
    shard_id: attributes[:shard_id],
    num_shards: attributes[:num_shards],
    redact_token: attributes.key?(:redact_token) ? attributes[:redact_token] : true,
    ignore_bots: attributes[:ignore_bots],
    compress_mode: attributes[:compress_mode],
    intents: attributes[:intents] || :all
  )

  @prefix = attributes[:prefix]
  @attributes = {
    # Whether advanced functionality such as command chains are enabled
    advanced_functionality: attributes[:advanced_functionality].nil? ? false : attributes[:advanced_functionality],

    # The name of the help command (that displays information to other commands). False if none should exist
    help_command: attributes[:help_command].is_a?(FalseClass) ? nil : (attributes[:help_command] || :help),

    # The message to display for when a command doesn't exist, %command% to get the command name in question and nil for no message
    # No default value here because it may not be desired behaviour
    command_doesnt_exist_message: attributes[:command_doesnt_exist_message],

    # The message to be displayed when `NoPermission` error is raised.
    no_permission_message: attributes[:no_permission_message],

    # Spaces allowed between prefix and command
    spaces_allowed: attributes[:spaces_allowed].nil? ? false : attributes[:spaces_allowed],

    # Webhooks allowed to trigger commands
    webhook_commands: attributes[:webhook_commands].nil? ? true : attributes[:webhook_commands],

    channels: attributes[:channels] || [],

    # All of the following need to be one character
    # String to designate previous result in command chain
    previous: attributes[:previous] || '~',

    # Command chain delimiter
    chain_delimiter: attributes[:chain_delimiter] || '>',

    # Chain argument delimiter
    chain_args_delim: attributes[:chain_args_delim] || ':',

    # Sub-chain starting character
    sub_chain_start: attributes[:sub_chain_start] || '[',

    # Sub-chain ending character
    sub_chain_end: attributes[:sub_chain_end] || ']',

    # Quoted mode starting character
    quote_start: attributes[:quote_start] || '"',

    # Quoted mode ending character
    quote_end: attributes[:quote_end] || attributes[:quote_start] || '"',

    # Default block for handling internal exceptions, or a string to respond with
    rescue: attributes[:rescue]
  }

  @permissions = {
    roles: {},
    users: {}
  }

  return unless @attributes[:help_command]

  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|
    if command_name
      command = @commands[command_name.to_sym]
      if command.is_a?(CommandAlias)
        command = command.aliased_command
        command_name = command.name
      end
      return "The command `#{command_name}` does not exist!" unless command

      desc = command.attributes[:description] || '*No description available*'
      usage = command.attributes[:usage]
      parameters = command.attributes[:parameters]
      result = "**`#{command_name}`**: #{desc}"
      aliases = command_aliases(command_name.to_sym)
      unless aliases.empty?
        result += "\nAliases: "
        result += aliases.map { |a| "`#{a.name}`" }.join(', ')
      end
      result += "\nUsage: `#{usage}`" if usage
      if parameters
        result += "\nAccepted Parameters:\n```"
        parameters.each { |p| result += "\n#{p}" }
        result += '```'
      end
      result
    else
      available_commands = @commands.values.reject do |c|
        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)
      end
      case available_commands.length
      when 0..5
        available_commands.reduce "**List of commands:**\n" do |memo, c|
          memo + "**`#{c.name}`**: #{c.attributes[:description] || '*No description available*'}\n"
        end
      when 5..50
        (available_commands.reduce "**List of commands:**\n" do |memo, c|
          memo + "`#{c.name}`, "
        end)[0..-3]
      else
        event.user.pm(available_commands.reduce("**List of commands:**\n") { |m, e| m + "`#{e.name}`, " }[0..-3])
        event.channel.pm? ? '' : 'Sending list in PM!'
      end
    end
  end
end

Instance Attribute Details

#attributesHash (readonly)

Returns this bot's attributes.

Returns:

  • (Hash)

    this bot's attributes.



17
18
19
# File 'lib/discordrb/commands/command_bot.rb', line 17

def attributes
  @attributes
end

#prefixString, ... (readonly)

Returns the prefix commands are triggered with.

Returns:

  • (String, Array<String>, #call)

    the prefix commands are triggered with.

See Also:



21
22
23
# File 'lib/discordrb/commands/command_bot.rb', line 21

def prefix
  @prefix
end

Instance Method Details

#add_channel(channel) ⇒ Object

Add a channel to the list of channels the bot accepts commands from.

Parameters:

  • channel (String, Integer, Channel)

    The channel name, integer ID, or Channel object to be added



387
388
389
390
391
# File 'lib/discordrb/commands/command_bot.rb', line 387

def add_channel(channel)
  return if @attributes[:channels].find { |c| channel.resolve_id == c.resolve_id }

  @attributes[:channels] << channel
end

#arg_check(args, types = nil, server = nil) ⇒ Object

Transforms an array of string arguments based on types array. For example, ['1', '10..14'] with types [Integer, Range] would turn into [1, 10..14].



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/discordrb/commands/command_bot.rb', line 253

def arg_check(args, types = nil, server = nil)
  return args unless types

  args.each_with_index.map do |arg, i|
    next arg if types[i].nil? || types[i] == String

    if types[i] == Integer
      begin
        Integer(arg, 10)
      rescue ArgumentError
        nil
      end
    elsif types[i] == Float
      begin
        Float(arg)
      rescue ArgumentError
        nil
      end
    elsif types[i] == Time
      begin
        Time.parse arg
      rescue ArgumentError
        nil
      end
    elsif types[i] == TrueClass || types[i] == FalseClass
      if arg.casecmp('true').zero? || arg.downcase.start_with?('y')
        true
      elsif arg.casecmp('false').zero? || arg.downcase.start_with?('n')
        false
      end
    elsif types[i] == Symbol
      arg.to_sym
    elsif types[i] == Encoding
      begin
        Encoding.find arg
      rescue ArgumentError
        nil
      end
    elsif types[i] == Regexp
      begin
        Regexp.new arg
      rescue ArgumentError
        nil
      end
    elsif types[i] == Rational
      begin
        Rational(arg)
      rescue ArgumentError
        nil
      end
    elsif types[i] == Range
      begin
        if arg.include? '...'
          Range.new(*arg.split('...').map(&:to_i), true)
        elsif arg.include? '..'
          Range.new(*arg.split('..').map(&:to_i))
        end
      rescue ArgumentError
        nil
      end
    elsif types[i] == NilClass
      nil
    elsif [Discordrb::User, Discordrb::Role, Discordrb::Emoji].include? types[i]
      result = parse_mention arg, server
      result if result.instance_of? types[i]
    elsif types[i] == Discordrb::Invite
      resolve_invite_code arg
    elsif types[i].respond_to?(:from_argument)
      begin
        types[i].from_argument arg
      rescue StandardError
        nil
      end
    else
      raise ArgumentError, "#{types[i]} doesn't implement from_argument"
    end
  end
end

#channels=(channels) ⇒ Object

See Also:



375
376
377
# File 'lib/discordrb/commands/command_bot.rb', line 375

def channels=(channels)
  update_channels(channels)
end

#command_aliases(name) ⇒ Array<CommandAlias>

Returns all aliases for the command with the given name

Parameters:

  • name (Symbol)

    the name of the Command

Returns:



198
199
200
201
202
# File 'lib/discordrb/commands/command_bot.rb', line 198

def command_aliases(name)
  commands.values.select do |command|
    command.is_a?(CommandAlias) && command.aliased_command.name == name
  end
end

#execute_command(name, event, arguments, chained = false, check_permissions = true) ⇒ String?

Executes a particular command on the bot. Mostly useful for internal stuff, but one can never know.

Parameters:

  • name (Symbol)

    The command to execute.

  • event (CommandEvent)

    The event to pass to the command.

  • arguments (Array<String>)

    The arguments to pass to the command.

  • chained (true, false) (defaults to: false)

    Whether or not it should be executed as part of a command chain. If this is false, commands that have chain_usable set to false will not work.

  • check_permissions (true, false) (defaults to: true)

    Whether permission parameters such as required_permission or permission_level should be checked.

Returns:

  • (String, nil)

    the command's result, if there is any.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/discordrb/commands/command_bot.rb', line 213

def execute_command(name, event, arguments, chained = false, check_permissions = true)
  debug("Executing command #{name} with arguments #{arguments}")
  return unless @commands

  command = @commands[name]
  command = command.aliased_command if command.is_a?(CommandAlias)
  return unless !check_permissions || channels?(event.channel, @attributes[:channels]) ||
                (command && !command.attributes[:channels].nil?)

  unless command
    if @attributes[:command_doesnt_exist_message]
      message = @attributes[:command_doesnt_exist_message]
      message = message.call(event) if message.respond_to?(:call)
      event.respond message.gsub('%command%', name.to_s) if message
    end
    return
  end
  return unless !check_permissions || channels?(event.channel, command.attributes[:channels])

  arguments = arg_check(arguments, command.attributes[:arg_types], event.server) if check_permissions
  if (check_permissions &&
     permission?(event.author, command.attributes[:permission_level], event.server) &&
     required_permissions?(event.author, command.attributes[:required_permissions], event.channel) &&
     required_roles?(event.author, command.attributes[:required_roles]) &&
     allowed_roles?(event.author, command.attributes[:allowed_roles])) ||
     !check_permissions
    event.command = command
    result = command.call(event, arguments, chained, check_permissions)
    stringify(result)
  else
    event.respond command.attributes[:permission_message].gsub('%name%', name.to_s) if command.attributes[:permission_message]
    nil
  end
rescue Discordrb::Errors::NoPermission
  event.respond @attributes[:no_permission_message] unless @attributes[:no_permission_message].nil?
  raise
end

#permission?(user, level, server) ⇒ true, false

Check if a user has permission to do something

Parameters:

  • user (User)

    The user to check

  • level (Integer)

    The minimum permission level the user should have (inclusive)

  • server (Server)

    The server on which to check

Returns:

  • (true, false)

    whether or not the user has the given permission



362
363
364
365
366
367
368
369
370
371
372
# File 'lib/discordrb/commands/command_bot.rb', line 362

def permission?(user, level, server)
  determined_level = if user.webhook? || server.nil?
                       0
                     else
                       user.roles.reduce(0) do |memo, role|
                         [@permissions[:roles][role.id] || 0, memo].max
                       end
                     end

  [@permissions[:users][user.id] || 0, determined_level].max >= level
end

#remove_channel(channel) ⇒ Object

Remove a channel from the list of channels the bot accepts commands from.

Parameters:

  • channel (String, Integer, Channel)

    The channel name, integer ID, or Channel object to be removed



395
396
397
# File 'lib/discordrb/commands/command_bot.rb', line 395

def remove_channel(channel)
  @attributes[:channels].delete_if { |c| channel.resolve_id == c.resolve_id }
end

#set_role_permission(id, level) ⇒ Object

Sets the permission level of a role - this applies to all users in the role

Parameters:

  • id (Integer)

    the ID of the role whose level to set

  • level (Integer)

    the level to set the permission to



353
354
355
# File 'lib/discordrb/commands/command_bot.rb', line 353

def set_role_permission(id, level)
  @permissions[:roles][id] = level
end

#set_user_permission(id, level) ⇒ Object

Sets the permission level of a user

Parameters:

  • id (Integer)

    the ID of the user whose level to set

  • level (Integer)

    the level to set the permission to



346
347
348
# File 'lib/discordrb/commands/command_bot.rb', line 346

def set_user_permission(id, level)
  @permissions[:users][id] = level
end

#simple_execute(chain, event) ⇒ String?

Executes a command in a simple manner, without command chains or permissions.

Parameters:

  • chain (String)

    The command with its arguments separated by spaces.

  • event (CommandEvent)

    The event to pass to the command.

Returns:

  • (String, nil)

    the command's result, if there is any.



336
337
338
339
340
341
# File 'lib/discordrb/commands/command_bot.rb', line 336

def simple_execute(chain, event)
  return nil if chain.empty?

  args = chain.split(' ')
  execute_command(args[0].to_sym, event, args[1..])
end

#update_channels(channels = []) ⇒ Object

Update the list of channels the bot accepts commands from.

Parameters:

  • channels (Array<String, Integer, Channel>) (defaults to: [])

    The channels this command bot accepts commands on.



381
382
383
# File 'lib/discordrb/commands/command_bot.rb', line 381

def update_channels(channels = [])
  @attributes[:channels] = Array(channels)
end