Class: DiscordRDA::Bot

Inherits:
Object
  • Object
show all
Defined in:
lib/discord_rda/bot.rb

Overview

Main Bot class for DiscordRDA. Entry point for building Discord bots.

Examples:

Basic bot

bot = DiscordRDA::Bot.new(token: ENV['DISCORD_TOKEN'])
bot.on(:message_create) { |e| puts e.content }
bot.run

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token:, **options) ⇒ Bot

Initialize a new bot

Parameters:

  • token (String)

    Bot token

  • options (Hash)

    Configuration options



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/discord_rda/bot.rb', line 52

def initialize(token:, **options)
  @config = Configuration.new(options.merge(token: token))
  @logger = Logger.new(level: @config.log_level, format: @config.log_format)
  @event_bus = EventBus.new(logger: @logger)
  @cache = build_cache
  @shard_manager = ShardManager.new(@config, @event_bus, @logger)
  @rest = RestClient.new(@config, @logger)

  # Configure entity API clients
  Message.api = @rest
  Interaction.api = @rest

  setup_event_handlers
  setup_interaction_handlers

  # Initialize scalable components
  @scalable_rest = nil
  @reshard_manager = ReshardManager.new(self, @shard_manager, @logger)
  @hot_reload_manager = HotReloadManager.new(self, @logger)
  @plugins = PluginRegistry.new(logger: @logger)
  @slash_commands = {}
  @running = false
  @commands = {}

  setup_event_handlers
end

Instance Attribute Details

#cacheEntityCache (readonly)

Returns Entity cache.

Returns:



23
24
25
# File 'lib/discord_rda/bot.rb', line 23

def cache
  @cache
end

#configConfiguration (readonly)

Returns Bot configuration.

Returns:



14
15
16
# File 'lib/discord_rda/bot.rb', line 14

def config
  @config
end

#event_busEventBus (readonly)

Returns Event bus.

Returns:



20
21
22
# File 'lib/discord_rda/bot.rb', line 20

def event_bus
  @event_bus
end

#hot_reload_managerHotReloadManager (readonly)

Returns Hot reload manager.

Returns:



38
39
40
# File 'lib/discord_rda/bot.rb', line 38

def hot_reload_manager
  @hot_reload_manager
end

#loggerLogger (readonly)

Returns Logger instance.

Returns:

  • (Logger)

    Logger instance



17
18
19
# File 'lib/discord_rda/bot.rb', line 17

def logger
  @logger
end

#pluginsPluginRegistry (readonly)

Returns Plugin registry.

Returns:



41
42
43
# File 'lib/discord_rda/bot.rb', line 41

def plugins
  @plugins
end

#reshard_managerReshardManager (readonly)

Returns Reshard manager.

Returns:



35
36
37
# File 'lib/discord_rda/bot.rb', line 35

def reshard_manager
  @reshard_manager
end

#restRestClient (readonly)

Returns REST client.

Returns:



29
30
31
# File 'lib/discord_rda/bot.rb', line 29

def rest
  @rest
end

#runningBoolean (readonly)

Returns Whether bot is running.

Returns:

  • (Boolean)

    Whether bot is running



44
45
46
# File 'lib/discord_rda/bot.rb', line 44

def running
  @running
end

#scalable_restScalableRestClient (readonly)

Returns Scalable REST client (if enabled).

Returns:



32
33
34
# File 'lib/discord_rda/bot.rb', line 32

def scalable_rest
  @scalable_rest
end

#shard_managerShardManager (readonly)

Returns Shard manager.

Returns:



26
27
28
# File 'lib/discord_rda/bot.rb', line 26

def shard_manager
  @shard_manager
end

#slash_commandsHash (readonly)

Returns Registered slash commands.

Returns:

  • (Hash)

    Registered slash commands



47
48
49
# File 'lib/discord_rda/bot.rb', line 47

def slash_commands
  @slash_commands
end

Instance Method Details

#add_guild_member_role(guild_id, user_id, role_id, reason: nil) ⇒ void

This method returns an undefined value.

Add role to guild member

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • user_id (String, Snowflake)

    User ID

  • role_id (String, Snowflake)

    Role ID

  • reason (String) (defaults to: nil)

    Audit log reason



493
494
495
496
# File 'lib/discord_rda/bot.rb', line 493

def add_guild_member_role(guild_id, user_id, role_id, reason: nil)
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  @rest.put("/guilds/#{guild_id}/members/#{user_id}/roles/#{role_id}", headers: headers)
end

#add_reaction(channel_id, message_id, emoji) ⇒ void

This method returns an undefined value.

