discord_rda 0.1.3 → 0.2.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.
@@ -40,6 +40,24 @@ module DiscordRDA
40
40
  # @return [PluginRegistry] Plugin registry
41
41
  attr_reader :plugins
42
42
 
43
+ # @return [Tracer] Trace helper
44
+ attr_reader :tracer
45
+
46
+ # @return [ErrorTracker] Error tracking helper
47
+ attr_reader :error_tracker
48
+
49
+ # @return [RestartManager] Instant restart helper
50
+ attr_reader :restart_manager
51
+
52
+ # @return [ExecutionSupervisor] Fault-tolerant execution supervisor
53
+ attr_reader :supervisor
54
+
55
+ # @return [Hash] Boot-time restart state
56
+ attr_reader :restart_state
57
+
58
+ # @return [ActiveRecordSystem, nil] ActiveRecord integration helper
59
+ attr_reader :active_record
60
+
43
61
  # @return [Boolean] Whether bot is running
44
62
  attr_reader :running
45
63
 
@@ -51,15 +69,25 @@ module DiscordRDA
51
69
  # @param options [Hash] Configuration options
52
70
  def initialize(token:, **options)
53
71
  @config = Configuration.new(options.merge(token: token))
54
- @logger = Logger.new(level: @config.log_level, format: @config.log_format)
72
+ @logger = Logger.new(
73
+ level: @config.log_level,
74
+ format: @config.log_format,
75
+ file_path: @config.log_file_path,
76
+ rotate_age: @config.log_rotate_age,
77
+ rotate_size: @config.log_rotate_size
78
+ )
79
+ @restart_manager = RestartManager.new(logger: @logger)
80
+ @restart_state = @restart_manager.consume_boot_state
81
+ @tracer = Tracer.new(enabled: @config.trace_enabled, logger: @logger)
82
+ @error_tracker = ErrorTracker.new(enabled: @config.error_tracking, logger: @logger)
83
+ @supervisor = ExecutionSupervisor.new(logger: @logger)
55
84
  @event_bus = EventBus.new(logger: @logger)
56
85
  @cache = build_cache
57
- @shard_manager = ShardManager.new(@config, @event_bus, @logger)
86
+ @shard_manager = ShardManager.new(@config, @event_bus, @logger, gateway_state: restart_gateway_state)
58
87
  @rest = RestClient.new(@config, @logger)
59
88
 
60
89
  # Configure entity API clients
61
- Message.api = @rest
62
- Interaction.api = @rest
90
+ configure_entity_apis(@rest)
63
91
 
64
92
  setup_event_handlers
65
93
  setup_interaction_handlers
@@ -69,13 +97,25 @@ module DiscordRDA
69
97
  @reshard_manager = ReshardManager.new(self, @shard_manager, @logger)
70
98
  @hot_reload_manager = HotReloadManager.new(self, @logger)
71
99
  @plugins = PluginRegistry.new(logger: @logger)
100
+ @restart_manager.attach(self)
72
101
  @slash_commands = {}
73
102
  @running = false
74
103
  @commands = {}
104
+ @active_record = nil
75
105
 
76
106
  setup_event_handlers
77
107
  end
78
108
 
109
+ def restart!(command: nil, env: {})
110
+ @restart_manager.restart!(command: command, env: env)
111
+ end
112
+
113
+ def enable_active_record(database_url: nil, **options)
114
+ @active_record = ActiveRecordSystem.new(logger: @logger)
115
+ @active_record.connect(database_url: database_url, **options)
116
+ @active_record
117
+ end
118
+
79
119
  # Register a slash command (global or guild-specific)
80
120
  # @param name [String] Command name
81
121
  # @param description [String] Command description
@@ -87,31 +127,14 @@ module DiscordRDA
87
127
  # @yield [CommandBuilder] DSL block for building command
88
128
  # @return [ApplicationCommand] Registered command
89
129
  def slash(name, description, **options, &block)
90
- builder = CommandBuilder.new(name, description)
130
+ builder = CommandBuilder.new(name, description, type: options[:type] || 1)
91
131
  builder.dm_allowed(options[:dm_permission]) if options.key?(:dm_permission)
92
132
  builder.default_permissions(options[:default_member_permissions]) if options[:default_member_permissions]
93
133
  builder.nsfw(options[:nsfw]) if options[:nsfw]
94
134
 
95
135
  block.call(builder) if block
96
136
 
97
- cmd = builder.build
98
- cmd.instance_variable_set(:@application_id, me.id.to_s) rescue nil
99
- cmd.instance_variable_set(:@guild_id, options[:guild_id].to_s) if options[:guild_id]
100
-
101
- key = options[:guild_id] ? "#{name}:#{options[:guild_id]}" : name
102
- @slash_commands[key] = cmd
103
-
104
- # Register with Discord if we have application ID
105
- if cmd.application_id
106
- if options[:guild_id]
107
- cmd.create_guild(self, options[:guild_id])
108
- else
109
- cmd.create_global(self)
110
- end
111
- end
112
-
113
- @logger.info('Registered slash command', name: name, guild: options[:guild_id] || 'global')
114
- cmd
137
+ register_application_command(builder.build, name: name, guild_id: options[:guild_id])
115
138
  end
116
139
 
117
140
  # Register a context menu command (user or message)
@@ -122,10 +145,13 @@ module DiscordRDA
122
145
  # @return [ApplicationCommand] Registered command
123
146
  def context_menu(type:, name:, **options, &block)
124
147
  cmd_type = type == :user ? 2 : 3
125
- options[:type] = cmd_type
126
- options[:description] = '' # Context menus don't have descriptions
148
+ builder = CommandBuilder.new(name, '', type: cmd_type)
149
+ builder.dm_allowed(options[:dm_permission]) if options.key?(:dm_permission)
150
+ builder.default_permissions(options[:default_member_permissions]) if options[:default_member_permissions]
151
+ builder.nsfw(options[:nsfw]) if options[:nsfw]
152
+ builder.handler(&block) if block
127
153
 
128
- slash(name, '', **options, &block)
154
+ register_application_command(builder.build, name: name, guild_id: options[:guild_id])
129
155
  end
130
156
 
131
157
  # Bulk register global commands (replaces existing)
@@ -216,6 +242,7 @@ module DiscordRDA
216
242
  # @return [void]
217
243
  def run(async: false)
218
244
  @running = true
245
+ install_signal_handlers
219
246
 
220
247
  @logger.info('Starting DiscordRDA bot', version: VERSION, shards: @config.shards.length)
221
248
 
@@ -291,6 +318,33 @@ module DiscordRDA
291
318
  nil
292
319
  end
293
320
 
321
+ # Create a guild
322
+ # @param name [String] Guild name
323
+ # @param options [Hash] Optional guild creation payload
324
+ # @return [Guild] Created guild
325
+ def create_guild(name:, **options)
326
+ payload = { name: name }.merge(options).compact
327
+ data = @rest.post('/guilds', body: payload)
328
+ Guild.new(data)
329
+ end
330
+
331
+ # Modify a guild
332
+ # @param guild_id [String, Snowflake] Guild ID
333
+ # @param reason [String, nil] Audit log reason
334
+ # @param options [Hash] Guild modification payload
335
+ # @return [Guild] Updated guild
336
+ def modify_guild(guild_id, reason: nil, **options)
337
+ data = @rest.patch("/guilds/#{guild_id}", body: options.compact, headers: audit_log_headers(reason))
338
+ Guild.new(data)
339
+ end
340
+
341
+ # Delete a guild
342
+ # @param guild_id [String, Snowflake] Guild ID
343
+ # @return [void]
344
+ def delete_guild(guild_id)
345
+ @rest.delete("/guilds/#{guild_id}")
346
+ end
347
+
294
348
  # Get a channel by ID
295
349
  # @param channel_id [String, Snowflake] Channel ID
296
350
  # @return [Channel, nil] Channel or nil
@@ -317,7 +371,16 @@ module DiscordRDA
317
371
  Message.new(data)
318
372
  end
319
373
 
320
- # Get messages from a channel with pagination (simplified)
374
+ # Crosspost a message in an announcement channel
375
+ # @param channel_id [String, Snowflake] Channel ID
376
+ # @param message_id [String, Snowflake] Message ID
377
+ # @return [Message] Crossposted message
378
+ def crosspost_message(channel_id, message_id)
379
+ data = @rest.post("/channels/#{channel_id}/messages/#{message_id}/crosspost")
380
+ Message.new(data)
381
+ end
382
+
383
+ # Get messages from a channel with pagination
321
384
  # @param channel_id [String, Snowflake] Channel ID
322
385
  # @param limit [Integer] Max messages to fetch (1-100, default 50)
323
386
  # @param before [String, Snowflake] Get messages before this ID
@@ -345,6 +408,226 @@ module DiscordRDA
345
408
  nil
346
409
  end
347
410
 
