diff --git a/lib/discordrb/api/channel.rb b/lib/discordrb/api/channel.rb index fd18f7a5da..692dae35c2 100644 --- a/lib/discordrb/api/channel.rb +++ b/lib/discordrb/api/channel.rb @@ -373,6 +373,61 @@ def unpin_message(token, channel_id, message_id, reason = nil) ) end + # Fetch a pre-exisiting stage instance. + # https://discord.com/developers/docs/resources/stage-instance#get-stage-instance + def get_stage_instance(token, channel_id) + Discordrb::API.request( + :stage_instances_cid, + channel_id, + :get, + "#{Discordrb::API.api_base}/stage-instances/#{channel_id}", + Authorization: token + ) + end + + # Create a stage instance in a stage channel. + # https://discord.com/developers/docs/resources/stage-instance#create-stage-instance + def create_stage_instance(token, channel_id, topic: :undef, send_start_notification: :undef, guild_scheduled_event_id: :undef, privacy_level: :undef, reason: nil) + Discordrb::API.request( + :stage_instances, + channel_id, + :post, + "#{Discordrb::API.api_base}/stage-instances", + { channel_id:, topic:, send_start_notification:, guild_scheduled_event_id:, privacy_level: }.reject { |_, value| value == :undef }.to_json, + content_type: :json, + Authorization: token, + 'X-Audit-Log-Reason': reason + ) + end + + # Update a pre-exisiting stage instance. + # https://discord.com/developers/docs/resources/stage-instance#modify-stage-instance + def update_stage_instance(token, channel_id, topic: :undef, privacy_level: :undef, reason: nil) + Discordrb::API.request( + :stage_instances_cid, + channel_id, + :patch, + "#{Discordrb::API.api_base}/stage-instances/#{channel_id}", + { topic:, privacy_level: }.reject { |_, value| value == :undef }.to_json, + content_type: :json, + Authorization: token, + 'X-Audit-Log-Reason': reason + ) + end + + # Delete a pre-exisiting stage instance. + # https://discord.com/developers/docs/resources/stage-instance#delete-stage-instance + def delete_stage_instance(token, channel_id, reason: nil) + Discordrb::API.request( + :stage_instances_cid, + channel_id, + :delete, + "#{Discordrb::API.api_base}/stage-instances/#{channel_id}", + Authorization: token, + 'X-Audit-Log-Reason': reason + ) + end + # Create an empty group channel. # @deprecated Discord no longer supports bots in group DMs, this endpoint was repurposed and no longer works as implemented here. # https://discord.com/developers/docs/resources/user#create-group-dm diff --git a/lib/discordrb/bot.rb b/lib/discordrb/bot.rb index a3e66f1f83..fa51f08e89 100644 --- a/lib/discordrb/bot.rb +++ b/lib/discordrb/bot.rb @@ -23,6 +23,7 @@ require 'discordrb/events/integrations' require 'discordrb/events/scheduled_events' require 'discordrb/events/polls' +require 'discordrb/events/stage_instances' require 'discordrb/api' require 'discordrb/api/channel' @@ -1226,6 +1227,18 @@ def update_guild_scheduled_event(data) end end + # Internal handler for STAGE_INSTANCE_CREATE and STAGE_INSTANCE_UPDATE + def update_stage_instance(data) + channel = @channels[data['channel_id'].to_i] + instance = channel&.stage_instance(request: false) + + if instance&.id == data['id'].to_i + instance&.update_data(data) + else + channel&.process_stage_instance(StageInstance.new(data, channel, self)) + end + end + # Internal handler for MESSAGE_CREATE def create_message(data); end @@ -1756,6 +1769,21 @@ def handle_dispatch(type, data) event = ThreadMembersUpdateEvent.new(data, self) raise_event(event) + when :STAGE_INSTANCE_CREATE + update_stage_instance(data) + + event = StageInstanceCreateEvent.new(data, self) + raise_event(event) + when :STAGE_INSTANCE_UPDATE + update_stage_instance(data) + + event = StageInstanceUpdateEvent.new(data, self) + raise_event(event) + when :STAGE_INSTANCE_DELETE + @channels[data['channel_id'].to_i]&.process_stage_instance(nil) + + event = StageInstanceDeleteEvent.new(data, self) + raise_event(event) when :MESSAGE_POLL_VOTE_ADD event = PollVoteAddEvent.new(data, self) raise_event(event) diff --git a/lib/discordrb/container.rb b/lib/discordrb/container.rb index 38877588c9..87a8d0b5d4 100644 --- a/lib/discordrb/container.rb +++ b/lib/discordrb/container.rb @@ -17,6 +17,7 @@ require 'discordrb/events/integrations' require 'discordrb/events/scheduled_events' require 'discordrb/events/polls' +require 'discordrb/events/stage_instances' require 'discordrb/await' @@ -290,6 +291,45 @@ def channel_recipient_remove(attributes = {}, &block) register_event(ChannelRecipientRemoveEvent, attributes, block) end + # This **event** is raised whenever a stage instance is created. + # @param attributes [Hash] The event's attributes. + # @option attributes [String, Regexp] :topic Matches the topic of the stage instance. + # @option attributes [Integer, String, Server] :server Matches the server of the stage instance. + # @option attributes [Integer, String, Channel] :channel Matches the channel of the stage instance. + # @option attributes [Integer, String, ScheduledEvent] :scheduled_event Matches the scheduled event of the stage instance. + # @yield The block is executed when the event is raised. + # @yieldparam event [StageInstanceCreateEvent] The event that was raised. + # @return [StageInstanceCreateEventHandler] the event handler that was registered. + def stage_instance_create(attributes = {}, &block) + register_event(StageInstanceCreateEvent, attributes, block) + end + + # This **event** is raised whenever a stage instance is updated. + # @param attributes [Hash] The event's attributes. + # @option attributes [String, Regexp] :topic Matches the topic of the stage instance. + # @option attributes [Integer, String, Server] :server Matches the server of the stage instance. + # @option attributes [Integer, String, Channel] :channel Matches the channel of the stage instance. + # @option attributes [Integer, String, ScheduledEvent] :scheduled_event Matches the scheduled event of the stage instance. + # @yield The block is executed when the event is raised. + # @yieldparam event [StageInstanceUpdateEvent] The event that was raised. + # @return [StageInstanceUpdateEventHandler] the event handler that was registered. + def stage_instance_update(attributes = {}, &block) + register_event(StageInstanceUpdateEvent, attributes, block) + end + + # This **event** is raised whenever a stage instance is deleted. + # @param attributes [Hash] The event's attributes. + # @option attributes [String, Regexp] :topic Matches the topic of the stage instance. + # @option attributes [Integer, String, Server] :server Matches the server of the stage instance. + # @option attributes [Integer, String, Channel] :channel Matches the channel of the stage instance. + # @option attributes [Integer, String, ScheduledEvent] :scheduled_event Matches the scheduled event of the stage instance. + # @yield The block is executed when the event is raised. + # @yieldparam event [StageInstanceDeleteEvent] The event that was raised. + # @return [StageInstanceDeleteEventHandler] the event handler that was registered. + def stage_instance_delete(attributes = {}, &block) + register_event(StageInstanceDeleteEvent, attributes, block) + end + # This **event** is raised when a user's voice state changes. This includes when a user joins, leaves, or # moves between voice channels, as well as their mute and deaf status for themselves and on the server. # @param attributes [Hash] The event's attributes. diff --git a/lib/discordrb/data.rb b/lib/discordrb/data.rb index f7687441a9..c154c2cac5 100644 --- a/lib/discordrb/data.rb +++ b/lib/discordrb/data.rb @@ -55,3 +55,4 @@ require 'discordrb/data/timestamp' require 'discordrb/data/scheduled_event' require 'discordrb/data/poll' +require 'discordrb/data/stage_instance' diff --git a/lib/discordrb/data/channel.rb b/lib/discordrb/data/channel.rb index 0b387fb343..277b153cf9 100644 --- a/lib/discordrb/data/channel.rb +++ b/lib/discordrb/data/channel.rb @@ -534,6 +534,40 @@ def slowmode? @rate_limit_per_user != 0 end + # Get the stage instance for a stage channel. + # @param request [true, false] Whether to make an API call to fetch the stage instance if it isn't cached. + # @return [StageInstance, nil] The stage instance for this stage channel, or `nil` if one couldn't be found. + def stage_instance(request: false) + raise 'Channel must be a stage channel' unless stage? + return @stage_instance if @stage_instance || !request + + instance = JSON.parse(API::Channel.get_stage_instance(@bot.token, @id)) + process_stage_instance(StageInstance.new(instance, self, @bot)) + rescue StandardError + nil + end + + # Create a stage instance in a stage channel. + # @param topic [String] The 1-120 character topic of the stage instance. + # @param mention_everyone [true, false] Whether to ping @everyone when the stage instance starts. + # @param scheduled_event [ScheduledEvent, nil] The scheduled event to associate with the stage instance. + # @param reason [String, nil] The reason to show in the server's audit log for creating the stage instance. + # @return [StageInstance] The stage instance that was created. + def create_stage_instance(topic:, mention_everyone:, scheduled_event: nil, reason: nil) + raise 'Channel must be a stage channel' unless stage? + + options = { + topic: topic, + reason: reason, + privacy_level: 2, + send_start_notification: mention_everyone, + guild_scheduled_event_id: scheduled_event&.resolve_id || :undef + } + + instance = JSON.parse(API::Channel.create_stage_instance(@bot.token, @id, **options)) + process_stage_instance(StageInstance.new(instance, self, @bot)) + end + # Sends a message to this channel. # @param content [String] The content to send. Should not be longer than 2000 characters or it will result in an error. # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech. @@ -1229,6 +1263,14 @@ def process_last_message_id(id) @last_message_id = id end + # Set the stage instance of a channel. + # @param instance [StageInstance, nil] the stage instance of the channel + # @note For internal use only + # @!visibility private + def process_stage_instance(instance) + @stage_instance = instance + end + # Set the available tags of a channel. # @param tag [Hash] the data for the tag to create # @param reason [String, nil] the reason to show in the audit log diff --git a/lib/discordrb/data/server.rb b/lib/discordrb/data/server.rb index 3f02395c97..406f4debe7 100644 --- a/lib/discordrb/data/server.rb +++ b/lib/discordrb/data/server.rb @@ -1378,6 +1378,7 @@ def update_data(new_data = nil) process_active_threads(new_data['threads']) if new_data['threads'] process_incident_actions(new_data['incidents_data']) if new_data.key?('incidents_data') process_scheduled_events(new_data['guild_scheduled_events']) if new_data['guild_scheduled_events'] + process_stage_instances(new_data['stage_instances']) if new_data['stage_instances'] end # Adds a channel to this server's cache @@ -1523,6 +1524,15 @@ def process_scheduled_events(events) @scheduled_events[event.resolve_id] = event end end + + def process_stage_instances(instances) + return unless instances + + instances.each do |element| + channel = @channels_by_id[element['channel_id'].to_i] + channel&.process_stage_instance(StageInstance.new(element, channel, @bot)) + end + end end # A ban entry on a server. diff --git a/lib/discordrb/data/stage_instance.rb b/lib/discordrb/data/stage_instance.rb new file mode 100644 index 0000000000..0cffbcfcfa --- /dev/null +++ b/lib/discordrb/data/stage_instance.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Discordrb + # Metadata about a live stage. + class StageInstance + include IDObject + + # @return [String] the topic of the stage instance. + attr_reader :topic + + # @return [Channel] the stage channel of the stage instance. + attr_reader :channel + + # @return [Integer, nil] the ID of the scheduled event for the stage instance. + attr_reader :scheduled_event_id + + # @!visibility private + def initialize(data, channel, bot) + @bot = bot + @channel = channel + @id = data['id'].to_i + update_data(data) + end + + # Get the stage instance's server. + # @return [Server] The server of the stage instance. + def server + @channel.server + end + + # Modify the properties of the stage instance. + # @param topic [String] The new 1-120 character topic of the stage instance. + # @param reason [String, nil] The reason to show in the audit log for updating the stage instance. + # @return [nil] + def modify(topic: :undef, reason: nil) + update_data(JSON.parse(API::Channel.update_stage_instance(@bot.token, @channel.id, topic:, reason:))) + nil + end + + # Get the scheduled event associated with the stage instance. + # @return [ScheduledEvent, nil] The scheduled event associated with the stage instance, or `nil`. + def scheduled_event + server.scheduled_event(@scheduled_event_id) if @scheduled_event_id + end + + # Permanenty delete the stage instance; this cannot be undone. + # @param reason [String, nil] The reason to show in the audit log for deleting the stage instance. + # @return [nil] + def delete(reason: nil) + API::Channel.delete_stage_instance(@bot.token, @channel.id, reason: reason) + server.delete_stage_instance(@id) + nil + end + + # @!visibility private + def update_data(new_data) + @topic = new_data['topic'] + @scheduled_event_id = new_data['guild_scheduled_event_id']&.to_i + end + + # @!visibility private + def inspect + "" + end + end +end diff --git a/lib/discordrb/events/stage_instances.rb b/lib/discordrb/events/stage_instances.rb new file mode 100644 index 0000000000..4c377f1d63 --- /dev/null +++ b/lib/discordrb/events/stage_instances.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Discordrb::Events + # Generic superclass for stage instance events. + class StageInstanceEvent < Event + # @return [Channel] the channel of the stage instance. + attr_reader :channel + + # @return [StageInstance] the stage instance in question. + attr_reader :stage_instance + + # @!visibility private + def initialize(data, bot) + @bot = bot + @channel = bot.channel(data['channel_id'].to_i) + @stage_instance = @channel.stage_instance(request: true) + end + end + + # Raised whenever a stage instance is created. + class StageInstanceCreateEvent < StageInstanceEvent; end + + # Raised whenever a stage instance is updated. + class StageInstanceUpdateEvent < StageInstanceEvent; end + + # Raised whenever a stage instance is deleted. + class StageInstanceDeleteEvent < StageInstanceEvent + # @!visibility private + def initialize(data, bot) + @bot = bot + @channel = bot.channel(data['channel_id'].to_i) + @stage_instance = Discordrb::StageInstance.new(data, channel, @bot) + end + end + + # Generic event handler class for stage instance events. + class StageInstanceEventHandler < EventHandler + # @!visibility private + def matches?(event) + return false unless event.is_a?(StageInstanceEvent) + + [ + matches_all(@attributes[:topic], event.stage_instance.topic) do |a, e| + case a + when Regexp + a.match?(e) + else + a == e + end + end, + + matches_all(@attributes[:channel], event.stage_instance.channel) do |a, e| + a&.resolve_id == e&.resolve_id + end, + + matches_all(@attributes[:server], event.stage_instance.channel.server) do |a, e| + a&.resolve_id == e&.resolve_id + end, + + matches_all(@attributes[:scheduled_event], event.stage_instance.scheduled_event_id) do |a, e| + a&.resolve_id == e&.resolve_id + end + ].reduce(true, &:&) + end + end + + # Event handler for :STAGE_INSTANCE_CREATE events. + class StageInstanceCreateEventHandler < StageInstanceEventHandler; end + + # Event handler for :STAGE_INSTANCE_UPDATE events. + class StageInstanceUpdateEventHandler < StageInstanceEventHandler; end + + # Event handler for :STAGE_INSTANCE_DELETE events. + class StageInstanceDeleteEventHandler < StageInstanceEventHandler; end +end