Add a reaction to a message

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • message_id (String, Snowflake)

    Message ID

  • emoji (String, Emoji)

    Emoji (unicode or name:id format)



404
405
406
407
# File 'lib/discord_rda/bot.rb', line 404

def add_reaction(channel_id, message_id, emoji)
  emoji_str = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
  @rest.put("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}/@me")
end

#analyticsHash

Get analytics data (if analytics plugin registered)

Returns:

  • (Hash)

    Analytics data



392
393
394
395
# File 'lib/discord_rda/bot.rb', line 392

def analytics
  analytics_plugin = @plugins.get(:Analytics)
  analytics_plugin&.summary || {}
end

#bulk_delete_messages(channel_id, message_ids, reason: nil) ⇒ void

This method returns an undefined value.

Bulk delete messages

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • message_ids (Array<String, Snowflake>)

    Message IDs to delete (2-100)

  • reason (String) (defaults to: nil)

    Audit log reason



700
701
702
703
# File 'lib/discord_rda/bot.rb', line 700

def bulk_delete_messages(channel_id, message_ids, reason: nil)
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  @rest.post("/channels/#{channel_id}/messages/bulk-delete", body: { messages: message_ids.map(&:to_s) }, headers: headers)
end

#bulk_register_commands(commands) ⇒ Array<ApplicationCommand>

Bulk register global commands (replaces existing)

Parameters:

Returns:



134
135
136
137
138
139
140
141
142
# File 'lib/discord_rda/bot.rb', line 134

def bulk_register_commands(commands)
  return [] unless me

  app_id = me.id.to_s
  payload = commands.map(&:to_h)

  data = @rest.put("/applications/#{app_id}/commands", body: payload)
  data.map { |cmd| ApplicationCommand.new(cmd) }
end

#channel(channel_id) ⇒ Channel?

Get a channel by ID

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

Returns:

  • (Channel, nil)

    Channel or nil



297
298
299
300
301
302
303
304
305
306
307
# File 'lib/discord_rda/bot.rb', line 297

def channel(channel_id)
  cached = @cache.channel(channel_id)
  return cached if cached

  data = @rest.get("/channels/#{channel_id}")
  channel = Channel.new(data)
  @cache.cache_channel(channel)
  channel
rescue RestClient::NotFoundError
  nil
end

#channel_message(channel_id, message_id) ⇒ Message?

Get a single message from a channel

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • message_id (String, Snowflake)

    Message ID

Returns:

  • (Message, nil)

    Message or nil



341
342
343
344
345
346
# File 'lib/discord_rda/bot.rb', line 341

def channel_message(channel_id, message_id)
  data = @rest.get("/channels/#{channel_id}/messages/#{message_id}")
  Message.new(data)
rescue RestClient::NotFoundError
  nil
end

#channel_messages(channel_id, limit: 50, before: nil, after: nil, around: nil) ⇒ Array<Message>

Get messages from a channel with pagination (simplified)

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • limit (Integer) (defaults to: 50)

    Max messages to fetch (1-100, default 50)

  • before (String, Snowflake) (defaults to: nil)

    Get messages before this ID

  • after (String, Snowflake) (defaults to: nil)

    Get messages after this ID

  • around (String, Snowflake) (defaults to: nil)

    Get messages around this ID

Returns:



327
328
329
330
331
332
333
334
335
# File 'lib/discord_rda/bot.rb', line 327

def channel_messages(channel_id, limit: 50, before: nil, after: nil, around: nil)
  params = { limit: limit }
  params[:before] = before.to_s if before
  params[:after] = after.to_s if after
  params[:around] = around.to_s if around

  data = @rest.get("/channels/#{channel_id}/messages", params: params)
  data.map { |msg| Message.new(msg) }
end

#channel_webhooks(channel_id) ⇒ Array<Hash>

Get channel webhooks

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

Returns:

  • (Array<Hash>)

    Webhooks



622
623
624
# File 'lib/discord_rda/bot.rb', line 622

def channel_webhooks(channel_id)
  @rest.get("/channels/#{channel_id}/webhooks")
end

#context_menu(type:, name:, **options) {|Interaction| ... } ⇒ ApplicationCommand

Register a context menu command (user or message)

Parameters:

  • type (Symbol)

    :user or :message

  • name (String)

    Command name

  • options (Hash)

    Command options

Yields:

Returns:



123
124
125
126
127
128
129
# File 'lib/discord_rda/bot.rb', line 123

def context_menu(type:, name:, **options, &block)
  cmd_type = type == :user ? 2 : 3
  options[:type] = cmd_type
  options[:description] = '' # Context menus don't have descriptions

  slash(name, '', **options, &block)