411
+ # Add a recipient to a group DM
412
+ # @param channel_id [String, Snowflake] Group DM channel ID
413
+ # @param user_id [String, Snowflake] User ID
414
+ # @param access_token [String] OAuth2 access token with gdm.join scope
415
+ # @param nick [String, nil] Nickname for the recipient in the group DM
416
+ # @return [void]
417
+ def add_group_dm_recipient(channel_id, user_id, access_token:, nick: nil)
418
+ payload = { access_token: access_token, nick: nick }.compact
419
+ @rest.put("/channels/#{channel_id}/recipients/#{user_id}", body: payload)
420
+ end
421
+
422
+ # Remove a recipient from a group DM
423
+ # @param channel_id [String, Snowflake] Group DM channel ID
424
+ # @param user_id [String, Snowflake] User ID
425
+ # @return [void]
426
+ def remove_group_dm_recipient(channel_id, user_id)
427
+ @rest.delete("/channels/#{channel_id}/recipients/#{user_id}")
428
+ end
429
+
430
+ # Trigger typing indicator for a channel
431
+ # @param channel_id [String, Snowflake] Channel ID
432
+ # @return [void]
433
+ def trigger_typing(channel_id)
434
+ @rest.post("/channels/#{channel_id}/typing")
435
+ end
436
+
437
+ # Get pinned messages for a channel
438
+ # @param channel_id [String, Snowflake] Channel ID
439
+ # @return [Array<Message>] Pinned messages
440
+ def pinned_messages(channel_id)
441
+ data = @rest.get("/channels/#{channel_id}/pins")
442
+ data.map { |message| Message.new(message) }
443
+ end
444
+
445
+ # Pin a channel message
446
+ # @param channel_id [String, Snowflake] Channel ID
447
+ # @param message_id [String, Snowflake] Message ID
448
+ # @param reason [String, nil] Audit log reason
449
+ # @return [void]
450
+ def pin_message(channel_id, message_id, reason: nil)
451
+ @rest.put("/channels/#{channel_id}/pins/#{message_id}", headers: audit_log_headers(reason))
452
+ end
453
+
454
+ # Unpin a channel message
455
+ # @param channel_id [String, Snowflake] Channel ID
456
+ # @param message_id [String, Snowflake] Message ID
457
+ # @param reason [String, nil] Audit log reason
458
+ # @return [void]
459
+ def unpin_message(channel_id, message_id, reason: nil)
460
+ @rest.delete("/channels/#{channel_id}/pins/#{message_id}", headers: audit_log_headers(reason))
461
+ end
462
+
463
+ # Edit a channel permission overwrite
464
+ # @param channel_id [String, Snowflake] Channel ID
465
+ # @param overwrite_id [String, Snowflake] Role or member ID
466
+ # @param allow [Integer, String] Allowed permissions bitfield
467
+ # @param deny [Integer, String] Denied permissions bitfield
468
+ # @param type [Integer] 0 for role, 1 for member
469
+ # @param reason [String, nil] Audit log reason
470
+ # @return [void]
471
+ def edit_channel_permissions(channel_id, overwrite_id, allow:, deny:, type:, reason: nil)
472
+ payload = {
473
+ allow: allow.to_s,
474
+ deny: deny.to_s,
475
+ type: type
476
+ }
477
+ @rest.put("/channels/#{channel_id}/permissions/#{overwrite_id}", body: payload, headers: audit_log_headers(reason))
478
+ end
479
+
480
+ # Delete a channel permission overwrite
481
+ # @param channel_id [String, Snowflake] Channel ID
482
+ # @param overwrite_id [String, Snowflake] Role or member overwrite ID
483
+ # @param reason [String, nil] Audit log reason
484
+ # @return [void]
485
+ def delete_channel_permission(channel_id, overwrite_id, reason: nil)
486
+ @rest.delete("/channels/#{channel_id}/permissions/#{overwrite_id}", headers: audit_log_headers(reason))
487
+ end
488
+
489
+ # Get invites for a channel
490
+ # @param channel_id [String, Snowflake] Channel ID
491
+ # @return [Array<Hash>] Invite payloads
492
+ def channel_invites(channel_id)
493
+ @rest.get("/channels/#{channel_id}/invites")
494
+ end
495
+
496
+ # Create a channel invite
497
+ # @param channel_id [String, Snowflake] Channel ID
498
+ # @param reason [String, nil] Audit log reason
499
+ # @yield [InviteBuilder] Optional invite builder block
500
+ # @return [Hash] Invite payload
501
+ def create_channel_invite(channel_id, reason: nil, **options, &block)
502
+ builder = InviteBuilder.new
503
+ options.each do |key, value|
504
+ builder.public_send(key, value) if builder.respond_to?(key)
505
+ end
506
+ block.call(builder) if block
507
+
508
+ @rest.post("/channels/#{channel_id}/invites", body: builder.to_h, headers: audit_log_headers(reason))
509
+ end
510
+
511
+ # Follow an announcement channel into a target channel
512
+ # @param channel_id [String, Snowflake] Source announcement channel ID
513
+ # @param webhook_channel_id [String, Snowflake] Destination channel ID
514
+ # @return [Hash] Followed channel response
515
+ def follow_news_channel(channel_id, webhook_channel_id)
516
+ @rest.post("/channels/#{channel_id}/followers", body: { webhook_channel_id: webhook_channel_id.to_s })
517
+ end
518
+
519
+ # Start a thread from an existing message
520
+ # @param channel_id [String, Snowflake] Parent channel ID
521
+ # @param message_id [String, Snowflake] Message ID
522
+ # @param name [String] Thread name
523
+ # @param auto_archive_duration [Integer, nil] Auto archive duration in minutes
524
+ # @param rate_limit_per_user [Integer, nil] Thread slowmode
525
+ # @return [Channel] Created thread
526
+ def start_thread_from_message(channel_id, message_id, name:, auto_archive_duration: nil, rate_limit_per_user: nil)
527
+ payload = {
528
+ name: name,
529
+ auto_archive_duration: auto_archive_duration,
530
+ rate_limit_per_user: rate_limit_per_user
531
+ }.compact
532
+ data = @rest.post("/channels/#{channel_id}/messages/#{message_id}/threads", body: payload)
533
+ Channel.new(data)
534
+ end
535
+
536
+ # Start a thread without a seed message
537
+ # @param channel_id [String, Snowflake] Parent channel ID
538
+ # @param name [String] Thread name
539
+ # @param type [Integer, nil] Thread type
540
+ # @param auto_archive_duration [Integer, nil] Auto archive duration in minutes
541
+ # @param invitable [Boolean, nil] Whether non-moderators can add users
542
+ # @param rate_limit_per_user [Integer, nil] Thread slowmode
543
+ # @return [Channel] Created thread
544
+ def start_thread(channel_id, name:, type: nil, auto_archive_duration: nil, invitable: nil, rate_limit_per_user: nil)
545
+ payload = {
546
+ name: name,
547
+ type: type,
548
+ auto_archive_duration: auto_archive_duration,
549
+ invitable: invitable,
550
+ rate_limit_per_user: rate_limit_per_user
551
+ }.compact
552
+ data = @rest.post("/channels/#{channel_id}/threads", body: payload)
553
+ Channel.new(data)
554
+ end
555
+
556
+ # Join a thread
557
+ # @param thread_id [String, Snowflake] Thread channel ID
558
+ # @return [void]
559
+ def join_thread(thread_id)
560
+ @rest.put("/channels/#{thread_id}/thread-members/@me")
561
+ end
562
+
563
+ # Add a user to a thread
564
+ # @param thread_id [String, Snowflake] Thread channel ID
565
+ # @param user_id [String, Snowflake] User ID
566
+ # @return [void]
567
+ def add_thread_member(thread_id, user_id)
568
+ @rest.put("/channels/#{thread_id}/thread-members/#{user_id}")
569
+ end
570
+
571
+ # Leave a thread
572
+ # @param thread_id [String, Snowflake] Thread channel ID
573
+ # @return [void]
574
+ def leave_thread(thread_id)
575
+ @rest.delete("/channels/#{thread_id}/thread-members/@me")
576
+ end
577
+
578
+ # Remove a user from a thread
579
+ # @param thread_id [String, Snowflake] Thread channel ID
580
+ # @param user_id [String, Snowflake] User ID
581
+ # @return [void]
582
+ def remove_thread_member(thread_id, user_id)
583
+ @rest.delete("/channels/#{thread_id}/thread-members/#{user_id}")
584
+ end
585
+
586
+ # Get a specific thread member
587
+ # @param thread_id [String, Snowflake] Thread channel ID
588
+ # @param user_id [String, Snowflake] User ID
589
+ # @param with_member [Boolean] Include member object when available
590
+ # @return [Hash, nil] Thread member payload
591
+ def thread_member(thread_id, user_id, with_member: false)
592
+ @rest.get("/channels/#{thread_id}/thread-members/#{user_id}", params: { with_member: with_member })
593
+ rescue RestClient::NotFoundError
594
+ nil
595
+ end
596
+
597
+ # List members in a thread
598
+ # @param thread_id [String, Snowflake] Thread channel ID
599
+ # @param with_member [Boolean] Include member objects when available
600
+ # @param after [String, Snowflake, nil] Cursor
601
+ # @param limit [Integer, nil] Max results
602
+ # @return [Array<Hash>] Thread member payloads
603
+ def thread_members(thread_id, with_member: false, after: nil, limit: nil)
604
+ params = { with_member: with_member }
605
+ params[:after] = after.to_s if after
606
+ params[:limit] = limit if limit
607
+ @rest.get("/channels/#{thread_id}/thread-members", params: params)
608
+ end
609
+
610
+ # List archived threads for a channel
611
+ # @param channel_id [String, Snowflake] Parent channel ID
612
+ # @param scope [Symbol] :public, :private, or :joined_private
613
+ # @param before [Time, String, nil] ISO8601 timestamp cursor
614
+ # @param limit [Integer, nil] Max threads to return
615
+ # @return [Hash] Archived thread response
616
+ def archived_threads(channel_id, scope: :public, before: nil, limit: nil)
617
+ path = case scope
618
+ when :public then "/channels/#{channel_id}/threads/archived/public"
619
+ when :private then "/channels/#{channel_id}/threads/archived/private"
620
+ when :joined_private then "/channels/#{channel_id}/users/@me/threads/archived/private"
621
+ else
622
+ raise ArgumentError, "Unknown archived thread scope: #{scope}"
623
+ end
624
+
625
+ params = {}
626
+ params[:before] = before.is_a?(Time) ? before.iso8601 : before if before
627
+ params[:limit] = limit if limit
628
+ @rest.get(path, params: params)
629
+ end
630
+
348
631
  # Enable scalable REST client (queue-based rate limiting)