end

#create_guild_ban(guild_id, user_id, delete_message_days: nil, reason: nil) ⇒ void

This method returns an undefined value.

Create guild ban

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • user_id (String, Snowflake)

    User ID

  • delete_message_days (Integer) (defaults to: nil)

    Days of messages to delete (0-7)

  • reason (String) (defaults to: nil)

    Audit log reason



589
590
591
592
593
594
# File 'lib/discord_rda/bot.rb', line 589

def create_guild_ban(guild_id, user_id, delete_message_days: nil, reason: nil)
  payload = {}
  payload[:delete_message_days] = delete_message_days if delete_message_days
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  @rest.put("/guilds/#{guild_id}/bans/#{user_id}", body: payload, headers: headers)
end

#create_guild_channel(guild_id, name:, type: 0, **options) ⇒ Channel

Create guild channel (simplified)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • name (String)

    Channel name

  • type (Integer) (defaults to: 0)

    Channel type (0=text, 2=voice, 4=category, etc.)

  • options (Hash)

    Optional settings

Returns:



669
670
671
672
673
# File 'lib/discord_rda/bot.rb', line 669

def create_guild_channel(guild_id, name:, type: 0, **options)
  payload = { name: name, type: type }.merge(options.slice(:topic, :bitrate, :user_limit, :parent_id, :nsfw, :permission_overwrites, :rate_limit_per_user))
  data = @rest.post("/guilds/#{guild_id}/channels", body: payload)
  Channel.new(data)
end

#create_guild_role(guild_id, name:, **options) ⇒ Role

Create guild role (simplified)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • name (String)

    Role name

  • options (Hash)

    Optional settings (permissions, color, hoist, mentionable)

Returns:

  • (Role)

    Created role



534
535
536
537
538
# File 'lib/discord_rda/bot.rb', line 534

def create_guild_role(guild_id, name:, **options)
  payload = { name: name }.merge(options.slice(:permissions, :color, :hoist, :mentionable, :icon, :unicode_emoji))
  data = @rest.post("/guilds/#{guild_id}/roles", body: payload)
  Role.new(data.merge('guild_id' => guild_id.to_s))
end

#create_webhook(channel_id, name:, avatar: nil) ⇒ Hash

Create a webhook

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • name (String)

    Webhook name

  • avatar (String) (defaults to: nil)

    Base64-encoded avatar image (optional)

Returns:

  • (Hash)

    Webhook data



613
614
615
616
617
# File 'lib/discord_rda/bot.rb', line 613

def create_webhook(channel_id, name:, avatar: nil)
  payload = { name: name }
  payload[:avatar] = avatar if avatar
  @rest.post("/channels/#{channel_id}/webhooks", body: payload)
end

#delete_channel(channel_id, reason: nil) ⇒ Channel

Delete channel

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • reason (String) (defaults to: nil)

    Audit log reason

Returns:



689
690
691
692
693
# File 'lib/discord_rda/bot.rb', line 689

def delete_channel(channel_id, reason: nil)
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  data = @rest.delete("/channels/#{channel_id}", headers: headers)
  Channel.new(data)
end

#delete_global_command(command_id) ⇒ void

This method returns an undefined value.

Delete a global command

Parameters:

  • command_id (String)

    Command ID



147
148
149
# File 'lib/discord_rda/bot.rb', line 147

def delete_global_command(command_id)
  @rest.delete("/applications/#{me.id}/commands/#{command_id}") if me
end

#delete_guild_command(guild_id, command_id) ⇒ void

This method returns an undefined value.

Delete a guild command

Parameters:

  • guild_id (String)

    Guild ID

  • command_id (String)

    Command ID



155
156
157
# File 'lib/discord_rda/bot.rb', line 155

def delete_guild_command(guild_id, command_id)
  @rest.delete("/applications/#{me.id}/guilds/#{guild_id}/commands/#{command_id}") if me
end

#delete_guild_role(guild_id, role_id, reason: nil) ⇒ void

This method returns an undefined value.

Delete guild role

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • role_id (String, Snowflake)

    Role ID

  • reason (String) (defaults to: nil)

    Audit log reason



556
557
558
559
# File 'lib/discord_rda/bot.rb', line 556

def delete_guild_role(guild_id, role_id, reason: nil)
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  @rest.delete("/guilds/#{guild_id}/roles/#{role_id}", headers: headers)
end

#delete_webhook(webhook_id, token: nil) ⇒ void

This method returns an undefined value.

Delete a webhook

Parameters:

  • webhook_id (String, Snowflake)

    Webhook ID

  • token (String) (defaults to: nil)

    Webhook token (optional, for webhook-owned deletes)



648
649
650
651
# File 'lib/discord_rda/bot.rb', line 648

def delete_webhook(webhook_id, token: nil)
  path = token ? "/webhooks/#{webhook_id}/#{token}" : "/webhooks/#{webhook_id}"
  @rest.delete(path)
end

#enable_auto_reshard(max_guilds_per_shard: 1000) ⇒ void

This method returns an undefined value.

Enable auto-resharding based on guild count

Parameters:

  • max_guilds_per_shard (Integer) (defaults to: 1000)

    Max guilds per shard



377
378
379
380
381
382
# File 'lib/discord_rda/bot.rb', line 377

def enable_auto_reshard(max_guilds_per_shard: 1000)
  @event_bus.on(:guild_create) do |_event|
    guild_count = @shard_manager.total_guilds || 0
    @reshard_manager.auto_reshard_if_needed(guild_count, max_guilds_per_shard: max_guilds_per_shard)
  end
end

#enable_hot_reload(watch_dir: 'lib') ⇒ void

This method returns an undefined value.

Enable hot reload for development

Parameters:

  • watch_dir (String) (defaults to: 'lib')

    Directory to watch



360
361
362
363
364
# File 'lib/discord_rda/bot.rb', line 360

def enable_hot_reload(watch_dir: 'lib')
  @logger.info('Enabling hot reload', watch_dir: watch_dir)
  @hot_reload_manager = HotReloadManager.new(self, @logger, watch_dir: watch_dir)
  @hot_reload_manager.enable
end

#enable_scalable_rest(proxy: nil) ⇒ void

This method returns an undefined value.

Enable scalable REST client (queue-based rate limiting)

Parameters:

  • proxy (Hash) (defaults to: nil)

    Optional proxy configuration



351
352
353
354
355
# File 'lib/discord_rda/bot.rb', line 351

def enable_scalable_rest(proxy: nil)
  @logger.info('Enabling scalable REST client')
  @scalable_rest = ScalableRestClient.new(@config, @logger, proxy: proxy)
  @scalable_rest.start
end

#execute_webhook(webhook_id, token, content = nil, **options) ⇒ void

This method returns an undefined value.

Execute webhook (simplified)

Parameters:

  • webhook_id (String, Snowflake)

    Webhook ID

  • token (String)

    Webhook token

  • content (String) (defaults to: nil)

    Message content

  • options (Hash)

    Options (username, avatar_url, embeds, etc.)



639
640
641
642
# File 'lib/discord_rda/bot.rb', line 639

def execute_webhook(webhook_id, token, content = nil, **options)
  payload = { content: content }.merge(options.slice(:username, :avatar_url, :embeds, :components, :allowed_mentions))
  @rest.post("/webhooks/#{webhook_id}/#{token}", body: payload)
end

#get_reactions(channel_id, message_id, emoji, limit: 25) ⇒ Array<User>

Get reactions for a message (simplified - no pagination)

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • message_id (String, Snowflake)

    Message ID

  • emoji (String, Emoji)

    Emoji filter

  • limit (Integer) (defaults to: 25)

    Max users to return (1-100, default 25)

Returns:

  • (Array<User>)

    Users who reacted



426
427
428
429
430
# File 'lib/discord_rda/bot.rb', line 426

def get_reactions(channel_id, message_id, emoji, limit: 25)
  emoji_str = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
  data = @rest.get("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}", params: { limit: limit })
  data.map { |u| User.new(u) }
end

#guild(guild_id) ⇒ Guild?

Get a guild by ID

Parameters:

Returns:

  • (Guild, nil)

    Guild or nil



282
283
284
285
286
287
288
289
290
291
292
# File 'lib/discord_rda/bot.rb', line 282

def guild(guild_id)
  cached = @cache.guild(guild_id)
  return cached if cached

  data = @rest.get("/guilds/#{guild_id}")
  guild = Guild.new(data)
  @cache.cache_guild(guild)
  guild
rescue RestClient::NotFoundError
  nil
end

#guild_ban(guild_id, user_id) ⇒ Hash?

Get a specific guild ban

Parameters:

Returns:

  • (Hash, nil)

    Ban data or nil



576
577
578
579
580
581
# File 'lib/discord_rda/bot.rb', line 576

def guild_ban(guild_id, user_id)
  data = @rest.get("/guilds/#{guild_id}/bans/#{user_id}")
  { user: User.new(data['user']), reason: data['reason'] }