349
632
  # @param proxy [Hash] Optional proxy configuration
350
633
  # @return [void]
@@ -352,6 +635,7 @@ module DiscordRDA
352
635
  @logger.info('Enabling scalable REST client')
353
636
  @scalable_rest = ScalableRestClient.new(@config, @logger, proxy: proxy)
354
637
  @scalable_rest.start
638
+ configure_entity_apis(@scalable_rest)
355
639
  end
356
640
 
357
641
  # Enable hot reload for development
@@ -394,7 +678,7 @@ module DiscordRDA
394
678
  analytics_plugin&.summary || {}
395
679
  end
396
680
 
397
- # === Message Reactions (Simplified) ===
681
+ # === Message Reactions ===
398
682
 
399
683
  # Add a reaction to a message
400
684
  # @param channel_id [String, Snowflake] Channel ID
@@ -417,15 +701,18 @@ module DiscordRDA
417
701
  @rest.delete("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}/#{user_id}")
418
702
  end
419
703
 
420
- # Get reactions for a message (simplified - no pagination)
704
+ # Get reactions for a message
421
705
  # @param channel_id [String, Snowflake] Channel ID
422
706
  # @param message_id [String, Snowflake] Message ID
423
707
  # @param emoji [String, Emoji] Emoji filter
424
708
  # @param limit [Integer] Max users to return (1-100, default 25)
709
+ # @param after [String, Snowflake, nil] Cursor for pagination
425
710
  # @return [Array<User>] Users who reacted
426
- def get_reactions(channel_id, message_id, emoji, limit: 25)
711
+ def get_reactions(channel_id, message_id, emoji, limit: 25, after: nil)
427
712
  emoji_str = emoji.respond_to?(:id) ? "#{emoji.name}:#{emoji.id}" : emoji.to_s
428
- data = @rest.get("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}", params: { limit: limit })
713
+ params = { limit: limit }
714
+ params[:after] = after.to_s if after
715
+ data = @rest.get("/channels/#{channel_id}/messages/#{message_id}/reactions/#{CGI.escape(emoji_str)}", params: params)
429
716
  data.map { |u| User.new(u) }
430
717
  end
431
718
 
@@ -437,7 +724,7 @@ module DiscordRDA
437
724
  @rest.delete("/channels/#{channel_id}/messages/#{message_id}/reactions")
438
725
  end
439
726
 
440
- # === Guild Members (Simplified) ===
727
+ # === Guild Members ===
441
728
 
442
729
  # Get a guild member
443
730
  # @param guild_id [String, Snowflake] Guild ID
@@ -450,7 +737,7 @@ module DiscordRDA
450
737
  nil
451
738
  end
452
739
 
453
- # List guild members (simplified - basic pagination)
740
+ # List guild members with pagination
454
741
  # @param guild_id [String, Snowflake] Guild ID
455
742
  # @param limit [Integer] Max members (1-1000, default 100)
456
743
  # @param after [String, Snowflake] Get members after this user ID
@@ -462,7 +749,7 @@ module DiscordRDA
462
749
  data.map { |m| Member.new(m.merge('guild_id' => guild_id.to_s)) }
463
750
  end
464
751
 
465
- # Search guild members by query (simplified)
752
+ # Search guild members by query
466
753
  # @param guild_id [String, Snowflake] Guild ID
467
754
  # @param query [String] Search query (username/nickname prefix)
468
755
  # @param limit [Integer] Max results (1-100, default 25)
@@ -473,7 +760,7 @@ module DiscordRDA
473
760
  data.map { |m| Member.new(m.merge('guild_id' => guild_id.to_s)) }
474
761
  end
475
762
 
476
- # Modify a guild member (simplified)
763
+ # Modify a guild member
477
764
  # @param guild_id [String, Snowflake] Guild ID
478
765
  # @param user_id [String, Snowflake] User ID
479
766
  # @param options [Hash] Options to modify (nick, roles, mute, deaf, channel_id)
@@ -484,6 +771,42 @@ module DiscordRDA
484
771
  Member.new(data.merge('guild_id' => guild_id.to_s))
485
772
  end
486
773
 
774
+ # Add a member to a guild through OAuth2
775
+ # @param guild_id [String, Snowflake] Guild ID
776
+ # @param user_id [String, Snowflake] User ID
777
+ # @param access_token [String] User OAuth2 access token
778
+ # @param options [Hash] Optional member settings
779
+ # @return [Hash] Discord add-member response
780
+ def add_guild_member(guild_id, user_id, access_token:, **options)
781
+ payload = {
782
+ access_token: access_token,
783
+ nick: options[:nick],
784
+ roles: options[:roles],
785
+ mute: options[:mute],
786
+ deaf: options[:deaf]
787
+ }.compact
788
+ @rest.put("/guilds/#{guild_id}/members/#{user_id}", body: payload)
789
+ end
790
+
791
+ # Modify the current bot member in a guild
792
+ # @param guild_id [String, Snowflake] Guild ID
793
+ # @param nick [String, nil] New nickname
794
+ # @param reason [String, nil] Audit log reason
795
+ # @return [Member] Updated member
796
+ def modify_current_member(guild_id, nick: nil, reason: nil)
797
+ data = @rest.patch("/guilds/#{guild_id}/members/@me", body: { nick: nick }.compact, headers: audit_log_headers(reason))
798
+ Member.new(data.merge('guild_id' => guild_id.to_s))
799
+ end
800
+
801
+ # Modify the current bot nickname in a guild
802
+ # @param guild_id [String, Snowflake] Guild ID
803
+ # @param nick [String, nil] New nickname
804
+ # @param reason [String, nil] Audit log reason
805
+ # @return [Hash] Discord nickname response
806
+ def modify_current_user_nick(guild_id, nick: nil, reason: nil)
807
+ @rest.patch("/guilds/#{guild_id}/members/@me/nick", body: { nick: nick }.compact, headers: audit_log_headers(reason))
808
+ end
809
+
487
810
  # Add role to guild member
488
811
  # @param guild_id [String, Snowflake] Guild ID
489
812
  # @param user_id [String, Snowflake] User ID
@@ -516,7 +839,7 @@ module DiscordRDA
516
839
  @rest.delete("/guilds/#{guild_id}/members/#{user_id}", headers: headers)
517
840
  end
518
841
 
519
- # === Guild Roles (Simplified) ===
842
+ # === Guild Roles ===
520
843
 
521
844
  # Get guild roles
522
845
  # @param guild_id [String, Snowflake] Guild ID
@@ -526,14 +849,14 @@ module DiscordRDA
526
849
  data.map { |r| Role.new(r.merge('guild_id' => guild_id.to_s)) }
527
850
  end
528
851
 
529
- # Create guild role (simplified)
852
+ # Create guild role
530
853
  # @param guild_id [String, Snowflake] Guild ID
531
854
  # @param name [String] Role name
532
855
  # @param options [Hash] Optional settings (permissions, color, hoist, mentionable)
533
856
  # @return [Role] Created role
534
- def create_guild_role(guild_id, name:, **options)
857
+ def create_guild_role(guild_id, name:, reason: nil, **options)
535
858
  payload = { name: name }.merge(options.slice(:permissions, :color, :hoist, :mentionable, :icon, :unicode_emoji))
536
- data = @rest.post("/guilds/#{guild_id}/roles", body: payload)
859
+ data = @rest.post("/guilds/#{guild_id}/roles", body: payload, headers: audit_log_headers(reason))
537
860
  Role.new(data.merge('guild_id' => guild_id.to_s))
538
861
  end
539
862
 
@@ -542,12 +865,25 @@ module DiscordRDA
542
865
  # @param role_id [String, Snowflake] Role ID
543
866
  # @param options [Hash] Settings to modify
544
867
  # @return [Role] Updated role
545
- def modify_guild_role(guild_id, role_id, **options)
868
+ def modify_guild_role(guild_id, role_id, reason: nil, **options)
546
869
  payload = options.slice(:name, :permissions, :color, :hoist, :mentionable, :icon, :unicode_emoji)
547
- data = @rest.patch("/guilds/#{guild_id}/roles/#{role_id}", body: payload)
870
+ data = @rest.patch("/guilds/#{guild_id}/roles/#{role_id}", body: payload, headers: audit_log_headers(reason))
548
871
  Role.new(data.merge('guild_id' => guild_id.to_s))
549
872
  end
550
873
 
874
+ # Modify guild role positions
875
+ # @param guild_id [String, Snowflake] Guild ID
876
+ # @param positions [Array<Hash>] Array of { id:, position: }
877
+ # @param reason [String, nil] Audit log reason
878
+ # @return [Array<Role>] Updated role ordering
879
+ def modify_guild_role_positions(guild_id, positions, reason: nil)
880
+ payload = positions.map do |position|
881
+ { id: (position[:id] || position['id']).to_s, position: position[:position] || position['position'] }
882
+ end
883
+ data = @rest.patch("/guilds/#{guild_id}/roles", body: payload, headers: audit_log_headers(reason))
884
+ data.map { |role| Role.new(role.merge('guild_id' => guild_id.to_s)) }
885
+ end
886
+
551
887
  # Delete guild role