rescue RestClient::NotFoundError
  nil
end

#guild_bans(guild_id, limit: 100) ⇒ Array<Hash>

Get guild bans (simplified - no pagination)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • limit (Integer) (defaults to: 100)

    Max bans (1-1000, default 100)

Returns:

  • (Array<Hash>)

    Bans (user + reason data)



567
568
569
570
# File 'lib/discord_rda/bot.rb', line 567

def guild_bans(guild_id, limit: 100)
  data = @rest.get("/guilds/#{guild_id}/bans", params: { limit: limit })
  data.map { |b| { user: User.new(b['user']), reason: b['reason'] } }
end

#guild_channels(guild_id) ⇒ Array<Channel>

Get guild channels

Parameters:

Returns:



658
659
660
661
# File 'lib/discord_rda/bot.rb', line 658

def guild_channels(guild_id)
  data = @rest.get("/guilds/#{guild_id}/channels")
  data.map { |c| Channel.new(c) }
end

#guild_member(guild_id, user_id) ⇒ Member?

Get a guild member

Parameters:

Returns:

  • (Member, nil)

    Member or nil



446
447
448
449
450
451
# File 'lib/discord_rda/bot.rb', line 446

def guild_member(guild_id, user_id)
  data = @rest.get("/guilds/#{guild_id}/members/#{user_id}")
  Member.new(data.merge('guild_id' => guild_id.to_s))
rescue RestClient::NotFoundError
  nil
end

#guild_members(guild_id, limit: 100, after: nil) ⇒ Array<Member>

List guild members (simplified - basic pagination)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • limit (Integer) (defaults to: 100)

    Max members (1-1000, default 100)

  • after (String, Snowflake) (defaults to: nil)

    Get members after this user ID

Returns:



458
459
460
461
462
463
# File 'lib/discord_rda/bot.rb', line 458

def guild_members(guild_id, limit: 100, after: nil)
  params = { limit: limit }
  params[:after] = after.to_s if after
  data = @rest.get("/guilds/#{guild_id}/members", params: params)
  data.map { |m| Member.new(m.merge('guild_id' => guild_id.to_s)) }
end

#guild_roles(guild_id) ⇒ Array<Role>

Get guild roles

Parameters:

Returns:

  • (Array<Role>)

    Roles



524
525
526
527
# File 'lib/discord_rda/bot.rb', line 524

def guild_roles(guild_id)
  data = @rest.get("/guilds/#{guild_id}/roles")
  data.map { |r| Role.new(r.merge('guild_id' => guild_id.to_s)) }
end

#guild_webhooks(guild_id) ⇒ Array<Hash>

Get guild webhooks

Parameters:

Returns:

  • (Array<Hash>)

    Webhooks



629
630
631
# File 'lib/discord_rda/bot.rb', line 629

def guild_webhooks(guild_id)
  @rest.get("/guilds/#{guild_id}/webhooks")
end

#handle_autocomplete(interaction) ⇒ Object



764
765
766
767
768
769
770
771
772
773
# File 'lib/discord_rda/bot.rb', line 764

def handle_autocomplete(interaction)
  # Autocomplete needs to be handled by the command that registered it
  cmd_name = interaction.command_name
  focused = interaction.focused_option

  @logger.debug('Autocomplete', command: cmd_name, option: focused&.dig('name'))

  # Emit autocomplete event
  @event_bus.emit(:autocomplete, interaction)
end

#handle_component(interaction) ⇒ Object



745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
# File 'lib/discord_rda/bot.rb', line 745

def handle_component(interaction)
  # Component interactions are handled by custom_id patterns or specific handlers
  custom_id = interaction.custom_id
  @logger.debug('Component interaction', custom_id: custom_id, user: interaction.user&.id)

  # Emit specific event for this component type
  event_type = case interaction.component_type
  when 2 then :button_click
  when 3 then :string_select
  when 5 then :user_select
  when 6 then :role_select
  when 7 then :mentionable_select
  when 8 then :channel_select
  else :component_interaction
  end

  @event_bus.emit(event_type, interaction)
end

#handle_modal_submit(interaction) ⇒ Object



775
776
777
778
779
780
781
782
783
# File 'lib/discord_rda/bot.rb', line 775

def handle_modal_submit(interaction)
  modal_id = interaction.custom_id
  values = interaction.modal_values

  @logger.debug('Modal submit', modal_id: modal_id, values: values.keys)

  # Emit modal submit event
  @event_bus.emit(:modal_submit, interaction)
end

#handle_slash_command(interaction) ⇒ Object



722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
# File 'lib/discord_rda/bot.rb', line 722

def handle_slash_command(interaction)
  cmd_name = interaction.command_name
  guild_id = interaction.guild_id

  # Try guild-specific command first, then global
  key = guild_id ? "#{cmd_name}:#{guild_id}" : cmd_name
  cmd = @slash_commands[key] || @slash_commands[cmd_name]

  if cmd && cmd.handler
    @logger.debug('Executing slash command', name: cmd_name, user: interaction.user&.id)
    begin
      cmd.handler.call(interaction)
    rescue => e
      @logger.error('Slash command error', command: cmd_name, error: e)
      # Send error response
      interaction.respond(content: "An error occurred while executing this command.", ephemeral: true) rescue nil
    end
  else
    @logger.warn('Unknown slash command', name: cmd_name)
    interaction.respond(content: "Unknown command: #{cmd_name}", ephemeral: true) rescue nil
  end
end

#invalid_bucket_statusHash?

Get invalid request bucket status

Returns:

  • (Hash, nil)

    Invalid bucket status



386
387
388
# File 'lib/discord_rda/bot.rb', line 386

def invalid_bucket_status
  @scalable_rest&.invalid_bucket&.status
end

#meUser

Fetch current user

Returns:

  • (User)

    Bot user



274
275
276
277
# File 'lib/discord_rda/bot.rb', line 274

def me
  data = @rest.get('/users/@me')
  User.new(data)
end

#modify_channel(channel_id, **options) ⇒ Channel

Modify channel

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • options (Hash)

    Settings to modify

Returns:



679
680
681
682
683
# File 'lib/discord_rda/bot.rb', line 679

def modify_channel(channel_id, **options)
  payload = options.slice(:name, :type, :position, :topic, :nsfw, :rate_limit_per_user, :bitrate, :user_limit, :parent_id, :default_auto_archive_duration)
  data = @rest.patch("/channels/#{channel_id}", body: payload)
  Channel.new(data)
end

#modify_guild_member(guild_id, user_id, **options) ⇒ Member

Modify a guild member (simplified)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • user_id (String, Snowflake)

    User ID

  • options (Hash)

    Options to modify (nick, roles, mute, deaf, channel_id)

Returns:



481
482
483
484
485
# File 'lib/discord_rda/bot.rb', line 481

def modify_guild_member(guild_id, user_id, **options)
  payload = options.slice(:nick, :roles, :mute, :deaf, :channel_id, :communication_disabled_until)
  data = @rest.patch("/guilds/#{guild_id}/members/#{user_id}", body: payload)
  Member.new(data.merge('guild_id' => guild_id.to_s))
end

#modify_guild_role(guild_id, role_id, **options) ⇒ Role

Modify guild role

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • role_id (String, Snowflake)

    Role ID

  • options (Hash)

    Settings to modify

Returns:

  • (Role)

    Updated role



545
546
547
548
549
# File 'lib/discord_rda/bot.rb', line 545

def modify_guild_role(guild_id, role_id, **options)
  payload = options.slice(:name, :permissions, :color, :hoist, :mentionable, :icon, :unicode_emoji)
  data = @rest.patch("/guilds/#{guild_id}/roles/#{role_id}", body: payload)
  Role.new(data.merge('guild_id' => guild_id.to_s))
end

#on(event) { ... } ⇒ Subscription

Register an event handler

Parameters:

  • event (String, Symbol)

    Event type

Yields:

  • Event handler block

Returns:

  • (Subscription)

    Subscription object



163
164
165
# File 'lib/discord_rda/bot.rb', line 163

def on(event, &block)
  @event_bus.on(event, &block)
end

#once(event) { ... } ⇒ Subscription

Register a one-time event handler

Parameters:

  • event (String, Symbol)

    Event type

Yields:

  • Event handler block

Returns:

  • (Subscription)

    Subscription object



171
172
173
# File 'lib/discord_rda/bot.rb', line 171

def once(event, &block)
  @event_bus.once(event, &block)
end

#register_command(name, description = '', options = []) { ... } ⇒ void Also known as: command

This method returns an undefined value.

Register a command

Parameters:

  • name (String)

    Command name

  • description (String) (defaults to: '')

    Command description

  • options (Array<Hash>) (defaults to: [])

    Command options

Yields:

  • Command handler



190
191
192
193
194
195
196
# File 'lib/discord_rda/bot.rb', line 190

def register_command(name, description = '', options = [], &block)
  @commands[name.to_s] = {
    description: description,
    options: options,
    handler: block
  }
end

#register_plugin(plugin) ⇒ Boolean Also known as: plugin

Register a plugin