552
888
  # @param guild_id [String, Snowflake] Guild ID
553
889
  # @param role_id [String, Snowflake] Role ID
@@ -558,14 +894,19 @@ module DiscordRDA
558
894
  @rest.delete("/guilds/#{guild_id}/roles/#{role_id}", headers: headers)
559
895
  end
560
896
 
561
- # === Guild Bans (Simplified) ===
897
+ # === Guild Bans ===
562
898
 
563
- # Get guild bans (simplified - no pagination)
899
+ # Get guild bans
564
900
  # @param guild_id [String, Snowflake] Guild ID
565
901
  # @param limit [Integer] Max bans (1-1000, default 100)
902
+ # @param before [String, Snowflake, nil] Cursor for pagination
903
+ # @param after [String, Snowflake, nil] Cursor for pagination
566
904
  # @return [Array<Hash>] Bans (user + reason data)
567
- def guild_bans(guild_id, limit: 100)
568
- data = @rest.get("/guilds/#{guild_id}/bans", params: { limit: limit })
905
+ def guild_bans(guild_id, limit: 100, before: nil, after: nil)
906
+ params = { limit: limit }
907
+ params[:before] = before.to_s if before
908
+ params[:after] = after.to_s if after
909
+ data = @rest.get("/guilds/#{guild_id}/bans", params: params)
569
910
  data.map { |b| { user: User.new(b['user']), reason: b['reason'] } }
570
911
  end
571
912
 
@@ -603,42 +944,48 @@ module DiscordRDA
603
944
  @rest.delete("/guilds/#{guild_id}/bans/#{user_id}", headers: headers)
604
945
  end
605
946
 
606
- # === Webhooks (Simplified) ===
947
+ # === Webhooks ===
607
948
 
608
949
  # Create a webhook
609
950
  # @param channel_id [String, Snowflake] Channel ID
610
951
  # @param name [String] Webhook name
611
952
  # @param avatar [String] Base64-encoded avatar image (optional)
612
- # @return [Hash] Webhook data
613
- def create_webhook(channel_id, name:, avatar: nil)
953
+ # @return [Webhook] Webhook data
954
+ def create_webhook(channel_id, name:, avatar: nil, reason: nil)
614
955
  payload = { name: name }
615
956
  payload[:avatar] = avatar if avatar
616
- @rest.post("/channels/#{channel_id}/webhooks", body: payload)
957
+ Webhook.new(@rest.post("/channels/#{channel_id}/webhooks", body: payload, headers: audit_log_headers(reason)))
617
958
  end
618
959
 
619
960
  # Get channel webhooks
620
961
  # @param channel_id [String, Snowflake] Channel ID
621
- # @return [Array<Hash>] Webhooks
962
+ # @return [Array<Webhook>] Webhooks
622
963
  def channel_webhooks(channel_id)
623
- @rest.get("/channels/#{channel_id}/webhooks")
964
+ @rest.get("/channels/#{channel_id}/webhooks").map { |hook| Webhook.new(hook) }
624
965
  end
625
966
 
626
967
  # Get guild webhooks
627
968
  # @param guild_id [String, Snowflake] Guild ID
628
- # @return [Array<Hash>] Webhooks
969
+ # @return [Array<Webhook>] Webhooks
629
970
  def guild_webhooks(guild_id)
630
- @rest.get("/guilds/#{guild_id}/webhooks")
971
+ @rest.get("/guilds/#{guild_id}/webhooks").map { |hook| Webhook.new(hook) }
631
972
  end
632
973
 
633
- # Execute webhook (simplified)
974
+ # Execute webhook
634
975
  # @param webhook_id [String, Snowflake] Webhook ID
635
976
  # @param token [String] Webhook token
636
977
  # @param content [String] Message content
637
978
  # @param options [Hash] Options (username, avatar_url, embeds, etc.)
638
- # @return [void]
979
+ # @option options [Boolean] :wait Return the created message
980
+ # @option options [String, Snowflake] :thread_id Execute in a thread
981
+ # @return [Message, nil]
639
982
  def execute_webhook(webhook_id, token, content = nil, **options)
640
- payload = { content: content }.merge(options.slice(:username, :avatar_url, :embeds, :components, :allowed_mentions))
641
- @rest.post("/webhooks/#{webhook_id}/#{token}", body: payload)
983
+ params = {}
984
+ params[:wait] = options[:wait] unless options[:wait].nil?
985
+ params[:thread_id] = options[:thread_id].to_s if options[:thread_id]
986
+ payload = { content: content }.merge(options.slice(:username, :avatar_url, :embeds, :components, :allowed_mentions, :tts))
987
+ response = @rest.post("/webhooks/#{webhook_id}/#{token}", body: payload, params: params)
988
+ response.is_a?(Hash) ? Message.new(response) : nil
642
989
  end
643
990
 
644
991
  # Delete a webhook
@@ -650,7 +997,109 @@ module DiscordRDA
650
997
  @rest.delete(path)
651
998
  end
652
999
 
653
- # === Channel Management (Simplified) ===
1000
+ # Get a webhook by ID
1001
+ # @param webhook_id [String, Snowflake] Webhook ID
1002
+ # @return [Webhook, nil] Webhook payload
1003
+ def webhook(webhook_id)
1004
+ Webhook.new(@rest.get("/webhooks/#{webhook_id}"))
1005
+ rescue RestClient::NotFoundError
1006
+ nil
1007
+ end
1008
+
1009
+ # Get a webhook by ID and token
1010
+ # @param webhook_id [String, Snowflake] Webhook ID
1011
+ # @param token [String] Webhook token
1012
+ # @return [Webhook, nil] Webhook payload
1013
+ def webhook_with_token(webhook_id, token)
1014
+ Webhook.new(@rest.get("/webhooks/#{webhook_id}/#{token}"))
1015
+ rescue RestClient::NotFoundError
1016
+ nil
1017
+ end
1018
+
1019
+ # Modify a webhook
1020
+ # @param webhook_id [String, Snowflake] Webhook ID
1021
+ # @param name [String, nil] New webhook name
1022
+ # @param avatar [String, nil] Base64 avatar data
1023
+ # @param channel_id [String, Snowflake, nil] New channel ID
1024
+ # @return [Webhook] Updated webhook payload
1025
+ def modify_webhook(webhook_id, name: nil, avatar: nil, channel_id: nil, reason: nil)
1026
+ payload = { name: name, avatar: avatar, channel_id: channel_id&.to_s }.compact
1027
+ Webhook.new(@rest.patch("/webhooks/#{webhook_id}", body: payload, headers: audit_log_headers(reason)))
1028
+ end
1029
+
1030
+ # Modify a webhook using its token
1031
+ # @param webhook_id [String, Snowflake] Webhook ID
1032
+ # @param token [String] Webhook token
1033
+ # @param name [String, nil] New webhook name
1034
+ # @param avatar [String, nil] Base64 avatar data
1035
+ # @return [Webhook] Updated webhook payload
1036
+ def modify_webhook_with_token(webhook_id, token, name: nil, avatar: nil)
1037
+ payload = { name: name, avatar: avatar }.compact
1038
+ Webhook.new(@rest.patch("/webhooks/#{webhook_id}/#{token}", body: payload))
1039
+ end
1040
+
1041
+ # Execute a Slack-compatible webhook
1042
+ # @param webhook_id [String, Snowflake] Webhook ID
1043
+ # @param token [String] Webhook token
1044
+ # @param payload [Hash] Slack webhook payload
1045
+ # @return [Object] API response
1046
+ def execute_slack_webhook(webhook_id, token, payload)
1047
+ @rest.post("/webhooks/#{webhook_id}/#{token}/slack", body: payload)
1048
+ end
1049
+
1050
+ # Execute a GitHub-compatible webhook
1051
+ # @param webhook_id [String, Snowflake] Webhook ID
1052
+ # @param token [String] Webhook token
1053
+ # @param payload [Hash] GitHub webhook payload
1054
+ # @return [Object] API response
1055
+ def execute_github_webhook(webhook_id, token, payload)
1056
+ @rest.post("/webhooks/#{webhook_id}/#{token}/github", body: payload)
1057
+ end
1058
+
1059
+ # Get a webhook message
1060
+ # @param webhook_id [String, Snowflake] Webhook ID
1061
+ # @param token [String] Webhook token
1062
+ # @param message_id [String, Snowflake] Message ID
1063
+ # @param thread_id [String, Snowflake, nil] Thread channel ID
1064
+ # @return [Message, nil] Webhook message
1065
+ def webhook_message(webhook_id, token, message_id, thread_id: nil)
1066
+ params = {}
1067
+ params[:thread_id] = thread_id.to_s if thread_id
1068
+ data = @rest.get("/webhooks/#{webhook_id}/#{token}/messages/#{message_id}", params: params)
1069
+ Message.new(data)
1070
+ rescue RestClient::NotFoundError
1071
+ nil
1072
+ end
1073
+
1074
+ # Edit a webhook message
1075
+ # @param webhook_id [String, Snowflake] Webhook ID
1076
+ # @param token [String] Webhook token
1077
+ # @param message_id [String, Snowflake] Message ID
1078
+ # @param thread_id [String, Snowflake, nil] Thread channel ID
1079
+ # @param content [String, nil] New content
1080
+ # @param options [Hash] Additional edit payload
1081
+ # @return [Message] Updated message
1082
+ def edit_webhook_message(webhook_id, token, message_id, thread_id: nil, content: nil, **options)
1083
+ params = {}
1084
+ params[:thread_id] = thread_id.to_s if thread_id
1085
+ payload = { content: content }.merge(options).compact
1086
+ data = @rest.patch("/webhooks/#{webhook_id}/#{token}/messages/#{message_id}", body: payload, params: params)
1087
+ Message.new(data)
1088
+ end
1089
+
1090
+ # Delete a webhook message
1091
+ # @param webhook_id [String, Snowflake] Webhook ID
1092
+ # @param token [String] Webhook token
1093
+ # @param message_id [String, Snowflake] Message ID
1094
+ # @param thread_id [String, Snowflake, nil] Thread channel ID
1095
+ # @return [void]
1096
+ def delete_webhook_message(webhook_id, token, message_id, thread_id: nil)
1097
+ params = {}
1098
+ params[:thread_id] = thread_id.to_s if thread_id
1099
+ @rest.delete("/webhooks/#{webhook_id}/#{token}/messages/#{message_id}", params: params)
1100
+ end
1101
+
1102
+ # === Channel Management ===
654
1103
 
655
1104
  # Get guild channels
656
1105
  # @param guild_id [String, Snowflake] Guild ID
@@ -660,7 +1109,547 @@ module DiscordRDA
660
1109
  data.map { |c| Channel.new(c) }
661
1110
  end
662
1111
 
663
- # Create guild channel (simplified)
1112
+ # Get a guild preview
1113
+ # @param guild_id [String, Snowflake] Guild ID
1114
+ # @return [Hash, nil] Guild preview payload
1115
+ def guild_preview(guild_id)
1116
+ @rest.get("/guilds/#{guild_id}/preview")
1117
+ rescue RestClient::NotFoundError
1118
+ nil
1119
+ end
1120
+
1121
+ # Get the expected prune count for a guild
1122
+ # @param guild_id [String, Snowflake] Guild ID
1123
+ # @param days [Integer] Inactive days threshold
1124
+ # @param include_roles [Array<String, Snowflake>] Optional role IDs
1125
+ # @return [Integer, nil] Number of prunable members
1126
+ def guild_prune_count(guild_id, days:, include_roles: nil)
1127
+ params = { days: days }
1128
+ params[:include_roles] = Array(include_roles).map(&:to_s).join(',') if include_roles
1129
+ @rest.get("/guilds/#{guild_id}/prune", params: params)['pruned']
1130
+ end
1131
+
1132
+ # Begin a guild prune
1133
+ # @param guild_id [String, Snowflake] Guild ID
1134
+ # @param days [Integer] Inactive days threshold
1135
+ # @param compute_prune_count [Boolean] Whether to include the prune count
1136
+ # @param include_roles [Array<String, Snowflake>] Optional role IDs
1137
+ # @param reason [String, nil] Audit log reason
1138
+ # @return [Integer, nil] Number of pruned members when requested
1139
+ def begin_guild_prune(guild_id, days:, compute_prune_count: true, include_roles: nil, reason: nil)
1140
+ payload = { days: days, compute_prune_count: compute_prune_count }
1141
+ payload[:include_roles] = Array(include_roles).map(&:to_s) if include_roles
1142
+ response = @rest.post("/guilds/#{guild_id}/prune", body: payload, headers: audit_log_headers(reason))
1143
+ response && response['pruned']
1144
+ end
1145
+
1146
+ # Get voice regions for a guild
1147
+ # @param guild_id [String, Snowflake] Guild ID
1148
+ # @return [Array<Hash>] Voice regions
1149
+ def guild_voice_regions(guild_id)
1150
+ @rest.get("/guilds/#{guild_id}/regions")
1151
+ end
1152
+
1153
+ # Get active invites for a guild
1154
+ # @param guild_id [String, Snowflake] Guild ID
1155
+ # @return [Array<Hash>] Invite payloads
1156
+ def guild_invites(guild_id)
1157
+ @rest.get("/guilds/#{guild_id}/invites")
1158
+ end
1159
+
1160
+ # Get integrations for a guild
1161
+ # @param guild_id [String, Snowflake] Guild ID
1162
+ # @return [Array<Integration>] Integration payloads
1163
+ def guild_integrations(guild_id)
1164
+ @rest.get("/guilds/#{guild_id}/integrations").map { |integration| Integration.new(integration.merge('guild_id' => guild_id.to_s)) }
1165
+ end
1166
+
1167
+ # Delete a guild integration
1168
+ # @param guild_id [String, Snowflake] Guild ID
1169
+ # @param integration_id [String, Snowflake] Integration ID
1170
+ # @param reason [String, nil] Audit log reason
1171
+ # @return [void]
1172
+ def delete_guild_integration(guild_id, integration_id, reason: nil)
1173
+ @rest.delete("/guilds/#{guild_id}/integrations/#{integration_id}", headers: audit_log_headers(reason))
1174
+ end
1175
+
1176
+ # Get guild widget settings
1177
+ # @param guild_id [String, Snowflake] Guild ID
1178
+ # @return [Hash, nil] Widget settings
1179
+ def guild_widget_settings(guild_id)
1180
+ @rest.get("/guilds/#{guild_id}/widget")
1181
+ rescue RestClient::NotFoundError
1182
+ nil
1183
+ end
1184
+
1185
+ # Modify guild widget settings
1186
+ # @param guild_id [String, Snowflake] Guild ID
1187
+ # @param enabled [Boolean, nil] Whether widget is enabled
1188
+ # @param channel_id [String, Snowflake, nil] Widget channel ID
1189
+ # @param reason [String, nil] Audit log reason
1190
+ # @return [Hash] Updated widget settings
1191
+ def modify_guild_widget(guild_id, enabled: nil, channel_id: nil, reason: nil)
1192
+ payload = { enabled: enabled, channel_id: channel_id&.to_s }.compact
1193
+ @rest.patch("/guilds/#{guild_id}/widget", body: payload, headers: audit_log_headers(reason))
1194
+ end
1195
+
1196
+ # Get guild widget data
1197
+ # @param guild_id [String, Snowflake] Guild ID
1198
+ # @return [Hash, nil] Widget payload
1199
+ def guild_widget(guild_id)
1200
+ @rest.get("/guilds/#{guild_id}/widget.json")
1201
+ rescue RestClient::NotFoundError
1202
+ nil
1203
+ end
1204
+
1205
+ # Get a guild vanity URL
1206
+ # @param guild_id [String, Snowflake] Guild ID
1207
+ # @return [Hash, nil] Vanity URL payload
1208
+ def guild_vanity_url(guild_id)
1209
+ @rest.get("/guilds/#{guild_id}/vanity-url")
1210
+ rescue RestClient::NotFoundError
1211
+ nil
1212
+ end
1213
+
1214
+ # Build a guild widget image URL
1215
+ # @param guild_id [String, Snowflake] Guild ID
1216
+ # @param style [String, nil] Widget image style
1217
+ # @return [String] Widget image URL
1218
+ def guild_widget_image(guild_id, style: nil)
1219
+ url = "https://discord.com/api/guilds/#{guild_id}/widget.png"
1220
+ style ? "#{url}?style=#{CGI.escape(style)}" : url
1221
+ end
1222
+
1223
+ # Get a guild welcome screen
1224
+ # @param guild_id [String, Snowflake] Guild ID
1225
+ # @return [Hash, nil] Welcome screen payload
1226
+ def guild_welcome_screen(guild_id)
1227
+ @rest.get("/guilds/#{guild_id}/welcome-screen")
1228
+ rescue RestClient::NotFoundError
1229
+ nil
1230
+ end
1231
+
1232
+ # Modify a guild welcome screen
1233
+ # @param guild_id [String, Snowflake] Guild ID
1234
+ # @param enabled [Boolean, nil] Whether enabled
1235
+ # @param welcome_channels [Array<Hash>, nil] Welcome channel configuration
1236
+ # @param description [String, nil] Welcome description
1237
+ # @param reason [String, nil] Audit log reason
1238
+ # @return [Hash] Updated welcome screen payload
1239
+ def modify_guild_welcome_screen(guild_id, enabled: nil, welcome_channels: nil, description: nil, reason: nil)
1240
+ payload = {
1241
+ enabled: enabled,
1242
+ welcome_channels: welcome_channels,
1243
+ description: description
1244
+ }.compact
1245
+ @rest.patch("/guilds/#{guild_id}/welcome-screen", body: payload, headers: audit_log_headers(reason))
1246
+ end
1247
+
1248
+ # Get guild onboarding
1249
+ # @param guild_id [String, Snowflake] Guild ID
1250
+ # @return [Hash, nil] Onboarding payload
1251
+ def guild_onboarding(guild_id)
1252
+ @rest.get("/guilds/#{guild_id}/onboarding")
1253
+ rescue RestClient::NotFoundError
1254
+ nil
1255
+ end
1256
+
1257
+ # Modify guild onboarding
1258
+ # @param guild_id [String, Snowflake] Guild ID
1259
+ # @param options [Hash] Raw onboarding payload
1260
+ # @return [Hash] Updated onboarding payload
1261
+ def modify_guild_onboarding(guild_id, **options)
1262
+ @rest.put("/guilds/#{guild_id}/onboarding", body: options)
1263
+ end
1264
+
1265
+ # Get audit log entries for a guild
1266
+ # @param guild_id [String, Snowflake] Guild ID
1267
+ # @param user_id [String, Snowflake, nil] Filter by acting user
1268
+ # @param action_type [Integer, nil] Filter by action type
1269
+ # @param before [String, Snowflake, nil] Pagination cursor
1270
+ # @param after [String, Snowflake, nil] Pagination cursor
1271
+ # @param limit [Integer, nil] Max entries
1272
+ # @return [AuditLog] Audit log payload
1273
+ def guild_audit_log(guild_id, user_id: nil, action_type: nil, before: nil, after: nil, limit: nil)
1274
+ params = {
1275
+ user_id: user_id&.to_s,
1276
+ action_type: action_type,
1277
+ before: before&.to_s,
1278
+ after: after&.to_s,
1279
+ limit: limit
1280
+ }.compact
1281
+ AuditLog.new(@rest.get("/guilds/#{guild_id}/audit-logs", params: params))
1282
+ end
1283
+
1284
+ # List scheduled events for a guild
1285
+ # @param guild_id [String, Snowflake] Guild ID
1286
+ # @param with_user_count [Boolean] Include subscriber counts
1287
+ # @return [Array<GuildScheduledEvent>] Scheduled events
1288
+ def guild_scheduled_events(guild_id, with_user_count: false)
1289
+ data = @rest.get("/guilds/#{guild_id}/scheduled-events", params: { with_user_count: with_user_count })
1290
+ data.map { |event| GuildScheduledEvent.new(event) }
1291
+ end
1292
+
1293
+ # Get a specific scheduled event
1294
+ # @param guild_id [String, Snowflake] Guild ID
1295
+ # @param event_id [String, Snowflake] Event ID
1296
+ # @param with_user_count [Boolean] Include subscriber count
1297
+ # @return [GuildScheduledEvent, nil] Scheduled event
1298
+ def guild_scheduled_event(guild_id, event_id, with_user_count: false)
1299
+ data = @rest.get("/guilds/#{guild_id}/scheduled-events/#{event_id}", params: { with_user_count: with_user_count })
1300
+ GuildScheduledEvent.new(data)
1301
+ rescue RestClient::NotFoundError
1302
+ nil
1303
+ end
1304
+
1305
+ # Create a scheduled event
1306
+ # @param guild_id [String, Snowflake] Guild ID
1307
+ # @param reason [String, nil] Audit log reason
1308
+ # @param options [Hash] Scheduled event payload
1309
+ # @return [GuildScheduledEvent] Created event
1310
+ def create_guild_scheduled_event(guild_id, reason: nil, **options)
1311
+ data = @rest.post("/guilds/#{guild_id}/scheduled-events", body: options.compact, headers: audit_log_headers(reason))
1312
+ GuildScheduledEvent.new(data)
1313
+ end
1314
+
1315
+ # Modify a scheduled event
1316
+ # @param guild_id [String, Snowflake] Guild ID
1317
+ # @param event_id [String, Snowflake] Event ID
1318
+ # @param reason [String, nil] Audit log reason
1319
+ # @param options [Hash] Scheduled event payload
1320
+ # @return [GuildScheduledEvent] Updated event
1321
+ def modify_guild_scheduled_event(guild_id, event_id, reason: nil, **options)
1322
+ data = @rest.patch("/guilds/#{guild_id}/scheduled-events/#{event_id}", body: options.compact, headers: audit_log_headers(reason))
1323
+ GuildScheduledEvent.new(data)
1324
+ end
1325
+
1326
+ # Delete a scheduled event
1327
+ # @param guild_id [String, Snowflake] Guild ID
1328
+ # @param event_id [String, Snowflake] Event ID
1329
+ # @return [void]
1330
+ def delete_guild_scheduled_event(guild_id, event_id)
1331
+ @rest.delete("/guilds/#{guild_id}/scheduled-events/#{event_id}")
1332
+ end
1333
+
1334
+ # List users subscribed to a scheduled event
1335
+ # @param guild_id [String, Snowflake] Guild ID
1336
+ # @param event_id [String, Snowflake] Event ID
1337
+ # @param limit [Integer, nil] Max users
1338
+ # @param with_member [Boolean] Include member objects
1339
+ # @param before [String, Snowflake, nil] Pagination cursor
1340
+ # @param after [String, Snowflake, nil] Pagination cursor
1341
+ # @return [Array<Hash>] Subscriber payloads
1342
+ def guild_scheduled_event_users(guild_id, event_id, limit: nil, with_member: false, before: nil, after: nil)
1343
+ params = {
1344
+ limit: limit,
1345
+ with_member: with_member,
1346
+ before: before&.to_s,
1347
+ after: after&.to_s
1348
+ }.compact
1349
+ @rest.get("/guilds/#{guild_id}/scheduled-events/#{event_id}/users", params: params)
1350
+ end
1351
+
1352
+ # Create a stage instance
1353
+ # @param channel_id [String, Snowflake] Stage channel ID
1354
+ # @param topic [String] Stage topic
1355
+ # @param privacy_level [Integer, nil] Privacy level
1356
+ # @param send_start_notification [Boolean, nil] Send notification
1357
+ # @param guild_scheduled_event_id [String, Snowflake, nil] Associated scheduled event
1358
+ # @return [Hash] Stage instance payload
1359
+ def create_stage_instance(channel_id:, topic:, privacy_level: nil, send_start_notification: nil, guild_scheduled_event_id: nil)
1360
+ payload = {
1361
+ channel_id: channel_id.to_s,
1362
+ topic: topic,
1363
+ privacy_level: privacy_level,
1364
+ send_start_notification: send_start_notification,
1365
+ guild_scheduled_event_id: guild_scheduled_event_id&.to_s
1366
+ }.compact
1367
+ @rest.post('/stage-instances', body: payload)
1368
+ end
1369
+
1370
+ # Get a stage instance
1371
+ # @param channel_id [String, Snowflake] Stage channel ID
1372
+ # @return [Hash, nil] Stage instance payload
1373
+ def stage_instance(channel_id)
1374
+ @rest.get("/stage-instances/#{channel_id}")
1375
+ rescue RestClient::NotFoundError
1376
+ nil
1377
+ end
1378
+
1379
+ # Modify a stage instance
1380
+ # @param channel_id [String, Snowflake] Stage channel ID
1381
+ # @param topic [String, nil] Updated topic
1382
+ # @param privacy_level [Integer, nil] Updated privacy level
1383
+ # @return [Hash] Updated stage instance payload
1384
+ def modify_stage_instance(channel_id, topic: nil, privacy_level: nil)
1385
+ @rest.patch("/stage-instances/#{channel_id}", body: { topic: topic, privacy_level: privacy_level }.compact)
1386
+ end
1387
+
1388
+ # Delete a stage instance
1389
+ # @param channel_id [String, Snowflake] Stage channel ID
1390
+ # @param reason [String, nil] Audit log reason
1391
+ # @return [void]
1392
+ def delete_stage_instance(channel_id, reason: nil)
1393
+ @rest.delete("/stage-instances/#{channel_id}", headers: audit_log_headers(reason))
1394
+ end
1395
+
1396
+ # List stickers for a guild
1397
+ # @param guild_id [String, Snowflake] Guild ID
1398
+ # @return [Array<Sticker>] Guild stickers
1399
+ def guild_stickers(guild_id)
1400
+ data = @rest.get("/guilds/#{guild_id}/stickers")
1401
+ data.map { |sticker| Sticker.new(sticker) }
1402
+ end
1403
+
1404
+ # Get a guild sticker
1405
+ # @param guild_id [String, Snowflake] Guild ID
1406
+ # @param sticker_id [String, Snowflake] Sticker ID
1407
+ # @return [Sticker, nil] Sticker
1408
+ def guild_sticker(guild_id, sticker_id)
1409
+ data = @rest.get("/guilds/#{guild_id}/stickers/#{sticker_id}")
1410
+ Sticker.new(data)
1411
+ rescue RestClient::NotFoundError
1412
+ nil
1413
+ end
1414
+
1415
+ # Get a standard sticker
1416
+ # @param sticker_id [String, Snowflake] Sticker ID
1417
+ # @return [Sticker, nil] Sticker
1418
+ def sticker(sticker_id)
1419
+ data = @rest.get("/stickers/#{sticker_id}")
1420
+ Sticker.new(data)
1421
+ rescue RestClient::NotFoundError
1422
+ nil
1423
+ end
1424
+
1425
+ # List available premium sticker packs
1426
+ # @return [Hash] Sticker pack payload
1427
+ def sticker_packs
1428
+ @rest.get('/sticker-packs')
1429
+ end
1430
+
1431
+ # Get guild templates
1432
+ # @param guild_id [String, Snowflake] Guild ID
1433
+ # @return [Array<Hash>] Template payloads
1434
+ def guild_templates(guild_id)
1435
+ @rest.get("/guilds/#{guild_id}/templates")
1436
+ end
1437
+
1438
+ # Create a guild template
1439
+ # @param guild_id [String, Snowflake] Guild ID
1440
+ # @param name [String] Template name
1441
+ # @param description [String, nil] Template description
1442
+ # @return [Hash] Template payload
1443
+ def create_guild_template(guild_id, name:, description: nil)
1444
+ @rest.post("/guilds/#{guild_id}/templates", body: { name: name, description: description }.compact)
1445
+ end
1446
+
1447
+ # Sync a guild template
1448
+ # @param guild_id [String, Snowflake] Guild ID
1449
+ # @param code [String] Template code
1450
+ # @return [Hash] Template payload
1451
+ def sync_guild_template(guild_id, code)
1452
+ @rest.put("/guilds/#{guild_id}/templates/#{code}")
1453
+ end
1454
+
1455
+ # Modify a guild template
1456
+ # @param guild_id [String, Snowflake] Guild ID
1457
+ # @param code [String] Template code
1458
+ # @param name [String, nil] New template name
1459
+ # @param description [String, nil] New template description
1460
+ # @return [Hash] Template payload
1461
+ def modify_guild_template(guild_id, code, name: nil, description: nil)
1462
+ payload = { name: name, description: description }.compact
1463
+ @rest.patch("/guilds/#{guild_id}/templates/#{code}", body: payload)
1464
+ end
1465
+
1466
+ # Delete a guild template
1467
+ # @param guild_id [String, Snowflake] Guild ID
1468
+ # @param code [String] Template code
1469
+ # @return [Hash] Deleted template payload
1470
+ def delete_guild_template(guild_id, code)
1471
+ @rest.delete("/guilds/#{guild_id}/templates/#{code}")
1472
+ end
1473
+
1474
+ # Fetch a guild template by code
1475
+ # @param code [String] Template code
1476
+ # @return [Hash, nil] Template payload
1477
+ def guild_template(code)
1478
+ @rest.get("/guilds/templates/#{code}")
1479
+ rescue RestClient::NotFoundError
1480
+ nil
1481
+ end
1482
+
1483
+ # Create a guild from a template
1484
+ # @param code [String] Template code
1485
+ # @param name [String] Guild name
1486
+ # @param icon [String, nil] Base64 icon data
1487
+ # @return [Guild] Created guild
1488
+ def create_guild_from_template(code, name:, icon: nil)
1489
+ data = @rest.post("/guilds/templates/#{code}", body: { name: name, icon: icon }.compact)
1490
+ Guild.new(data)
1491
+ end
1492
+
1493
+ # List auto moderation rules for a guild
1494
+ # @param guild_id [String, Snowflake] Guild ID
1495
+ # @return [Array<AutoModerationRule>] Rules
1496
+ def auto_moderation_rules(guild_id)
1497
+ data = @rest.get("/guilds/#{guild_id}/auto-moderation/rules")
1498
+ data.map { |rule| AutoModerationRule.new(rule) }
1499
+ end
1500
+
1501
+ # Get a specific auto moderation rule
1502
+ # @param guild_id [String, Snowflake] Guild ID
1503
+ # @param rule_id [String, Snowflake] Rule ID
1504
+ # @return [AutoModerationRule, nil] Rule
1505
+ def auto_moderation_rule(guild_id, rule_id)
1506
+ data = @rest.get("/guilds/#{guild_id}/auto-moderation/rules/#{rule_id}")
1507
+ AutoModerationRule.new(data)
1508
+ rescue RestClient::NotFoundError
1509
+ nil
1510
+ end
1511
+
1512
+ # Create an auto moderation rule
1513
+ # @param guild_id [String, Snowflake] Guild ID
1514
+ # @param reason [String, nil] Audit log reason
1515
+ # @param options [Hash] Rule payload
1516
+ # @return [AutoModerationRule] Created rule
1517
+ def create_auto_moderation_rule(guild_id, reason: nil, **options)
1518
+ data = @rest.post("/guilds/#{guild_id}/auto-moderation/rules", body: options.compact, headers: audit_log_headers(reason))
1519
+ AutoModerationRule.new(data)
1520
+ end
1521
+
1522
+ # Modify an auto moderation rule
1523
+ # @param guild_id [String, Snowflake] Guild ID
1524
+ # @param rule_id [String, Snowflake] Rule ID
1525
+ # @param reason [String, nil] Audit log reason
1526
+ # @param options [Hash] Rule payload
1527
+ # @return [AutoModerationRule] Updated rule
1528
+ def modify_auto_moderation_rule(guild_id, rule_id, reason: nil, **options)
1529
+ data = @rest.patch("/guilds/#{guild_id}/auto-moderation/rules/#{rule_id}", body: options.compact, headers: audit_log_headers(reason))
1530
+ AutoModerationRule.new(data)
1531
+ end
1532
+
1533
+ # Delete an auto moderation rule
1534
+ # @param guild_id [String, Snowflake] Guild ID
1535
+ # @param rule_id [String, Snowflake] Rule ID
1536
+ # @param reason [String, nil] Audit log reason
1537
+ # @return [void]
1538
+ def delete_auto_moderation_rule(guild_id, rule_id, reason: nil)
1539
+ @rest.delete("/guilds/#{guild_id}/auto-moderation/rules/#{rule_id}", headers: audit_log_headers(reason))
1540
+ end
1541
+
1542
+ # Get SKUs for the current application
1543
+ # @param application_id [String, Snowflake, nil] Application ID
1544
+ # @return [Array<Hash>] SKU payloads
1545
+ def skus(application_id: nil)
1546
+ app_id = application_id || application_id_for_rest
1547
+ @rest.get("/applications/#{app_id}/skus")
1548
+ end
1549
+
1550
+ # Get entitlements for the current application
1551
+ # @param application_id [String, Snowflake, nil] Application ID
1552
+ # @param options [Hash] Query filters
1553
+ # @return [Array<Hash>] Entitlement payloads
1554
+ def entitlements(application_id: nil, **options)
1555
+ app_id = application_id || application_id_for_rest
1556
+ params = normalize_rest_params(options, :before, :after, :guild_id, :user_id, :limit, :exclude_ended)
1557
+ params[:sku_ids] = Array(options[:sku_ids]).map(&:to_s).join(',') if options[:sku_ids]
1558
+ @rest.get("/applications/#{app_id}/entitlements", params: params)
1559
+ end
1560
+
1561
+ # Create a test entitlement
1562
+ # @param sku_id [String, Snowflake] SKU ID
1563
+ # @param owner_id [String, Snowflake] User or guild owner ID
1564
+ # @param owner_type [Integer] 1 for guild, 2 for user
1565
+ # @param application_id [String, Snowflake, nil] Application ID
1566
+ # @return [Hash] Entitlement payload
1567
+ def create_test_entitlement(sku_id:, owner_id:, owner_type:, application_id: nil)
1568
+ app_id = application_id || application_id_for_rest
1569
+ payload = { sku_id: sku_id.to_s, owner_id: owner_id.to_s, owner_type: owner_type }
1570
+ @rest.post("/applications/#{app_id}/entitlements", body: payload)
1571
+ end
1572
+
1573
+ # Delete a test entitlement
1574
+ # @param entitlement_id [String, Snowflake] Entitlement ID
1575
+ # @param application_id [String, Snowflake, nil] Application ID
1576
+ # @return [void]
1577
+ def delete_test_entitlement(entitlement_id, application_id: nil)
1578
+ app_id = application_id || application_id_for_rest
1579
+ @rest.delete("/applications/#{app_id}/entitlements/#{entitlement_id}")
1580
+ end
1581
+
1582
+ # Consume an entitlement
1583
+ # @param entitlement_id [String, Snowflake] Entitlement ID
1584
+ # @param application_id [String, Snowflake, nil] Application ID
1585
+ # @return [void]
1586
+ def consume_entitlement(entitlement_id, application_id: nil)
1587
+ app_id = application_id || application_id_for_rest
1588
+ @rest.post("/applications/#{app_id}/entitlements/#{entitlement_id}/consume")
1589
+ end
1590
+
1591
+ # Get default soundboard sounds
1592
+ # @return [Hash] Soundboard payload
1593
+ def default_soundboard_sounds
1594
+ @rest.get('/soundboard-default-sounds')
1595
+ end
1596
+
1597
+ # Get guild soundboard sounds
1598
+ # @param guild_id [String, Snowflake] Guild ID
1599
+ # @return [Hash] Soundboard payload
1600
+ def guild_soundboard_sounds(guild_id)
1601
+ @rest.get("/guilds/#{guild_id}/soundboard-sounds")
1602
+ end
1603
+
1604
+ # Get a single guild soundboard sound
1605
+ # @param guild_id [String, Snowflake] Guild ID
1606
+ # @param sound_id [String, Snowflake] Sound ID
1607
+ # @return [Hash, nil] Soundboard sound payload
1608
+ def guild_soundboard_sound(guild_id, sound_id)
1609
+ @rest.get("/guilds/#{guild_id}/soundboard-sounds/#{sound_id}")
1610
+ rescue RestClient::NotFoundError
1611
+ nil
1612
+ end
1613
+
1614
+ # Create a guild soundboard sound
1615
+ # @param guild_id [String, Snowflake] Guild ID
1616
+ # @param reason [String, nil] Audit log reason
1617
+ # @param options [Hash] Soundboard payload
1618
+ # @return [Hash] Soundboard sound payload
1619
+ def create_guild_soundboard_sound(guild_id, reason: nil, **options)
1620
+ @rest.post("/guilds/#{guild_id}/soundboard-sounds", body: options.compact, headers: audit_log_headers(reason))
1621
+ end
1622
+
1623
+ # Modify a guild soundboard sound
1624
+ # @param guild_id [String, Snowflake] Guild ID
1625
+ # @param sound_id [String, Snowflake] Sound ID
1626
+ # @param reason [String, nil] Audit log reason
1627
+ # @param options [Hash] Soundboard payload
1628
+ # @return [Hash] Soundboard sound payload
1629
+ def modify_guild_soundboard_sound(guild_id, sound_id, reason: nil, **options)
1630
+ @rest.patch("/guilds/#{guild_id}/soundboard-sounds/#{sound_id}", body: options.compact, headers: audit_log_headers(reason))
1631
+ end
1632
+
1633
+ # Delete a guild soundboard sound
1634
+ # @param guild_id [String, Snowflake] Guild ID
1635
+ # @param sound_id [String, Snowflake] Sound ID
1636
+ # @param reason [String, nil] Audit log reason
1637
+ # @return [void]
1638
+ def delete_guild_soundboard_sound(guild_id, sound_id, reason: nil)
1639
+ @rest.delete("/guilds/#{guild_id}/soundboard-sounds/#{sound_id}", headers: audit_log_headers(reason))
1640
+ end
1641
+
1642
+ # Send a soundboard sound in a voice-connected channel
1643
+ # @param channel_id [String, Snowflake] Voice channel ID
1644
+ # @param sound_id [String, Snowflake] Sound ID
1645
+ # @param source_guild_id [String, Snowflake, nil] Source guild for default/shared sounds
1646
+ # @return [void]
1647
+ def send_soundboard_sound(channel_id, sound_id:, source_guild_id: nil)
1648
+ payload = { sound_id: sound_id.to_s, source_guild_id: source_guild_id&.to_s }.compact
1649
+ @rest.post("/channels/#{channel_id}/send-soundboard-sound", body: payload)
1650
+ end
1651
+
1652
+ # Create guild channel
664
1653
  # @param guild_id [String, Snowflake] Guild ID