Parameters:

  • plugin (Plugin)

    Plugin to register

Returns:

  • (Boolean)

    True if registered



202
203
204
# File 'lib/discord_rda/bot.rb', line 202

def register_plugin(plugin)
  @plugins.register(plugin, self)
end

#remove_all_reactions(channel_id, message_id) ⇒ void

This method returns an undefined value.

Remove all reactions from a message

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • message_id (String, Snowflake)

    Message ID



436
437
438
# File 'lib/discord_rda/bot.rb', line 436

def remove_all_reactions(channel_id, message_id)
  @rest.delete("/channels/#{channel_id}/messages/#{message_id}/reactions")
end

#remove_guild_ban(guild_id, user_id, reason: nil) ⇒ void

This method returns an undefined value.

Remove guild ban (unban)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • user_id (String, Snowflake)

    User ID

  • reason (String) (defaults to: nil)

    Audit log reason



601
602
603
604
# File 'lib/discord_rda/bot.rb', line 601

def remove_guild_ban(guild_id, user_id, reason: nil)
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  @rest.delete("/guilds/#{guild_id}/bans/#{user_id}", headers: headers)
end

#remove_guild_member(guild_id, user_id, reason: nil) ⇒ void

This method returns an undefined value.

Remove guild member (kick)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • user_id (String, Snowflake)

    User ID

  • reason (String) (defaults to: nil)

    Audit log reason



514
515
516
517
# File 'lib/discord_rda/bot.rb', line 514

def remove_guild_member(guild_id, user_id, reason: nil)
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  @rest.delete("/guilds/#{guild_id}/members/#{user_id}", headers: headers)
end

#remove_guild_member_role(guild_id, user_id, role_id, reason: nil) ⇒ void

This method returns an undefined value.

Remove role from guild member

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • user_id (String, Snowflake)

    User ID

  • role_id (String, Snowflake)

    Role ID

  • reason (String) (defaults to: nil)

    Audit log reason



504
505
506
507
# File 'lib/discord_rda/bot.rb', line 504

def remove_guild_member_role(guild_id, user_id, role_id, reason: nil)
  headers = reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
  @rest.delete("/guilds/#{guild_id}/members/#{user_id}/roles/#{role_id}", headers: headers)
end

#remove_reaction(channel_id, message_id, emoji, user_id: '@me') ⇒ void

This method returns an undefined value.

Remove a reaction from a message

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • message_id (String, Snowflake)

    Message ID

  • emoji (String, Emoji)

    Emoji

  • user_id (String, Snowflake) (defaults to: '@me')

    User ID (default: @me)



415
416
417
418
# File 'lib/discord_rda/bot.rb', line 415

def remove_reaction(channel_id, message_id, emoji, user_id: '@me')
  emoji_str = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
  @rest.delete("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}/#{user_id}")
end

#reshard_to(new_shard_count) ⇒ void

This method returns an undefined value.

Trigger zero-downtime resharding

Parameters:

  • new_shard_count (Integer)

    New shard count



369
370
371
372
# File 'lib/discord_rda/bot.rb', line 369

def reshard_to(new_shard_count)
  @logger.info('Triggering resharding', new_count: new_shard_count)
  @reshard_manager.reshard_to(new_shard_count)
end

#run(async: false) ⇒ void

This method returns an undefined value.

Run the bot

Parameters:

  • async (Boolean) (defaults to: false)

    Run asynchronously



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/discord_rda/bot.rb', line 217

def run(async: false)
  @running = true

  @logger.info('Starting DiscordRDA bot', version: VERSION, shards: @config.shards.length)

  # Start REST client
  @rest.start

  # Calculate shard count if auto
  shard_count = if @config.shards == [:auto]
                   @shard_manager.calculate_shard_count(:auto, @rest)
                 else
                   @config.shards.length
                 end

  @shard_manager.instance_variable_set(:@shard_count, shard_count)

  # Start shards
  if async
    Async { start_shards }
  else
    start_shards
  end
end

#search_guild_members(guild_id, query, limit: 25) ⇒ Array<Member>

Search guild members by query (simplified)

Parameters:

  • guild_id (String, Snowflake)

    Guild ID

  • query (String)

    Search query (username/nickname prefix)

  • limit (Integer) (defaults to: 25)

    Max results (1-100, default 25)

Returns:

  • (Array<Member>)

    Matching members



470
471
472
473
474
# File 'lib/discord_rda/bot.rb', line 470

def search_guild_members(guild_id, query, limit: 25)
  params = { query: query, limit: limit }
  data = @rest.get("/guilds/#{guild_id}/members/search", params: params)
  data.map { |m| Member.new(m.merge('guild_id' => guild_id.to_s)) }