665
1654
  # @param name [String] Channel name
666
1655
  # @param type [Integer] Channel type (0=text, 2=voice, 4=category, etc.)
@@ -702,6 +1691,165 @@ module DiscordRDA
702
1691
  @rest.post("/channels/#{channel_id}/messages/bulk-delete", body: { messages: message_ids.map(&:to_s) }, headers: headers)
703
1692
  end
704
1693
 
1694
+ # Modify the bot user's profile
1695
+ # @param username [String, nil] New username
1696
+ # @param avatar [File, String, nil] New avatar
1697
+ # @return [User, nil] Updated user
1698
+ def modify_current_user(username: nil, avatar: nil)
1699
+ User.modify_current_user(username: username, avatar: avatar)
1700
+ end
1701
+
1702
+ # Get the current user's guilds
1703
+ # @param limit [Integer] Max guilds to return
1704
+ # @param after [String, Snowflake, nil] Cursor
1705
+ # @param before [String, Snowflake, nil] Cursor
1706
+ # @param with_counts [Boolean] Include approximate counts
1707
+ # @return [Array<Hash>] Partial guild payloads
1708
+ def current_user_guilds(limit: 200, after: nil, before: nil, with_counts: false)
1709
+ User.get_current_user_guilds(limit: limit, after: after&.to_s, before: before&.to_s, with_counts: with_counts)
1710
+ end
1711
+
1712
+ # Get the current user's member object in a guild
1713
+ # @param guild_id [String, Snowflake] Guild ID
1714
+ # @return [Hash, nil] Member payload
1715
+ def current_user_guild_member(guild_id)
1716
+ User.get_current_user_guild_member(guild_id)
1717
+ end
1718
+
1719
+ # Leave a guild as the current user
1720
+ # @param guild_id [String, Snowflake] Guild ID
1721
+ # @return [void]
1722
+ def leave_guild(guild_id)
1723
+ User.leave_guild(guild_id)
1724
+ end
1725
+
1726
+ # Create a DM channel with a user
1727
+ # @param user_id [String, Snowflake] Target user ID
1728
+ # @return [Channel] DM channel
1729
+ def create_dm(user_id)
1730
+ data = @rest.post('/users/@me/channels', body: { recipient_id: user_id.to_s })
1731
+ Channel.new(data)
1732
+ end
1733
+
1734
+ # Get OAuth2 connections for the current user
1735
+ # @return [Array<Hash>] User connections
1736
+ def current_user_connections
1737
+ User.get_connections
1738
+ end
1739
+
1740
+ # Get application role connection metadata for the current user
1741
+ # @param application_id [String, Snowflake] Application ID
1742
+ # @return [Hash, nil] Role connection payload
1743
+ def application_role_connection(application_id)
1744
+ User.get_application_role_connection(application_id)
1745
+ end
1746
+
1747
+ # Update application role connection metadata for the current user
1748
+ # @param application_id [String, Snowflake] Application ID
1749
+ # @param platform_name [String, nil] Platform name
1750
+ # @param platform_username [String, nil] Platform username
1751
+ # @param metadata [Hash] Metadata payload
1752
+ # @return [Hash, nil] Updated role connection payload
1753
+ def update_application_role_connection(application_id, platform_name: nil, platform_username: nil, metadata: {})
1754
+ User.update_application_role_connection(
1755
+ application_id,
1756
+ platform_name: platform_name,
1757
+ platform_username: platform_username,
1758
+ metadata: metadata
1759
+ )
1760
+ end
1761
+
1762
+ # Get current bot application metadata
1763
+ # @return [Hash] Application payload
1764
+ def application_info
1765
+ @rest.get('/oauth2/applications/@me')
1766
+ end
1767
+
1768
+ # Get current authorization information
1769
+ # @return [Hash] Authorization payload
1770
+ def authorization_info
1771
+ @rest.get('/oauth2/@me')
1772
+ end
1773
+
1774
+ # Get gateway information
1775
+ # @return [Hash] Gateway payload
1776
+ def gateway
1777
+ @rest.get('/gateway')
1778
+ end
1779
+
1780
+ # Get gateway bot information
1781
+ # @return [Hash] Gateway bot payload
1782
+ def gateway_bot
1783
+ @rest.get('/gateway/bot')
1784
+ end
1785
+
1786
+ private
1787
+
1788
+ def application_id_for_rest
1789
+ application_info['id']
1790
+ end
1791
+
1792
+ def normalize_rest_params(options, *snowflake_keys)
1793
+ options.each_with_object({}) do |(key, value), params|
1794
+ next if value.nil?
1795
+
1796
+ params[key] = snowflake_keys.include?(key) ? value.to_s : value
1797
+ end
1798
+ end
1799
+
1800
+ def audit_log_headers(reason)
1801
+ reason ? { 'X-Audit-Log-Reason' => CGI.escape(reason) } : {}
1802
+ end
1803
+
1804
+ def register_application_command(cmd, name:, guild_id: nil)
1805
+ cmd.instance_variable_set(:@application_id, me.id.to_s) rescue nil
1806
+ cmd.instance_variable_set(:@guild_id, guild_id.to_s) if guild_id
1807
+
1808
+ key = guild_id ? "#{name}:#{guild_id}" : name
1809
+ @slash_commands[key] = cmd
1810
+
1811
+ if cmd.application_id
1812
+ if guild_id
1813
+ cmd.create_guild(self, guild_id)
1814
+ else
1815
+ cmd.create_global(self)
1816
+ end
1817
+ end
1818
+
1819
+ @logger.info('Registered application command', name: name, type: cmd.command_type, guild: guild_id || 'global')
1820
+ cmd
1821
+ end
1822
+
1823
+ def configure_entity_apis(client)
1824
+ Message.api = client
1825
+ Interaction.api = client
1826
+ Interaction.supervisor = @supervisor
1827
+ User.api = client
1828
+ Guild.api = client
1829
+ Channel.api = client
1830
+ end
1831
+
1832
+ def restart_gateway_state
1833
+ Array(@restart_state['shards']).each_with_object({}) do |shard, states|
1834
+ states[shard['shard_id']] = shard
1835
+ end
1836
+ end
1837
+
1838
+ def install_signal_handlers
1839
+ return if defined?(@signal_handlers_installed) && @signal_handlers_installed
1840
+
1841
+ %w[INT TERM].each do |signal|
1842
+ Signal.trap(signal) do
1843
+ @logger.info('Received shutdown signal', signal: signal)
1844
+ stop
1845
+ rescue StandardError => e
1846
+ @logger&.error('Failed during signal shutdown', signal: signal, error: e)
1847
+ end
1848
+ end
1849
+
1850
+ @signal_handlers_installed = true
1851
+ end
1852
+
705
1853
  def setup_interaction_handlers
706
1854
  # Handle slash commands
707
1855
  @event_bus.on(:interaction_create) do |event|
@@ -730,11 +1878,25 @@ module DiscordRDA
730
1878
  if cmd && cmd.handler
731
1879
  @logger.debug('Executing slash command', name: cmd_name, user: interaction.user&.id)
732
1880
  begin
733
- cmd.handler.call(interaction)
1881
+ @supervisor.execute(
1882
+ "command:#{key}",
1883
+ policy: cmd.execution_policy
1884
+ ) do
1885
+ cmd.handler.call(interaction)
1886
+ end
1887
+ rescue ExecutionSupervisor::TimeoutError => e
1888
+ @logger.error('Slash command timeout', command: cmd_name, error: e)
1889
+ interaction.respond(content: 'This command timed out and was stopped.', ephemeral: true) rescue nil
1890
+ rescue ExecutionSupervisor::ConcurrencyLimitError => e
1891
+ @logger.warn('Slash command concurrency limit', command: cmd_name, error: e)
1892
+ interaction.respond(content: 'This command is busy right now. Try again in a moment.', ephemeral: true) rescue nil
1893
+ rescue ExecutionSupervisor::CircuitOpenError => e
1894
+ @logger.warn('Slash command circuit open', command: cmd_name, error: e)
1895
+ interaction.respond(content: 'This command was temporarily disabled after repeated failures.', ephemeral: true) rescue nil
734
1896
  rescue => e
735
1897
  @logger.error('Slash command error', command: cmd_name, error: e)
736
- # Send error response
737
1898
  interaction.respond(content: "An error occurred while executing this command.", ephemeral: true) rescue nil
1899
+ @error_tracker.capture(e, command: cmd_name, user_id: interaction.user&.id)
738
1900
  end
739
1901
  else
740
1902
  @logger.warn('Unknown slash command', name: cmd_name)