end

#send_message(channel_id, content = nil, **options) ⇒ Message

Send a message to a channel

Parameters:

  • channel_id (String, Snowflake)

    Channel ID

  • content (String) (defaults to: nil)

    Message content

  • options (Hash)

    Message options

Returns:



314
315
316
317
318
# File 'lib/discord_rda/bot.rb', line 314

def send_message(channel_id, content = nil, **options)
  payload = { content: content }.merge(options).compact
  data = @rest.post("/channels/#{channel_id}/messages", body: payload)
  Message.new(data)
end

#setup_interaction_handlersObject



705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
# File 'lib/discord_rda/bot.rb', line 705

def setup_interaction_handlers
  # Handle slash commands
  @event_bus.on(:interaction_create) do |event|
    interaction = event.interaction

    if interaction.command?
      handle_slash_command(interaction)
    elsif interaction.component?
      handle_component(interaction)
    elsif interaction.autocomplete?
      handle_autocomplete(interaction)
    elsif interaction.modal_submit?
      handle_modal_submit(interaction)
    end
  end
end

#slash(name, description, **options) {|CommandBuilder| ... } ⇒ ApplicationCommand

Register a slash command (global or guild-specific)

Parameters:

  • name (String)

    Command name

  • description (String)

    Command description

  • options (Hash)

    Command options

Options Hash (**options):

  • :guild_id (String)

    Guild-specific command (nil for global)

  • :options (Array<Hash>)

    Command options

  • :default_member_permissions (Integer)

    Default required permissions

  • :dm_permission (Boolean)

    Whether works in DMs

Yields:

Returns:



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
# File 'lib/discord_rda/bot.rb', line 89

def slash(name, description, **options, &block)
  builder = CommandBuilder.new(name, description)
  builder.dm_allowed(options[:dm_permission]) if options.key?(:dm_permission)
  builder.default_permissions(options[:default_member_permissions]) if options[:default_member_permissions]
  builder.nsfw(options[:nsfw]) if options[:nsfw]

  block.call(builder) if block

  cmd = builder.build
  cmd.instance_variable_set(:@application_id, me.id.to_s) rescue nil
  cmd.instance_variable_set(:@guild_id, options[:guild_id].to_s) if options[:guild_id]

  key = options[:guild_id] ? "#{name}:#{options[:guild_id]}" : name
  @slash_commands[key] = cmd

  # Register with Discord if we have application ID
  if cmd.application_id
    if options[:guild_id]
      cmd.create_guild(self, options[:guild_id])
    else
      cmd.create_global(self)
    end
  end

  @logger.info('Registered slash command', name: name, guild: options[:guild_id] || 'global')
  cmd
end

#statusHash

Get bot status

Returns:

  • (Hash)

    Status information



263
264
265
266
267
268
269
270
# File 'lib/discord_rda/bot.rb', line 263

def status
  {
    running: @running,
    shards: @shard_manager.status,
    cache: @cache.stats,
    plugins: @plugins.stats
  }
end

#stopvoid

This method returns an undefined value.

Stop the bot



244
245
246
247
248
249
# File 'lib/discord_rda/bot.rb', line 244

def stop
  @logger.info('Stopping bot')
  @running = false
  @shard_manager.stop
  @rest.stop
end

#update_presence(status: 'online', activity: nil) ⇒ void

This method returns an undefined value.

Update bot presence

Parameters:

  • status (String) (defaults to: 'online')

    online, idle, dnd, invisible

  • activity (Hash) (defaults to: nil)

    Activity data



255
256
257
258
259
# File 'lib/discord_rda/bot.rb', line 255

def update_presence(status: 'online', activity: nil)
  @shard_manager.shards.each do |shard|
    shard.update_presence(status: status, activity: activity)
  end
end

#use(middleware) ⇒ void

This method returns an undefined value.

Use middleware

Parameters:



210
211
212
# File 'lib/discord_rda/bot.rb', line 210

def use(middleware)
  @event_bus.use(middleware)
end

#wait_for(event, timeout: nil) { ... } ⇒ Event?

Wait for an event

Parameters:

  • event (String, Symbol)

    Event type

  • timeout (Float) (defaults to: nil)

    Timeout in seconds

Yields:

  • Block to match event

Returns:

  • (Event, nil)

    Event or nil if timeout



180
181
182
# File 'lib/discord_rda/bot.rb', line 180

def wait_for(event, timeout: nil, &block)
  @event_bus.wait_for(event, timeout: timeout, &block)
end