From 4ba0fb536d49a8f13765974f2bd42777e1d0dc9e Mon Sep 17 00:00:00 2001 From: GnomedDev Date: Tue, 12 Nov 2024 00:08:13 +0000 Subject: [PATCH] Split GuildThread from GuildChannel --- Cargo.toml | 1 + examples/e12_parallel_loops/src/main.rs | 4 +- examples/testing/src/main.rs | 13 +- examples/testing/src/model_type_sizes.rs | 6 +- src/builder/create_components.rs | 2 +- src/builder/create_message.rs | 2 +- src/builder/create_scheduled_event.rs | 4 +- src/builder/edit_channel.rs | 5 +- src/builder/edit_guild_welcome_screen.rs | 6 +- src/builder/edit_message.rs | 2 +- src/builder/edit_thread.rs | 8 +- src/builder/edit_webhook_message.rs | 4 +- src/builder/execute_webhook.rs | 8 +- src/builder/get_messages.rs | 2 +- src/cache/event.rs | 112 +-- src/cache/mod.rs | 39 +- src/collector.rs | 8 +- src/gateway/client/dispatch.rs | 4 +- src/gateway/client/event_handler.rs | 12 +- src/http/client.rs | 92 +- src/http/routing.rs | 32 +- src/http/typing.rs | 4 +- src/model/application/command_interaction.rs | 16 +- .../application/component_interaction.rs | 4 +- src/model/application/modal_interaction.rs | 4 +- src/model/channel/channel_id.rs | 786 +++++++++--------- src/model/channel/followed_channel.rs | 13 + src/model/channel/guild_channel.rs | 155 ++-- src/model/channel/interaction_channel.rs | 85 ++ src/model/channel/message.rs | 32 +- src/model/channel/mod.rs | 133 +-- src/model/channel/partial_channel.rs | 46 - src/model/channel/private_channel.rs | 2 +- src/model/channel/reaction.rs | 2 +- src/model/channel/thread.rs | 273 ++++++ src/model/event.rs | 25 +- src/model/guild/audit_log/mod.rs | 2 +- src/model/guild/automod.rs | 8 +- src/model/guild/member.rs | 54 +- src/model/guild/mod.rs | 6 +- src/model/id.rs | 11 +- src/model/mention.rs | 11 +- src/model/user.rs | 7 +- src/model/webhook.rs | 4 +- src/utils/argument_convert/channel.rs | 11 +- src/utils/argument_convert/emoji.rs | 2 +- src/utils/argument_convert/guild.rs | 2 +- src/utils/argument_convert/member.rs | 2 +- src/utils/argument_convert/message.rs | 2 +- src/utils/argument_convert/mod.rs | 8 +- src/utils/argument_convert/role.rs | 2 +- src/utils/argument_convert/user.rs | 2 +- src/utils/content_safe.rs | 12 +- src/utils/custom_message.rs | 4 +- src/utils/message_builder.rs | 6 +- src/utils/mod.rs | 2 +- 56 files changed, 1106 insertions(+), 998 deletions(-) create mode 100644 src/model/channel/followed_channel.rs create mode 100644 src/model/channel/interaction_channel.rs delete mode 100644 src/model/channel/partial_channel.rs create mode 100644 src/model/channel/thread.rs diff --git a/Cargo.toml b/Cargo.toml index 348ced6dbd8..e4f5eea2cc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ to-arraystring = "0.2.0" extract_map = { version = "0.1.0", features = ["serde", "iter_mut"] } aformat = "0.1.3" bytes = "1.5.0" +ref-cast = "1.0.23" # Optional dependencies fxhash = { version = "0.2.1", optional = true } chrono = { version = "0.4.31", default-features = false, features = ["clock", "serde"], optional = true } diff --git a/examples/e12_parallel_loops/src/main.rs b/examples/e12_parallel_loops/src/main.rs index b0bca910064..9d162de20ab 100644 --- a/examples/e12_parallel_loops/src/main.rs +++ b/examples/e12_parallel_loops/src/main.rs @@ -8,7 +8,7 @@ use serenity::builder::{CreateEmbed, CreateMessage}; use serenity::gateway::ActivityData; use serenity::model::channel::Message; use serenity::model::gateway::Ready; -use serenity::model::id::{ChannelId, GuildId}; +use serenity::model::id::{ChannelOrThreadId, GuildId}; use serenity::prelude::*; struct Handler { @@ -85,7 +85,7 @@ async fn log_system_load(ctx: &Context) { false, ); let builder = CreateMessage::new().embed(embed); - let message = ChannelId::new(381926291785383946).send_message(&ctx.http, builder).await; + let message = ChannelOrThreadId::new(381926291785383946).send_message(&ctx.http, builder).await; if let Err(why) = message { eprintln!("Error sending message: {why:?}"); }; diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 630dd278f8d..fd2c74d9c92 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -85,7 +85,8 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { model_type_sizes::print_ranking(); } else if msg.content == "auditlog" { // Test special characters in audit log reason - msg.channel_id + channel_id + .expect_channel() .edit( &ctx.http, EditChannel::new().name("new-channel-name").audit_log_reason("hello\nworld\nšŸ™‚"), @@ -153,6 +154,7 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { guild_id.ban(&ctx.http, user_id, 0, None).await?; } else if msg.content == "createtags" { channel_id + .expect_channel() .edit( &ctx.http, EditChannel::new().available_tags(vec![ @@ -163,9 +165,10 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { .await?; } else if msg.content == "assigntags" { let forum_id = msg.guild_channel(&ctx).await?.parent_id.unwrap(); - let forum = forum_id.to_guild_channel(&ctx, msg.guild_id).await?; + let forum = forum_id.widen().to_guild_channel(&ctx, msg.guild_id).await?; channel_id - .edit_thread( + .expect_thread() + .edit( &ctx.http, EditThread::new() .applied_tags(forum.available_tags.iter().map(|t| t.id).collect::>()), @@ -197,7 +200,7 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { msg.author.id.dm(&ctx.http, builder).await?; } else if let Some(channel) = msg.content.strip_prefix("movetorootandback") { let mut channel = { - let channel_id = channel.trim().parse::().unwrap(); + let channel_id = channel.trim().parse::().unwrap(); channel_id.to_guild_channel(&ctx, msg.guild_id).await.unwrap() }; @@ -207,7 +210,7 @@ async fn message(ctx: &Context, msg: Message) -> Result<(), serenity::Error> { } else if msg.content == "channelperms" { let guild = guild_id.to_guild_cached(&ctx.cache).unwrap().clone(); let perms = guild.user_permissions_in( - guild.channels.get(&channel_id).unwrap(), + guild.channels.get(&channel_id.expect_channel()).unwrap(), &*guild.member(&ctx.http, msg.author.id).await?, ); channel_id.say(&ctx.http, format!("{:?}", perms)).await?; diff --git a/examples/testing/src/model_type_sizes.rs b/examples/testing/src/model_type_sizes.rs index 9f0f076a88e..c86792b3ae6 100644 --- a/examples/testing/src/model_type_sizes.rs +++ b/examples/testing/src/model_type_sizes.rs @@ -131,10 +131,12 @@ pub fn print_ranking() { ("ModalInteraction", std::mem::size_of::()), ("ModalInteractionData", std::mem::size_of::()), ("Options", std::mem::size_of::()), - ("PartialChannel", std::mem::size_of::()), + ("InteractionChannel", std::mem::size_of::()), + ("InteractionGuildChannel", std::mem::size_of::()), + ("InteractionGuildThread", std::mem::size_of::()), ("PartialCurrentApplicationInfo", std::mem::size_of::()), ("PartialGuild", std::mem::size_of::()), - ("PartialGuildChannel", std::mem::size_of::()), + ("PartialGuildThread", std::mem::size_of::()), ("PartialMember", std::mem::size_of::()), ("PermissionOverwrite", std::mem::size_of::()), ("Permissions", std::mem::size_of::()), diff --git a/src/builder/create_components.rs b/src/builder/create_components.rs index a2fc8842a92..0e326f8ff61 100644 --- a/src/builder/create_components.rs +++ b/src/builder/create_components.rs @@ -224,7 +224,7 @@ pub enum CreateSelectMenuKind<'a> { }, Channel { channel_types: Option>, - default_channels: Option>, + default_channels: Option>, }, } diff --git a/src/builder/create_message.rs b/src/builder/create_message.rs index 3b8a450e3a6..2d4671f0aca 100644 --- a/src/builder/create_message.rs +++ b/src/builder/create_message.rs @@ -290,7 +290,7 @@ impl<'a> CreateMessage<'a> { pub async fn execute( mut self, http: &Http, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, guild_id: Option, ) -> Result { self.check_length()?; diff --git a/src/builder/create_scheduled_event.rs b/src/builder/create_scheduled_event.rs index 2ee1c2fc9ea..e2a081b2b27 100644 --- a/src/builder/create_scheduled_event.rs +++ b/src/builder/create_scheduled_event.rs @@ -131,8 +131,8 @@ impl<'a> CreateScheduledEvent<'a> { /// /// [Create Events]: Permissions::CREATE_EVENTS #[cfg(feature = "http")] - pub async fn execute(self, http: &Http, channel_id: GuildId) -> Result { - http.create_scheduled_event(channel_id, &self, self.audit_log_reason).await + pub async fn execute(self, http: &Http, guild_id: GuildId) -> Result { + http.create_scheduled_event(guild_id, &self, self.audit_log_reason).await } } diff --git a/src/builder/edit_channel.rs b/src/builder/edit_channel.rs index 025e04b168a..c48bfc7f00c 100644 --- a/src/builder/edit_channel.rs +++ b/src/builder/edit_channel.rs @@ -331,6 +331,9 @@ impl<'a> EditChannel<'a> { .await?; } - http.edit_channel(channel_id, &self, self.audit_log_reason).await + http.edit_channel(channel_id.widen(), &self, self.audit_log_reason) + .await? + .guild() + .ok_or(Error::Model(ModelError::InvalidChannelType)) } } diff --git a/src/builder/edit_guild_welcome_screen.rs b/src/builder/edit_guild_welcome_screen.rs index 679337e7c9d..38cbfbe49be 100644 --- a/src/builder/edit_guild_welcome_screen.rs +++ b/src/builder/edit_guild_welcome_screen.rs @@ -82,14 +82,14 @@ impl<'a> EditGuildWelcomeScreen<'a> { #[derive(Clone, Debug, Serialize)] #[must_use] pub struct CreateGuildWelcomeChannel<'a> { - channel_id: ChannelId, + channel_id: ChannelOrThreadId, emoji_name: Option, emoji_id: Option, description: Cow<'a, str>, } impl<'a> CreateGuildWelcomeChannel<'a> { - pub fn new(channel_id: ChannelId, description: impl Into>) -> Self { + pub fn new(channel_id: ChannelOrThreadId, description: impl Into>) -> Self { Self { channel_id, emoji_id: None, @@ -99,7 +99,7 @@ impl<'a> CreateGuildWelcomeChannel<'a> { } /// The Id of the channel to show. - pub fn id(mut self, id: ChannelId) -> Self { + pub fn id(mut self, id: ChannelOrThreadId) -> Self { self.channel_id = id; self } diff --git a/src/builder/edit_message.rs b/src/builder/edit_message.rs index dc4fcf1c79f..abcaff6e037 100644 --- a/src/builder/edit_message.rs +++ b/src/builder/edit_message.rs @@ -239,7 +239,7 @@ impl<'a> EditMessage<'a> { pub async fn execute( mut self, cache_http: impl CacheHttp, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, user_id: Option, ) -> Result { diff --git a/src/builder/edit_thread.rs b/src/builder/edit_thread.rs index 4175226f31a..181c99f8df1 100644 --- a/src/builder/edit_thread.rs +++ b/src/builder/edit_thread.rs @@ -108,8 +108,12 @@ impl<'a> EditThread<'a> { /// # Errors /// /// Returns [`Error::Http`] if the current user lacks permission. + /// Returns [`ModelError::InvalidChannelType`] if the `ThreadId` is not identifying a thread. #[cfg(feature = "http")] - pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { - http.edit_thread(channel_id, &self, self.audit_log_reason).await + pub async fn execute(self, http: &Http, thread_id: ThreadId) -> Result { + http.edit_channel(thread_id.widen(), &self, self.audit_log_reason) + .await? + .thread() + .ok_or(Error::Model(ModelError::InvalidChannelType)) } } diff --git a/src/builder/edit_webhook_message.rs b/src/builder/edit_webhook_message.rs index bbed86fe17d..38928c90388 100644 --- a/src/builder/edit_webhook_message.rs +++ b/src/builder/edit_webhook_message.rs @@ -31,7 +31,7 @@ pub struct EditWebhookMessage<'a> { pub(crate) attachments: Option>, #[serde(skip)] - thread_id: Option, + thread_id: Option, } impl<'a> EditWebhookMessage<'a> { @@ -55,7 +55,7 @@ impl<'a> EditWebhookMessage<'a> { /// Edits a message within a given thread. If the provided thread Id doesn't belong to the /// current webhook, the API will return an error. - pub fn in_thread(mut self, thread_id: ChannelId) -> Self { + pub fn in_thread(mut self, thread_id: ThreadId) -> Self { self.thread_id = Some(thread_id); self } diff --git a/src/builder/execute_webhook.rs b/src/builder/execute_webhook.rs index 02259b8d95d..623560cbf4d 100644 --- a/src/builder/execute_webhook.rs +++ b/src/builder/execute_webhook.rs @@ -76,7 +76,7 @@ pub struct ExecuteWebhook<'a> { attachments: EditAttachments<'a>, #[serde(skip)] - thread_id: Option, + thread_id: Option, } impl<'a> ExecuteWebhook<'a> { @@ -159,19 +159,19 @@ impl<'a> ExecuteWebhook<'a> { /// ```rust,no_run /// # use serenity::builder::ExecuteWebhook; /// # use serenity::http::Http; - /// # use serenity::model::{id::ChannelId, webhook::Webhook}; + /// # use serenity::model::{id::ThreadId, webhook::Webhook}; /// # /// # async fn run() -> Result<(), Box> { /// # let http: Http = unimplemented!(); /// let url = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// let mut webhook = Webhook::from_url(&http, url).await?; /// - /// let builder = ExecuteWebhook::new().in_thread(ChannelId::new(12345678)).content("test"); + /// let builder = ExecuteWebhook::new().in_thread(ThreadId::new(12345678)).content("test"); /// webhook.execute(&http, false, builder).await?; /// # Ok(()) /// # } /// ``` - pub fn in_thread(mut self, thread_id: ChannelId) -> Self { + pub fn in_thread(mut self, thread_id: ThreadId) -> Self { self.thread_id = Some(thread_id); self } diff --git a/src/builder/get_messages.rs b/src/builder/get_messages.rs index d34e4f68030..f5e65f954c2 100644 --- a/src/builder/get_messages.rs +++ b/src/builder/get_messages.rs @@ -106,7 +106,7 @@ impl GetMessages { pub async fn execute( self, cache_http: impl CacheHttp, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, ) -> Result> { let http = cache_http.http(); let search_filter = self.search_filter.map(Into::into); diff --git a/src/cache/event.rs b/src/cache/event.rs index 64cdeb3f016..24c2ce82c74 100644 --- a/src/cache/event.rs +++ b/src/cache/event.rs @@ -1,6 +1,6 @@ use std::collections::{HashSet, VecDeque}; -use super::{Cache, CacheUpdate}; +use super::{BaseGuildChannel, Cache, CacheUpdate, ChannelOrThreadId, GuildThread}; use crate::internal::prelude::*; use crate::model::channel::{GuildChannel, Message}; use crate::model::event::{ @@ -42,7 +42,7 @@ impl CacheUpdate for ChannelCreateEvent { fn update(&mut self, cache: &Cache) -> Option { let old_channel = cache .guilds - .get_mut(&self.channel.guild_id) + .get_mut(&self.channel.base.guild_id) .and_then(|mut g| g.channels.insert(self.channel.clone())); old_channel @@ -53,12 +53,12 @@ impl CacheUpdate for ChannelDeleteEvent { type Output = VecDeque; fn update(&mut self, cache: &Cache) -> Option> { - let (channel_id, guild_id) = (self.channel.id, self.channel.guild_id); + let (channel_id, guild_id) = (self.channel.id, self.channel.base.guild_id); cache.guilds.get_mut(&guild_id).map(|mut g| g.channels.remove(&channel_id)); // Remove the cached messages for the channel. - cache.messages.remove(&channel_id).map(|(_, messages)| messages) + cache.messages.remove(&channel_id.widen()).map(|(_, messages)| messages) } } @@ -68,7 +68,7 @@ impl CacheUpdate for ChannelUpdateEvent { fn update(&mut self, cache: &Cache) -> Option { cache .guilds - .get_mut(&self.channel.guild_id) + .get_mut(&self.channel.base.guild_id) .and_then(|mut g| g.channels.insert(self.channel.clone())) } } @@ -79,8 +79,14 @@ impl CacheUpdate for ChannelPinsUpdateEvent { fn update(&mut self, cache: &Cache) -> Option<()> { if let Some(guild_id) = self.guild_id { if let Some(mut guild) = cache.guilds.get_mut(&guild_id) { - if let Some(mut channel) = guild.channels.get_mut(&self.channel_id) { - channel.last_pin_timestamp = self.last_pin_timestamp; + let (channel_id, thread_id) = self.channel_id.split(); + if let Some(mut channel) = guild.channels.get_mut(&channel_id) { + channel.base.last_pin_timestamp = self.last_pin_timestamp; + return None; + } + + if let Some(mut thread) = guild.threads.get_mut(&thread_id) { + thread.base.last_pin_timestamp = self.last_pin_timestamp; } } } @@ -117,7 +123,7 @@ impl CacheUpdate for GuildDeleteEvent { Some(guild) => { for channel in &guild.1.channels { // Remove the channel's cached messages. - cache.messages.remove(&channel.id); + cache.messages.remove(&channel.id.into()); } Some(guild.1) @@ -321,20 +327,14 @@ impl CacheUpdate for MessageCreateEvent { let guild = self.message.guild_id.and_then(|g_id| cache.guilds.get_mut(&g_id)); if let Some(mut guild) = guild { - let mut found_channel = false; - if let Some(mut channel) = guild.channels.get_mut(&self.message.channel_id) { - update_channel_last_message_id(&self.message, &mut channel, cache); - found_channel = true; + let shared_id = self.message.channel_id; + let (channel_id, thread_id) = shared_id.split(); + if let Some(mut channel) = guild.channels.get_mut(&channel_id) { + update_channel_last_message_id(&self.message, &mut channel.base, shared_id, cache); } - // found_channel is to avoid limitations of the NLL borrow checker. - if !found_channel { - // This may be a thread. - let thread = - guild.threads.iter_mut().find(|thread| thread.id == self.message.channel_id); - if let Some(thread) = thread { - update_channel_last_message_id(&self.message, thread, cache); - } + if let Some(mut thread) = guild.threads.get_mut(&thread_id) { + update_channel_last_message_id(&self.message, &mut thread.base, shared_id, cache); } } @@ -360,9 +360,14 @@ impl CacheUpdate for MessageCreateEvent { } } -fn update_channel_last_message_id(message: &Message, channel: &mut GuildChannel, cache: &Cache) { +fn update_channel_last_message_id( + message: &Message, + channel: &mut BaseGuildChannel, + channel_id: ChannelOrThreadId, + cache: &Cache, +) { if let Some(last_message_id) = channel.last_message_id { - let most_recent_timestamp = cache.message(channel.id, last_message_id).map(|m| m.timestamp); + let most_recent_timestamp = cache.message(channel_id, last_message_id).map(|m| m.timestamp); if let Some(most_recent_timestamp) = most_recent_timestamp { if message.timestamp > most_recent_timestamp { channel.last_message_id = Some(message.id); @@ -476,68 +481,35 @@ impl CacheUpdate for ReadyEvent { } impl CacheUpdate for ThreadCreateEvent { - type Output = GuildChannel; + type Output = GuildThread; fn update(&mut self, cache: &Cache) -> Option { - let (guild_id, thread_id) = (self.thread.guild_id, self.thread.id); - - cache.guilds.get_mut(&guild_id).and_then(|mut g| { - if let Some(i) = g.threads.iter().position(|e| e.id == thread_id) { - Some(std::mem::replace(&mut g.threads[i as u32], self.thread.clone())) - } else { - // This is a rare enough occurence to realloc. - let mut threads = std::mem::take(&mut g.threads).into_vec(); - threads.push(self.thread.clone()); - - g.threads = FixedArray::try_from(threads.into_boxed_slice()) - .expect("A guild should not have 4 billion threads"); - - None - } - }) + cache + .guilds + .get_mut(&self.thread.base.guild_id) + .and_then(|mut g| g.threads.insert(self.thread.clone())) } } impl CacheUpdate for ThreadUpdateEvent { - type Output = GuildChannel; + type Output = GuildThread; fn update(&mut self, cache: &Cache) -> Option { - let (guild_id, thread_id) = (self.thread.guild_id, self.thread.id); - - cache.guilds.get_mut(&guild_id).and_then(|mut g| { - if let Some(i) = g.threads.iter().position(|e| e.id == thread_id) { - Some(std::mem::replace(&mut g.threads[i as u32], self.thread.clone())) - } else { - // This is a rare enough occurence to realloc. - let mut threads = std::mem::take(&mut g.threads).into_vec(); - threads.push(self.thread.clone()); - - g.threads = FixedArray::try_from(threads.into_boxed_slice()) - .expect("A guild should not have 4 billion threads"); - - None - } - }) + cache + .guilds + .get_mut(&self.thread.base.guild_id) + .and_then(|mut g| g.threads.insert(self.thread.clone())) } } impl CacheUpdate for ThreadDeleteEvent { - type Output = GuildChannel; + type Output = GuildThread; fn update(&mut self, cache: &Cache) -> Option { - let (guild_id, thread_id) = (self.thread.guild_id, self.thread.id); - - cache.guilds.get_mut(&guild_id).and_then(|mut g| { - g.threads.iter().position(|e| e.id == thread_id).map(|i| { - let mut threads = std::mem::take(&mut g.threads).into_vec(); - let thread = threads.remove(i); - - g.threads = FixedArray::try_from(threads.into_boxed_slice()) - .expect("A guild should not have 4 billion threads"); - - thread - }) - }) + cache + .guilds + .get_mut(&self.thread.guild_id) + .and_then(|mut g| g.threads.remove(&self.thread.id)) } } diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 5ab26c85809..b62ec4322f9 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -112,8 +112,8 @@ pub type UserRef<'a> = CacheRef<'a, UserId, User, Never>; pub type GuildRef<'a> = CacheRef<'a, GuildId, Guild, Never>; pub type SettingsRef<'a> = CacheRef<'a, Never, Settings, Never>; pub type CurrentUserRef<'a> = CacheRef<'a, Never, CurrentUser, Never>; -pub type MessageRef<'a> = CacheRef<'a, ChannelId, Message, VecDeque>; -pub type ChannelMessagesRef<'a> = CacheRef<'a, ChannelId, VecDeque, Never>; +pub type MessageRef<'a> = CacheRef<'a, ChannelOrThreadId, Message, VecDeque>; +pub type ChannelMessagesRef<'a> = CacheRef<'a, ChannelOrThreadId, VecDeque, Never>; #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Debug)] @@ -152,6 +152,11 @@ pub struct Cache { /// The TTL for each value is configured in CacheSettings. #[cfg(feature = "temp_cache")] pub(crate) temp_channels: MokaCache, BuildHasher>, + /// Cache of threads that have been fetched via to_channel. + /// + /// The TTL for each value is configured in CacheSettings. + #[cfg(feature = "temp_cache")] + pub(crate) temp_threads: MokaCache, BuildHasher>, /// Cache of private channels created via create_dm_channel. /// /// The TTL for each value is configured in CacheSettings. @@ -181,7 +186,7 @@ pub struct Cache { // Messages cache: // --- - pub(crate) messages: DashMap, BuildHasher>, + pub(crate) messages: DashMap, BuildHasher>, // Miscellanous fixed-size data // --- @@ -234,6 +239,8 @@ impl Cache { #[cfg(feature = "temp_cache")] temp_channels: temp_cache(settings.time_to_live), #[cfg(feature = "temp_cache")] + temp_threads: temp_cache(settings.time_to_live), + #[cfg(feature = "temp_cache")] temp_messages: temp_cache(settings.time_to_live), #[cfg(feature = "temp_cache")] temp_users: temp_cache(settings.time_to_live), @@ -343,7 +350,10 @@ impl Cache { /// messages_in_channel.iter().filter(|m| m.author.id == 8).collect(); /// } /// ``` - pub fn channel_messages(&self, channel_id: ChannelId) -> Option> { + pub fn channel_messages( + &self, + channel_id: ChannelOrThreadId, + ) -> Option> { self.messages.get(&channel_id).map(CacheRef::from_ref) } @@ -405,7 +415,11 @@ impl Cache { /// ``` /// /// [`EventHandler::message`]: crate::gateway::client::EventHandler::message - pub fn message(&self, channel_id: ChannelId, message_id: MessageId) -> Option> { + pub fn message( + &self, + channel_id: ChannelOrThreadId, + message_id: MessageId, + ) -> Option> { #[cfg(feature = "temp_cache")] if let Some(message) = self.temp_messages.get(&message_id) { return Some(CacheRef::from_arc(message)); @@ -466,7 +480,7 @@ impl Cache { ) -> Option> { let guild = self.guilds.get(&guild_id)?; - let filter = |channel: &&GuildChannel| channel.kind == ChannelType::Category; + let filter = |channel: &&GuildChannel| channel.base.kind == ChannelType::Category; Some(guild.channels.iter().filter(filter).cloned().collect()) } @@ -476,7 +490,7 @@ impl Cache { /// contains randomly ordered messages, and respects the [`Settings::max_messages`] setting. pub(crate) fn fill_message_cache( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, new_messages: impl Iterator, ) { let max_messages = self.settings().max_messages; @@ -567,8 +581,11 @@ mod test { } let channel = GuildChannel { - id: event.message.channel_id, - guild_id: event.message.guild_id.unwrap(), + id: event.message.channel_id.expect_channel(), + base: BaseGuildChannel { + guild_id: event.message.guild_id.unwrap(), + ..Default::default() + }, ..Default::default() }; @@ -578,7 +595,7 @@ mod test { channel: channel.clone(), }; assert!(cache.update(&mut delete).is_some()); - assert!(!cache.messages.contains_key(&delete.channel.id)); + assert!(!cache.messages.contains_key(&delete.channel.id.into())); // Test deletion of a guild channel's message cache when a GuildDeleteEvent is received. let mut guild_create = GuildCreateEvent { @@ -602,6 +619,6 @@ mod test { assert!(cache.update(&mut guild_delete).is_some()); // Assert that the channel's message cache no longer exists. - assert!(!cache.messages.contains_key(&ChannelId::new(2))); + assert!(!cache.messages.contains_key(&ChannelOrThreadId::new(2))); } } diff --git a/src/collector.rs b/src/collector.rs index 9fa3db23af2..7bc487d6e55 100644 --- a/src/collector.rs +++ b/src/collector.rs @@ -158,7 +158,7 @@ make_specific_collector!( // - filter argument type (used as argument of the builder-like method on the collector type) // - filter expression (this expressoin must return true to let the event through) author_id: UserId => interaction.user.id == *author_id, - channel_id: ChannelId => interaction.channel_id == *channel_id, + channel_id: ChannelOrThreadId => interaction.channel_id == *channel_id, guild_id: GuildId => interaction.guild_id.map_or(true, |x| x == *guild_id), message_id: MessageId => interaction.message.id == *message_id, custom_ids: FixedArray => custom_ids.contains(&interaction.data.custom_id), @@ -169,7 +169,7 @@ make_specific_collector!( interaction: Interaction::Modal(interaction), }) => interaction, author_id: UserId => interaction.user.id == *author_id, - channel_id: ChannelId => interaction.channel_id == *channel_id, + channel_id: ChannelOrThreadId => interaction.channel_id == *channel_id, guild_id: GuildId => interaction.guild_id.map_or(true, |g| g == *guild_id), message_id: MessageId => interaction.message.as_ref().map_or(true, |m| m.id == *message_id), custom_ids: Vec => custom_ids.contains(&interaction.data.custom_id), @@ -178,7 +178,7 @@ make_specific_collector!( ReactionCollector, Reaction, Event::ReactionAdd(ReactionAddEvent { reaction }) => reaction, author_id: UserId => reaction.user_id.map_or(true, |a| a == *author_id), - channel_id: ChannelId => reaction.channel_id == *channel_id, + channel_id: ChannelOrThreadId => reaction.channel_id == *channel_id, guild_id: GuildId => reaction.guild_id.map_or(true, |g| g == *guild_id), message_id: MessageId => reaction.message_id == *message_id, ); @@ -186,6 +186,6 @@ make_specific_collector!( MessageCollector, Message, Event::MessageCreate(MessageCreateEvent { message }) => message, author_id: UserId => message.author.id == *author_id, - channel_id: ChannelId => message.channel_id == *channel_id, + channel_id: ChannelOrThreadId => message.channel_id == *channel_id, guild_id: GuildId => message.guild_id.map_or(true, |g| g == *guild_id), ); diff --git a/src/gateway/client/dispatch.rs b/src/gateway/client/dispatch.rs index fba9e54b907..72abc78963c 100644 --- a/src/gateway/client/dispatch.rs +++ b/src/gateway/client/dispatch.rs @@ -110,7 +110,7 @@ fn update_cache_with_event( update_cache!(cache, event); let channel = event.channel; - if channel.kind == ChannelType::Category { + if channel.base.kind == ChannelType::Category { FullEvent::CategoryCreate { category: channel, } @@ -124,7 +124,7 @@ fn update_cache_with_event( let cached_messages = if_cache!(event.update(cache)); let channel = event.channel; - if channel.kind == ChannelType::Category { + if channel.base.kind == ChannelType::Category { FullEvent::CategoryDelete { category: channel, } diff --git a/src/gateway/client/event_handler.rs b/src/gateway/client/event_handler.rs index d7766c6f617..a3e950b4a63 100644 --- a/src/gateway/client/event_handler.rs +++ b/src/gateway/client/event_handler.rs @@ -289,12 +289,12 @@ event_handler! { /// Dispatched when a message is deleted. /// /// Provides the guild's id, the channel's id and the message's id. - MessageDelete { channel_id: ChannelId, deleted_message_id: MessageId, guild_id: Option } => async fn message_delete(&self, ctx: Context); + MessageDelete { channel_id: ChannelOrThreadId, deleted_message_id: MessageId, guild_id: Option } => async fn message_delete(&self, ctx: Context); /// Dispatched when multiple messages were deleted at once. /// /// Provides the guild's id, channel's id and the deleted messages' ids. - MessageDeleteBulk { channel_id: ChannelId, multiple_deleted_messages_ids: Vec, guild_id: Option } => async fn message_delete_bulk(&self, ctx: Context); + MessageDeleteBulk { channel_id: ChannelOrThreadId, multiple_deleted_messages_ids: Vec, guild_id: Option } => async fn message_delete_bulk(&self, ctx: Context); /// Dispatched when a message is updated. /// @@ -315,7 +315,7 @@ event_handler! { /// Dispatched when all reactions of a message are detached from a message. /// /// Provides the channel's id and the message's id. - ReactionRemoveAll { channel_id: ChannelId, removed_from_message_id: MessageId } => async fn reaction_remove_all(&self, ctx: Context); + ReactionRemoveAll { channel_id: ChannelOrThreadId, removed_from_message_id: MessageId } => async fn reaction_remove_all(&self, ctx: Context); /// Dispatched when all reactions of a message are detached from a message. /// @@ -411,18 +411,18 @@ event_handler! { /// Dispatched when a thread is created or the current user is added to a private thread. /// /// Provides the thread. - ThreadCreate { thread: GuildChannel } => async fn thread_create(&self, ctx: Context); + ThreadCreate { thread: GuildThread } => async fn thread_create(&self, ctx: Context); /// Dispatched when a thread is updated. /// /// Provides the updated thread and the old thread data, provided the thread was cached prior to dispatch. - ThreadUpdate { old: Option, new: GuildChannel } => async fn thread_update(&self, ctx: Context); + ThreadUpdate { old: Option, new: GuildThread } => async fn thread_update(&self, ctx: Context); /// Dispatched when a thread is deleted. /// /// Provides the partial data about the deleted thread and, if it was present in the cache /// before its deletion, its full data. - ThreadDelete { thread: PartialGuildChannel, full_thread_data: Option } => async fn thread_delete(&self, ctx: Context); + ThreadDelete { thread: PartialGuildThread, full_thread_data: Option } => async fn thread_delete(&self, ctx: Context); /// Dispatched when the current user gains access to a channel. /// diff --git a/src/http/client.rs b/src/http/client.rs index a6df0701a49..6b3a4e17cad 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -379,7 +379,7 @@ impl Http { } /// Broadcasts that the current user is typing in the given [`Channel`]. - pub async fn broadcast_typing(&self, channel_id: ChannelId) -> Result<()> { + pub async fn broadcast_typing(&self, channel_id: ChannelOrThreadId) -> Result<()> { self.wind(204, Request { body: None, multipart: None, @@ -790,7 +790,7 @@ impl Http { async fn create_reaction_( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, reaction_type: &ReactionType, burst: bool, @@ -813,7 +813,7 @@ impl Http { /// Reacts to a message. pub async fn create_reaction( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, reaction_type: &ReactionType, ) -> Result<()> { @@ -823,7 +823,7 @@ impl Http { /// Super reacts to a message. pub async fn create_super_reaction( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, reaction_type: &ReactionType, ) -> Result<()> { @@ -967,7 +967,7 @@ impl Http { /// Deletes a private channel or a channel in a guild. pub async fn delete_channel( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, audit_log_reason: Option<&str>, ) -> Result { self.fire(Request { @@ -1155,7 +1155,7 @@ impl Http { /// Deletes a message if created by us or we have specific permissions. pub async fn delete_message( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, audit_log_reason: Option<&str>, ) -> Result<()> { @@ -1176,7 +1176,7 @@ impl Http { /// Deletes a bunch of messages, only works for bots. pub async fn delete_messages( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, map: &impl serde::Serialize, audit_log_reason: Option<&str>, ) -> Result<()> { @@ -1196,7 +1196,7 @@ impl Http { /// Deletes all of the [`Reaction`]s associated with a [`Message`]. pub async fn delete_message_reactions( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, ) -> Result<()> { self.wind(204, Request { @@ -1216,7 +1216,7 @@ impl Http { /// Deletes all the reactions for a given emoji on a message. pub async fn delete_message_reaction_emoji( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, reaction_type: &ReactionType, ) -> Result<()> { @@ -1278,7 +1278,7 @@ impl Http { /// Deletes a user's reaction from a message. pub async fn delete_reaction( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, user_id: UserId, reaction_type: &ReactionType, @@ -1302,7 +1302,7 @@ impl Http { /// Deletes a reaction by the current user from a message. pub async fn delete_reaction_me( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, reaction_type: &ReactionType, ) -> Result<()> { @@ -1445,14 +1445,12 @@ impl Http { /// Changes channel information. pub async fn edit_channel( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, map: &impl serde::Serialize, audit_log_reason: Option<&str>, - ) -> Result { - let body = to_vec(map)?; - + ) -> Result { self.fire(Request { - body: Some(body), + body: Some(to_vec(map)?), multipart: None, headers: audit_log_reason.map(reason_into_header), method: LightMethod::Patch, @@ -1802,7 +1800,7 @@ impl Http { /// **Note**: Only the author of a message can modify it. pub async fn edit_message( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, map: &impl serde::Serialize, new_attachments: Vec>, @@ -2105,26 +2103,6 @@ impl Http { from_value(value).map_err(From::from) } - /// Edits a thread channel in the [`GuildChannel`] given its Id. - pub async fn edit_thread( - &self, - channel_id: ChannelId, - map: &impl serde::Serialize, - audit_log_reason: Option<&str>, - ) -> Result { - self.fire(Request { - body: Some(to_vec(map)?), - multipart: None, - headers: audit_log_reason.map(reason_into_header), - method: LightMethod::Patch, - route: Route::Channel { - channel_id, - }, - params: None, - }) - .await - } - /// Changes another user's voice state in a stage channel. pub async fn edit_voice_state( &self, @@ -2235,7 +2213,7 @@ impl Http { pub async fn execute_webhook( &self, webhook_id: WebhookId, - thread_id: Option, + thread_id: Option, token: &str, wait: bool, files: Vec>, @@ -2282,7 +2260,7 @@ impl Http { pub async fn get_webhook_message( &self, webhook_id: WebhookId, - thread_id: Option, + thread_id: Option, token: &str, message_id: MessageId, ) -> Result { @@ -2313,7 +2291,7 @@ impl Http { pub async fn edit_webhook_message( &self, webhook_id: WebhookId, - thread_id: Option, + thread_id: Option, token: &str, message_id: MessageId, map: &impl serde::Serialize, @@ -2357,7 +2335,7 @@ impl Http { pub async fn delete_webhook_message( &self, webhook_id: WebhookId, - thread_id: Option, + thread_id: Option, token: &str, message_id: MessageId, ) -> Result<()> { @@ -2626,7 +2604,7 @@ impl Http { /// Gets all thread members for a thread. pub async fn get_channel_thread_members( &self, - channel_id: ChannelId, + thread_id: ThreadId, ) -> Result> { self.fire(Request { body: None, @@ -2750,7 +2728,7 @@ impl Http { } /// Joins a thread channel. - pub async fn join_thread_channel(&self, channel_id: ChannelId) -> Result<()> { + pub async fn join_thread_channel(&self, thread_id: ThreadId) -> Result<()> { self.wind(204, Request { body: None, multipart: None, @@ -2765,7 +2743,7 @@ impl Http { } /// Leaves a thread channel. - pub async fn leave_thread_channel(&self, channel_id: ChannelId) -> Result<()> { + pub async fn leave_thread_channel(&self, thread_id: ThreadId) -> Result<()> { self.wind(204, Request { body: None, multipart: None, @@ -2782,7 +2760,7 @@ impl Http { /// Adds a member to a thread channel. pub async fn add_thread_channel_member( &self, - channel_id: ChannelId, + thread_id: ThreadId, user_id: UserId, ) -> Result<()> { self.wind(204, Request { @@ -2802,7 +2780,7 @@ impl Http { /// Removes a member from a thread channel. pub async fn remove_thread_channel_member( &self, - channel_id: ChannelId, + thread_id: ThreadId, user_id: UserId, ) -> Result<()> { self.wind(204, Request { @@ -2821,7 +2799,7 @@ impl Http { pub async fn get_thread_channel_member( &self, - channel_id: ChannelId, + thread_id: ThreadId, user_id: UserId, with_member: bool, ) -> Result { @@ -2855,7 +2833,7 @@ impl Http { } /// Gets channel information. - pub async fn get_channel(&self, channel_id: ChannelId) -> Result { + pub async fn get_channel(&self, channel_id: ChannelOrThreadId) -> Result { self.fire(Request { body: None, multipart: None, @@ -2905,7 +2883,7 @@ impl Http { /// Get a list of users that voted for this specific answer. pub async fn get_poll_answer_voters( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, answer_id: AnswerId, after: Option, @@ -2948,7 +2926,7 @@ impl Http { pub async fn expire_poll( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, ) -> Result { self.fire(Request { @@ -3805,7 +3783,7 @@ impl Http { /// Gets a message by an Id, bots only. pub async fn get_message( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, ) -> Result { self.fire(Request { @@ -3825,7 +3803,7 @@ impl Http { /// Gets X messages from a channel. pub async fn get_messages( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, target: Option, limit: Option, ) -> Result> { @@ -3896,7 +3874,7 @@ impl Http { } /// Gets all pins of a channel. - pub async fn get_pins(&self, channel_id: ChannelId) -> Result> { + pub async fn get_pins(&self, channel_id: ChannelOrThreadId) -> Result> { self.fire(Request { body: None, multipart: None, @@ -3913,7 +3891,7 @@ impl Http { /// Gets user Ids based on their reaction to a message. This endpoint is dumb. pub async fn get_reaction_users( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, reaction_type: &ReactionType, limit: u8, @@ -4182,7 +4160,7 @@ impl Http { /// Sends a message to a channel. pub async fn send_message( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, files: Vec>, map: &impl serde::Serialize, ) -> Result { @@ -4213,7 +4191,7 @@ impl Http { /// Pins a message in a channel. pub async fn pin_message( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, audit_log_reason: Option<&str>, ) -> Result<()> { @@ -4352,7 +4330,7 @@ impl Http { /// Unpins a message from a channel. pub async fn unpin_message( &self, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, message_id: MessageId, audit_log_reason: Option<&str>, ) -> Result<()> { diff --git a/src/http/routing.rs b/src/http/routing.rs index 7602244f385..041769759c5 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -89,7 +89,7 @@ macro_rules! routes { // 2. The second line provides the url for that endpoint. // 3. The third line indicates what type of ratelimiting the endpoint employs. routes! ('a, { - Channel { channel_id: ChannelId }, + Channel { channel_id: ChannelOrThreadId }, api!("/channels/{}", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); @@ -97,7 +97,7 @@ routes! ('a, { api!("/channels/{}/invites", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelMessage { channel_id: ChannelId, message_id: MessageId }, + ChannelMessage { channel_id: ChannelOrThreadId, message_id: MessageId }, api!("/channels/{}/messages/{}", channel_id, message_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); @@ -105,27 +105,27 @@ routes! ('a, { api!("/channels/{}/messages/{}/crosspost", channel_id, message_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelMessageReaction { channel_id: ChannelId, message_id: MessageId, user_id: UserId, reaction: &'a str }, + ChannelMessageReaction { channel_id: ChannelOrThreadId, message_id: MessageId, user_id: UserId, reaction: &'a str }, api!("/channels/{}/messages/{}/reactions/{}/{}", channel_id, message_id, reaction, user_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelMessageReactionMe { channel_id: ChannelId, message_id: MessageId, reaction: &'a str }, + ChannelMessageReactionMe { channel_id: ChannelOrThreadId, message_id: MessageId, reaction: &'a str }, api!("/channels/{}/messages/{}/reactions/{}/@me", channel_id, message_id, reaction), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelMessageReactionEmoji { channel_id: ChannelId, message_id: MessageId, reaction: &'a str }, + ChannelMessageReactionEmoji { channel_id: ChannelOrThreadId, message_id: MessageId, reaction: &'a str }, api!("/channels/{}/messages/{}/reactions/{}", channel_id, message_id, reaction), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelMessageReactions { channel_id: ChannelId, message_id: MessageId }, + ChannelMessageReactions { channel_id: ChannelOrThreadId, message_id: MessageId }, api!("/channels/{}/messages/{}/reactions", channel_id, message_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelMessages { channel_id: ChannelId }, + ChannelMessages { channel_id: ChannelOrThreadId }, api!("/channels/{}/messages", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelMessagesBulkDelete { channel_id: ChannelId }, + ChannelMessagesBulkDelete { channel_id: ChannelOrThreadId }, api!("/channels/{}/messages/bulk-delete", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); @@ -137,15 +137,15 @@ routes! ('a, { api!("/channels/{}/permissions/{}", channel_id, target_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelPin { channel_id: ChannelId, message_id: MessageId }, + ChannelPin { channel_id: ChannelOrThreadId, message_id: MessageId }, api!("/channels/{}/pins/{}", channel_id, message_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelPins { channel_id: ChannelId }, + ChannelPins { channel_id: ChannelOrThreadId }, api!("/channels/{}/pins", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelTyping { channel_id: ChannelId }, + ChannelTyping { channel_id: ChannelOrThreadId }, api!("/channels/{}/typing", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); @@ -165,15 +165,15 @@ routes! ('a, { api!("/channels/{}/threads", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelThreadMember { channel_id: ChannelId, user_id: UserId }, + ChannelThreadMember { thread_id: ThreadId, user_id: UserId }, api!("/channels/{}/thread-members/{}", channel_id, user_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelThreadMemberMe { channel_id: ChannelId }, + ChannelThreadMemberMe { thread_id: ThreadId }, api!("/channels/{}/thread-members/@me", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelThreadMembers { channel_id: ChannelId }, + ChannelThreadMembers { thread_id: ThreadId }, api!("/channels/{}/thread-members", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); @@ -189,11 +189,11 @@ routes! ('a, { api!("/channels/{}/users/@me/threads/archived/private", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelPollGetAnswerVoters { channel_id: ChannelId, message_id: MessageId, answer_id: AnswerId }, + ChannelPollGetAnswerVoters { channel_id: ChannelOrThreadId, message_id: MessageId, answer_id: AnswerId }, api!("/channels/{}/polls/{}/answers/{}", channel_id, message_id, answer_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); - ChannelPollExpire { channel_id: ChannelId, message_id: MessageId }, + ChannelPollExpire { channel_id: ChannelOrThreadId, message_id: MessageId }, api!("/channels/{}/polls/{}/expire", channel_id, message_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); diff --git a/src/http/typing.rs b/src/http/typing.rs index 43ec79889e9..d2aa3cb86b1 100644 --- a/src/http/typing.rs +++ b/src/http/typing.rs @@ -7,7 +7,7 @@ use tokio::time::{sleep, Duration}; use crate::http::Http; use crate::internal::prelude::*; use crate::internal::tokio::spawn_named; -use crate::model::id::ChannelId; +use crate::model::id::ChannelOrThreadId; /// A struct to start typing in a [`Channel`] for an indefinite period of time. /// @@ -60,7 +60,7 @@ impl Typing { /// Returns an [`Error::Http`] if there is an error. /// /// [`Channel`]: crate::model::channel::Channel - pub fn start(http: Arc, channel_id: ChannelId) -> Self { + pub fn start(http: Arc, channel_id: ChannelOrThreadId) -> Self { let (sx, mut rx) = oneshot::channel(); spawn_named::<_, Result<_>>("typing::start", async move { diff --git a/src/model/application/command_interaction.rs b/src/model/application/command_interaction.rs index f5dd7bb3319..0127f7c3d16 100644 --- a/src/model/application/command_interaction.rs +++ b/src/model/application/command_interaction.rs @@ -39,9 +39,9 @@ pub struct CommandInteraction { #[serde(skip_serializing_if = "Option::is_none")] pub guild_id: Option, /// Channel that the interaction was sent from. - pub channel: Option, + pub channel: Option, /// The channel Id this interaction was sent from. - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, /// The `member` data for the invoking user. /// /// **Note**: It is only present if the interaction is triggered in a guild. @@ -431,7 +431,7 @@ pub enum ResolvedValue<'a> { SubCommand(FixedArray>), SubCommandGroup(FixedArray>), Attachment(&'a Attachment), - Channel(&'a PartialChannel), + Channel(&'a InteractionChannel), Role(&'a Role), User(&'a User, Option<&'a PartialMember>), Unresolved(Unresolved), @@ -442,7 +442,7 @@ pub enum ResolvedValue<'a> { #[non_exhaustive] pub enum Unresolved { Attachment(AttachmentId), - Channel(ChannelId), + Channel(ChannelOrThreadId), Mentionable(GenericId), RoleId(RoleId), User(UserId), @@ -490,7 +490,7 @@ pub struct CommandDataResolved { skip_serializing_if = "ExtractMap::is_empty", serialize_with = "extract_map::serialize_as_map" )] - pub channels: ExtractMap, + pub channels: ExtractMap, /// The resolved messages. #[serde( default, @@ -651,7 +651,7 @@ pub enum CommandDataOptionValue { SubCommand(FixedArray), SubCommandGroup(FixedArray), Attachment(AttachmentId), - Channel(ChannelId), + Channel(ChannelOrThreadId), Mentionable(GenericId), Role(RoleId), User(UserId), @@ -730,7 +730,7 @@ impl CommandDataOptionValue { /// If the value is an `ChannelId`, returns the associated ID. Returns None otherwise. #[must_use] - pub fn as_channel_id(&self) -> Option { + pub fn as_channel_id(&self) -> Option { match self { Self::Channel(id) => Some(*id), _ => None, @@ -820,7 +820,7 @@ mod tests { value: CommandDataOptionValue::SubCommand( vec![CommandDataOption { name: FixedString::from_static_trunc("channel"), - value: CommandDataOptionValue::Channel(ChannelId::new(3)), + value: CommandDataOptionValue::Channel(ChannelOrThreadId::new(3)), }] .trunc_into(), ), diff --git a/src/model/application/component_interaction.rs b/src/model/application/component_interaction.rs index f3565cf1e04..27bc9e5d138 100644 --- a/src/model/application/component_interaction.rs +++ b/src/model/application/component_interaction.rs @@ -35,9 +35,9 @@ pub struct ComponentInteraction { #[serde(skip_serializing_if = "Option::is_none")] pub guild_id: Option, /// Channel that the interaction was sent from. - pub channel: Option, + pub channel: Option, /// The channel Id this interaction was sent from. - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, /// The `member` data for the invoking user. /// /// **Note**: It is only present if the interaction is triggered in a guild. diff --git a/src/model/application/modal_interaction.rs b/src/model/application/modal_interaction.rs index ee3cfb3a438..b4cf013dbaa 100644 --- a/src/model/application/modal_interaction.rs +++ b/src/model/application/modal_interaction.rs @@ -29,9 +29,9 @@ pub struct ModalInteraction { #[serde(skip_serializing_if = "Option::is_none")] pub guild_id: Option, /// Channel that the interaction was sent from. - pub channel: Option, + pub channel: Option, /// The channel Id this interaction was sent from. - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, /// The `member` data for the invoking user. /// /// **Note**: It is only present if the interaction is triggered in a guild. diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 46e49e28ea2..71632a18ca5 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -18,7 +18,6 @@ use crate::builder::{ EditChannel, EditMessage, EditStageInstance, - EditThread, GetMessages, }; #[cfg(all(feature = "cache", feature = "model"))] @@ -33,35 +32,13 @@ use crate::model::prelude::*; #[cfg(feature = "model")] impl ChannelId { - /// Broadcasts that the current user is typing to a channel for the next 5 seconds. - /// - /// After 5 seconds, another request must be made to continue broadcasting that the current - /// user is typing. - /// - /// This should rarely be used for bots, and should likely only be used for signifying that a - /// long-running command is still being executed. - /// - /// **Note**: Requires the [Send Messages] permission. - /// - /// # Examples - /// - /// ```rust,no_run - /// use serenity::model::id::ChannelId; + /// Converts the type of this Id to [`ChannelOrThreadId`]. /// - /// # async fn run() { - /// # let http: serenity::http::Http = unimplemented!(); - /// let _successful = ChannelId::new(7).broadcast_typing(&http).await; - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission to send messages to this - /// channel. - /// - /// [Send Messages]: Permissions::SEND_MESSAGES - pub async fn broadcast_typing(self, http: &Http) -> Result<()> { - http.broadcast_typing(self).await + /// This allows you to call methods which are shared between channels and threads, and does not + /// change the inner value at all. + #[must_use] + pub fn widen(self) -> ChannelOrThreadId { + self.into() } /// Creates an invite for the given channel. @@ -99,6 +76,329 @@ impl ChannelId { http.create_permission(self, data.id, &data, reason).await } + /// Deletes all permission overrides in the channel from a member or role. + /// + /// **Note**: Requires the [Manage Channel] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Channel]: Permissions::MANAGE_CHANNELS + pub async fn delete_permission( + self, + http: &Http, + permission_type: PermissionOverwriteType, + reason: Option<&str>, + ) -> Result<()> { + let id = match permission_type { + PermissionOverwriteType::Member(id) => id.into(), + PermissionOverwriteType::Role(id) => id.get().into(), + }; + http.delete_permission(self, id, reason).await + } + + /// Edits a channel's settings. + /// + /// Refer to the documentation for [`EditChannel`] for a full list of methods. + /// + /// **Note**: Requires the [Manage Channels] permission. Modifying permissions via + /// [`EditChannel::permissions`] also requires the [Manage Roles] permission. + /// + /// # Examples + /// + /// Change a voice channel's name and bitrate: + /// + /// ```rust,no_run + /// # use serenity::builder::EditChannel; + /// # use serenity::http::Http; + /// # use serenity::model::id::ChannelId; + /// # async fn run() { + /// # let http: Http = unimplemented!(); + /// # let channel_id = ChannelId::new(1234); + /// let builder = EditChannel::new().name("test").bitrate(64000); + /// channel_id.edit(&http, builder).await; + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. + /// + /// [Manage Channels]: Permissions::MANAGE_CHANNELS + /// [Manage Roles]: Permissions::MANAGE_ROLES + pub async fn edit(self, http: &Http, builder: EditChannel<'_>) -> Result { + builder.execute(http, self).await + } + + /// Edits a [`Message`] in the channel given its Id. + /// + /// Message editing preserves all unchanged message data, with some exceptions for embeds and + /// attachments. + /// + /// **Note**: In most cases requires that the current user be the author of the message. + /// + /// Refer to the documentation for [`EditMessage`] for information regarding content + /// restrictions and requirements. + /// + /// # Errors + /// + /// See [`EditMessage::execute`] for a list of possible errors, and their corresponding + /// reasons. + pub async fn edit_message( + self, + http: &Http, + message_id: MessageId, + builder: EditMessage<'_>, + ) -> Result { + builder.execute(http, self.into(), message_id, None).await + } + + /// Follows the News Channel + /// + /// Requires [Manage Webhook] permissions on the target channel. + /// + /// **Note**: Only available on news channels. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. [Manage Webhook]: + /// Permissions::MANAGE_WEBHOOKS + pub async fn follow( + self, + http: &Http, + target_channel_id: ChannelId, + ) -> Result { + #[derive(serde::Serialize)] + struct FollowChannel { + webhook_channel_id: ChannelId, + } + + let map = FollowChannel { + webhook_channel_id: target_channel_id, + }; + + http.follow_news_channel(self, &map).await + } + + /// Gets all of the channel's invites. + /// + /// Requires the [Manage Channels] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Channels]: Permissions::MANAGE_CHANNELS + pub async fn invites(self, http: &Http) -> Result> { + http.get_channel_invites(self).await + } + + /// Crossposts a [`Message`]. + /// + /// Requires either to be the message author or to have manage [Manage Messages] permissions on + /// this channel. + /// + /// **Note**: Only available on news channels. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, and if the user is not the + /// author of the message. + /// + /// [Manage Messages]: Permissions::MANAGE_MESSAGES + pub async fn crosspost(self, http: &Http, message_id: MessageId) -> Result { + http.crosspost_message(self, message_id).await + } + + /// Retrieves the channel's webhooks. + /// + /// **Note**: Requires the [Manage Webhooks] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Webhooks]: Permissions::MANAGE_WEBHOOKS + pub async fn webhooks(self, http: &Http) -> Result> { + http.get_channel_webhooks(self).await + } + + /// Creates a webhook in the channel. + /// + /// # Errors + /// + /// See [`CreateWebhook::execute`] for a detailed list of possible errors. + pub async fn create_webhook(self, http: &Http, builder: CreateWebhook<'_>) -> Result { + builder.execute(http, self).await + } + + /// Gets a stage instance. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, or if there is no stage + /// instance currently. + pub async fn get_stage_instance(self, http: &Http) -> Result { + http.get_stage_instance(self).await + } + + /// Creates a stage instance. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if there is already a stage instance currently. + pub async fn create_stage_instance( + self, + http: &Http, + builder: CreateStageInstance<'_>, + ) -> Result { + builder.execute(http, self).await + } + + /// Edits the stage instance + /// + /// # Errors + /// + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, or there is no stage + /// instance currently. + pub async fn edit_stage_instance( + self, + http: &Http, + builder: EditStageInstance<'_>, + ) -> Result { + builder.execute(http, self).await + } + + /// Deletes a stage instance. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, or if there is no stage + /// instance currently. + pub async fn delete_stage_instance(self, http: &Http, reason: Option<&str>) -> Result<()> { + http.delete_stage_instance(self, reason).await + } + + /// Creates a public thread that is connected to a message. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + #[doc(alias = "create_public_thread")] + pub async fn create_thread_from_message( + self, + http: &Http, + message_id: MessageId, + builder: CreateThread<'_>, + ) -> Result { + builder.execute(http, self, Some(message_id)).await + } + + /// Creates a thread that is not connected to a message. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + #[doc(alias = "create_public_thread", alias = "create_private_thread")] + pub async fn create_thread( + self, + http: &Http, + builder: CreateThread<'_>, + ) -> Result { + builder.execute(http, self, None).await + } + + /// Creates a post in a forum channel. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. + pub async fn create_forum_post( + self, + http: &Http, + builder: CreateForumPost<'_>, + ) -> Result { + builder.execute(http, self).await + } + + /// Gets private archived threads of a channel. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. + pub async fn get_archived_private_threads( + self, + http: &Http, + before: Option, + limit: Option, + ) -> Result { + http.get_channel_archived_private_threads(self, before, limit).await + } + + /// Gets public archived threads of a channel. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. + pub async fn get_archived_public_threads( + self, + http: &Http, + before: Option, + limit: Option, + ) -> Result { + http.get_channel_archived_public_threads(self, before, limit).await + } + + /// Gets private archived threads joined by the current user of a channel. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. + pub async fn get_joined_archived_private_threads( + self, + http: &Http, + before: Option, + limit: Option, + ) -> Result { + http.get_channel_joined_archived_private_threads(self, before, limit).await + } +} + +impl ChannelOrThreadId { + /// Broadcasts that the current user is typing to a channel for the next 5 seconds. + /// + /// After 5 seconds, another request must be made to continue broadcasting that the current + /// user is typing. + /// + /// This should rarely be used for bots, and should likely only be used for signifying that a + /// long-running command is still being executed. + /// + /// **Note**: Requires the [Send Messages] permission. + /// + /// # Examples + /// + /// ```rust,no_run + /// use serenity::model::id::ChannelId; + /// + /// # async fn run() { + /// # let http: serenity::http::Http = unimplemented!(); + /// let _successful = ChannelId::new(7).broadcast_typing(&http).await; + /// # } + /// ``` + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission to send messages to this + /// channel. + /// + /// [Send Messages]: Permissions::SEND_MESSAGES + pub async fn broadcast_typing(self, http: &Http) -> Result<()> { + http.broadcast_typing(self).await + } + /// React to a [`Message`] with a custom [`Emoji`] or unicode character. /// /// [`Message::react`] may be a more suited method of reacting in most cases. @@ -198,28 +498,6 @@ impl ChannelId { } } - /// Deletes all permission overrides in the channel from a member or role. - /// - /// **Note**: Requires the [Manage Channel] permission. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - /// - /// [Manage Channel]: Permissions::MANAGE_CHANNELS - pub async fn delete_permission( - self, - http: &Http, - permission_type: PermissionOverwriteType, - reason: Option<&str>, - ) -> Result<()> { - let id = match permission_type { - PermissionOverwriteType::Member(id) => id.into(), - PermissionOverwriteType::Role(id) => id.get().into(), - }; - http.delete_permission(self, id, reason).await - } - /// Deletes the given [`Reaction`] from the channel. /// /// **Note**: Requires the [Manage Messages] permission, _if_ the current user did not perform @@ -265,99 +543,16 @@ impl ChannelId { /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission. - /// - /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn delete_reaction_emoji( - self, - http: &Http, - message_id: MessageId, - reaction_type: impl Into, - ) -> Result<()> { - http.delete_message_reaction_emoji(self, message_id, &reaction_type.into()).await - } - - /// Edits a channel's settings. - /// - /// Refer to the documentation for [`EditChannel`] for a full list of methods. - /// - /// **Note**: Requires the [Manage Channels] permission. Modifying permissions via - /// [`EditChannel::permissions`] also requires the [Manage Roles] permission. - /// - /// # Examples - /// - /// Change a voice channel's name and bitrate: - /// - /// ```rust,no_run - /// # use serenity::builder::EditChannel; - /// # use serenity::http::Http; - /// # use serenity::model::id::ChannelId; - /// # async fn run() { - /// # let http: Http = unimplemented!(); - /// # let channel_id = ChannelId::new(1234); - /// let builder = EditChannel::new().name("test").bitrate(64000); - /// channel_id.edit(&http, builder).await; - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. - /// - /// [Manage Channels]: Permissions::MANAGE_CHANNELS - /// [Manage Roles]: Permissions::MANAGE_ROLES - pub async fn edit(self, http: &Http, builder: EditChannel<'_>) -> Result { - builder.execute(http, self).await - } - - /// Edits a [`Message`] in the channel given its Id. - /// - /// Message editing preserves all unchanged message data, with some exceptions for embeds and - /// attachments. - /// - /// **Note**: In most cases requires that the current user be the author of the message. - /// - /// Refer to the documentation for [`EditMessage`] for information regarding content - /// restrictions and requirements. - /// - /// # Errors - /// - /// See [`EditMessage::execute`] for a list of possible errors, and their corresponding - /// reasons. - pub async fn edit_message( - self, - http: &Http, - message_id: MessageId, - builder: EditMessage<'_>, - ) -> Result { - builder.execute(http, self, message_id, None).await - } - - /// Follows the News Channel - /// - /// Requires [Manage Webhook] permissions on the target channel. - /// - /// **Note**: Only available on news channels. - /// - /// # Errors + /// Returns [`Error::Http`] if the current user lacks permission. /// - /// Returns [`Error::Http`] if the current user lacks permission. [Manage Webhook]: - /// Permissions::MANAGE_WEBHOOKS - pub async fn follow( + /// [Manage Messages]: Permissions::MANAGE_MESSAGES + pub async fn delete_reaction_emoji( self, http: &Http, - target_channel_id: ChannelId, - ) -> Result { - #[derive(serde::Serialize)] - struct FollowChannel { - webhook_channel_id: ChannelId, - } - - let map = FollowChannel { - webhook_channel_id: target_channel_id, - }; - - http.follow_news_channel(self, &map).await + message_id: MessageId, + reaction_type: impl Into, + ) -> Result<()> { + http.delete_message_reaction_emoji(self, message_id, &reaction_type.into()).await } /// Attempts to retrieve the channel from the guild cache, otherwise from HTTP/temp cache. @@ -365,39 +560,57 @@ impl ChannelId { /// # Errors /// /// Returns [`Error::Http`] if the channel retrieval request failed. - #[cfg_attr(not(feature = "cache"), allow(unused_variables))] pub async fn to_channel( self, cache_http: impl CacheHttp, guild_id: Option, ) -> Result { + #[cfg_attr(not(feature = "cache"), expect(unused_variables))] + let (channel_id, thread_id) = self.split(); + #[cfg(feature = "cache")] if let Some(cache) = cache_http.cache() { if let Some(guild_id) = guild_id { if let Some(guild) = cache.guild(guild_id) { - if let Some(channel) = guild.channels.get(&self) { + if let Some(channel) = guild.channels.get(&channel_id) { return Ok(Channel::Guild(channel.clone())); } + + if let Some(thread) = guild.threads.get(&thread_id) { + return Ok(Channel::GuildThread(thread.clone())); + } } } #[cfg(feature = "temp_cache")] - if let Some(channel) = cache.temp_channels.get(&self) { - return Ok(Channel::Guild(GuildChannel::clone(&*channel))); + { + if let Some(channel) = cache.temp_channels.get(&channel_id) { + return Ok(Channel::Guild(GuildChannel::clone(&*channel))); + } + + if let Some(thread) = cache.temp_threads.get(&thread_id) { + return Ok(Channel::GuildThread(GuildThread::clone(&*thread))); + } } } let channel = cache_http.http().get_channel(self).await?; #[cfg(all(feature = "cache", feature = "temp_cache"))] - { - if let Some(cache) = cache_http.cache() { - if let Channel::Guild(guild_channel) = &channel { - use crate::cache::MaybeOwnedArc; + if let Some(cache) = cache_http.cache() { + use crate::cache::MaybeOwnedArc; + match &channel { + Channel::Guild(guild_channel) => { let cached_channel = MaybeOwnedArc::new(guild_channel.clone()); - cache.temp_channels.insert(cached_channel.id, cached_channel); - } + cache.temp_channels.insert(channel_id, cached_channel); + }, + Channel::GuildThread(guild_thread) => { + let cached_channel = MaybeOwnedArc::new(guild_thread.clone()); + cache.temp_threads.insert(thread_id, cached_channel); + }, + // No access to `UserId`, so this can't cache. + Channel::Private(_) => {}, } } @@ -420,26 +633,13 @@ impl ChannelId { let channel = self.to_channel(cache_http, guild_id).await?; let guild_channel = channel.guild().ok_or(ModelError::InvalidChannelType)?; - if guild_id.is_some_and(|id| guild_channel.guild_id != id) { + if guild_id.is_some_and(|id| guild_channel.base.guild_id != id) { return Err(Error::Model(ModelError::ChannelNotFound)); } Ok(guild_channel) } - /// Gets all of the channel's invites. - /// - /// Requires the [Manage Channels] permission. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - /// - /// [Manage Channels]: Permissions::MANAGE_CHANNELS - pub async fn invites(self, http: &Http) -> Result> { - http.get_channel_invites(self).await - } - /// Gets a message from the channel. /// /// If the cache feature is enabled the cache will be checked first. If not found it will @@ -544,23 +744,6 @@ impl ChannelId { http.pin_message(self, message_id, reason).await } - /// Crossposts a [`Message`]. - /// - /// Requires either to be the message author or to have manage [Manage Messages] permissions on - /// this channel. - /// - /// **Note**: Only available on news channels. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, and if the user is not the - /// author of the message. - /// - /// [Manage Messages]: Permissions::MANAGE_MESSAGES - pub async fn crosspost(self, http: &Http, message_id: MessageId) -> Result { - http.crosspost_message(self, message_id).await - } - /// Gets the list of [`Message`]s which are pinned to the channel. /// /// If the cache is enabled, this method will fill up the message cache for the channel, if the @@ -780,28 +963,6 @@ impl ChannelId { http.unpin_message(self, message_id, reason).await } - /// Retrieves the channel's webhooks. - /// - /// **Note**: Requires the [Manage Webhooks] permission. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - /// - /// [Manage Webhooks]: Permissions::MANAGE_WEBHOOKS - pub async fn webhooks(self, http: &Http) -> Result> { - http.get_channel_webhooks(self).await - } - - /// Creates a webhook in the channel. - /// - /// # Errors - /// - /// See [`CreateWebhook::execute`] for a detailed list of possible errors. - pub async fn create_webhook(self, http: &Http, builder: CreateWebhook<'_>) -> Result { - builder.execute(http, self).await - } - /// Returns a builder which can be awaited to obtain a message or stream of messages in this /// channel. #[cfg(feature = "collector")] @@ -828,209 +989,6 @@ impl ChannelId { self.await_reaction(shard_messenger) } - /// Gets a stage instance. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the channel is not a stage channel, or if there is no stage - /// instance currently. - pub async fn get_stage_instance(self, http: &Http) -> Result { - http.get_stage_instance(self).await - } - - /// Creates a stage instance. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if there is already a stage instance currently. - pub async fn create_stage_instance( - self, - http: &Http, - builder: CreateStageInstance<'_>, - ) -> Result { - builder.execute(http, self).await - } - - /// Edits the stage instance - /// - /// # Errors - /// - /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. - /// - /// Returns [`Error::Http`] if the channel is not a stage channel, or there is no stage - /// instance currently. - pub async fn edit_stage_instance( - self, - http: &Http, - builder: EditStageInstance<'_>, - ) -> Result { - builder.execute(http, self).await - } - - /// Edits a thread. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn edit_thread(self, http: &Http, builder: EditThread<'_>) -> Result { - builder.execute(http, self).await - } - - /// Deletes a stage instance. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the channel is not a stage channel, or if there is no stage - /// instance currently. - pub async fn delete_stage_instance(self, http: &Http, reason: Option<&str>) -> Result<()> { - http.delete_stage_instance(self, reason).await - } - - /// Creates a public thread that is connected to a message. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - #[doc(alias = "create_public_thread")] - pub async fn create_thread_from_message( - self, - http: &Http, - message_id: MessageId, - builder: CreateThread<'_>, - ) -> Result { - builder.execute(http, self, Some(message_id)).await - } - - /// Creates a thread that is not connected to a message. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - #[doc(alias = "create_public_thread", alias = "create_private_thread")] - pub async fn create_thread( - self, - http: &Http, - builder: CreateThread<'_>, - ) -> Result { - builder.execute(http, self, None).await - } - - /// Creates a post in a forum channel. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission, or if invalid data is given. - pub async fn create_forum_post( - self, - http: &Http, - builder: CreateForumPost<'_>, - ) -> Result { - builder.execute(http, self).await - } - - /// Gets the thread members, if this channel is a thread. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn get_thread_members(self, http: &Http) -> Result> { - http.get_channel_thread_members(self).await - } - - /// Joins the thread, if this channel is a thread. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn join_thread(self, http: &Http) -> Result<()> { - http.join_thread_channel(self).await - } - - /// Leaves the thread, if this channel is a thread. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn leave_thread(self, http: &Http) -> Result<()> { - http.leave_thread_channel(self).await - } - - /// Adds a thread member, if this channel is a thread. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn add_thread_member(self, http: &Http, user_id: UserId) -> Result<()> { - http.add_thread_channel_member(self, user_id).await - } - - /// Removes a thread member, if this channel is a thread. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn remove_thread_member(self, http: &Http, user_id: UserId) -> Result<()> { - http.remove_thread_channel_member(self, user_id).await - } - - /// Gets a thread member, if this channel is a thread. - /// - /// `with_member` controls if ThreadMember::member should be `Some` - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the channel is not a thread channel - pub async fn get_thread_member( - self, - http: &Http, - user_id: UserId, - with_member: bool, - ) -> Result { - http.get_thread_channel_member(self, user_id, with_member).await - } - - /// Gets private archived threads of a channel. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. - pub async fn get_archived_private_threads( - self, - http: &Http, - before: Option, - limit: Option, - ) -> Result { - http.get_channel_archived_private_threads(self, before, limit).await - } - - /// Gets public archived threads of a channel. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. - pub async fn get_archived_public_threads( - self, - http: &Http, - before: Option, - limit: Option, - ) -> Result { - http.get_channel_archived_public_threads(self, before, limit).await - } - - /// Gets private archived threads joined by the current user of a channel. - /// - /// # Errors - /// - /// It may return an [`Error::Http`] if the bot doesn't have the permission to get it. - pub async fn get_joined_archived_private_threads( - self, - http: &Http, - before: Option, - limit: Option, - ) -> Result { - http.get_channel_joined_archived_private_threads(self, before, limit).await - } - /// Get a list of users that voted for this specific answer. /// /// # Errors @@ -1058,17 +1016,17 @@ impl ChannelId { } #[cfg(feature = "model")] -impl From for ChannelId { +impl From for ChannelOrThreadId { /// Gets the Id of a [`Channel`]. - fn from(channel: Channel) -> ChannelId { + fn from(channel: Channel) -> Self { channel.id() } } #[cfg(feature = "model")] -impl From<&Channel> for ChannelId { +impl From<&Channel> for ChannelOrThreadId { /// Gets the Id of a [`Channel`]. - fn from(channel: &Channel) -> ChannelId { + fn from(channel: &Channel) -> Self { channel.id() } } @@ -1122,7 +1080,7 @@ pub struct MessagesIter<'a> { http: &'a Http, #[cfg(feature = "cache")] cache: Option<&'a Arc>, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, buffer: Vec, before: Option, tried_fetch: bool, @@ -1130,7 +1088,7 @@ pub struct MessagesIter<'a> { #[cfg(feature = "model")] impl<'a> MessagesIter<'a> { - fn new(cache_http: &'a impl CacheHttp, channel_id: ChannelId) -> MessagesIter<'a> { + fn new(cache_http: &'a impl CacheHttp, channel_id: ChannelOrThreadId) -> MessagesIter<'a> { MessagesIter { http: cache_http.http(), #[cfg(feature = "cache")] @@ -1219,7 +1177,7 @@ impl<'a> MessagesIter<'a> { /// ``` pub fn stream( cache_http: &'a impl CacheHttp, - channel_id: ChannelId, + channel_id: ChannelOrThreadId, ) -> impl Stream> + 'a { let init_state = MessagesIter::new(cache_http, channel_id); diff --git a/src/model/channel/followed_channel.rs b/src/model/channel/followed_channel.rs new file mode 100644 index 00000000000..c5a0b520268 --- /dev/null +++ b/src/model/channel/followed_channel.rs @@ -0,0 +1,13 @@ +use super::{ChannelId, WebhookId}; + +/// A container for the IDs returned by following a news channel. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/channel#followed-channel-object). +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct FollowedChannel { + /// The source news channel + pub channel_id: ChannelId, + /// The created webhook ID in the target channel + pub webhook_id: WebhookId, +} diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index fd7d5dfce5e..0d8f44c1e51 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -1,6 +1,6 @@ use std::fmt; -use nonmax::{NonMaxU16, NonMaxU32, NonMaxU8}; +use nonmax::{NonMaxU16, NonMaxU32}; #[cfg(feature = "model")] use crate::builder::{ @@ -9,7 +9,6 @@ use crate::builder::{ CreateWebhook, EditChannel, EditStageInstance, - EditThread, EditVoiceState, }; #[cfg(feature = "cache")] @@ -18,6 +17,33 @@ use crate::cache::{self, Cache}; use crate::http::Http; use crate::model::prelude::*; +/// Represents the shared fields between [`GuildChannel`] and [`GuildThread`]. +/// +/// [Discord docs](https://discord.com/developers/docs/topics/threads#thread-fields) +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[non_exhaustive] +pub struct BaseGuildChannel { + /// The Id of the guild the channel is located in. + #[serde(default)] + pub guild_id: GuildId, + /// The type of the channel. + #[serde(rename = "type")] + pub kind: ChannelType, + /// The name of the channel. (1-100 characters) + pub name: FixedString, + /// The Id of the last message sent in the channel. + pub last_message_id: Option, + /// The timestamp of the time a pin was most recently made. + pub last_pin_timestamp: Option, + /// A rate limit that applies per user and excludes bots. + /// + /// **Note**: This is only available for text channels excluding news channels. + #[doc(alias = "slowmode")] + #[serde(default)] + pub rate_limit_per_user: Option, +} + /// Represents a guild's text, news, or voice channel. /// /// Some methods are available only for voice channels and some are only available for text @@ -29,40 +55,19 @@ use crate::model::prelude::*; #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[non_exhaustive] pub struct GuildChannel { - /// The unique Id of the channel. + /// The shared fields between [`GuildChannel`] and [`GuildThread`]. + pub base: BaseGuildChannel, + /// The unique ID of the channel. pub id: ChannelId, + /// The Id of the parent category for a channel. + /// + /// **Note**: This is only available for channels in a category. + // Technically shared, but for different purposes. + pub parent_id: Option, /// The bitrate of the channel. /// /// **Note**: This is only available for voice and stage channels. pub bitrate: Option, - /// The Id of the parent category for a channel, or of the parent text channel for a thread. - /// - /// **Note**: This is only available for channels in a category and thread channels. - pub parent_id: Option, - /// The Id of the guild the channel is located in. - /// - /// The original voice channel has an Id equal to the guild's Id, incremented by one. - /// - /// [`id`]: GuildChannel::id - #[serde(default)] - pub guild_id: GuildId, - /// The type of the channel. - #[serde(rename = "type")] - pub kind: ChannelType, - /// The Id of the user who created this channel - /// - /// **Note**: This is only available for threads and forum posts - pub owner_id: Option, - /// The Id of the last message sent in the channel. - /// - /// **Note**: This is only available for text channels. - pub last_message_id: Option, - /// The timestamp of the time a pin was most recently made. - /// - /// **Note**: This is only available for text channels. - pub last_pin_timestamp: Option, - /// The name of the channel. (1-100 characters) - pub name: FixedString, /// Permission overwrites for [`Member`]s and for [`Role`]s. #[serde(default)] pub permission_overwrites: FixedArray, @@ -83,12 +88,6 @@ pub struct GuildChannel { // This field can or can not be present sometimes, but if it isn't default to `false`. #[serde(default)] pub nsfw: bool, - /// A rate limit that applies per user and excludes bots. - /// - /// **Note**: This is only available for text channels excluding news channels. - #[doc(alias = "slowmode")] - #[serde(default)] - pub rate_limit_per_user: Option, /// The region override. /// /// **Note**: This is only available for voice and stage channels. [`None`] for voice and stage @@ -96,21 +95,6 @@ pub struct GuildChannel { pub rtc_region: Option>, /// The video quality mode for a voice channel. pub video_quality_mode: Option, - /// An approximate count of messages in the thread. - /// - /// **Note**: This is only available on thread channels. - pub message_count: Option, - /// An approximate count of users in a thread, stops counting at 50. - /// - /// **Note**: This is only available on thread channels. - pub member_count: Option, - /// The thread metadata. - /// - /// **Note**: This is only available on thread channels. - pub thread_metadata: Option, - /// Thread member object for the current user, if they have joined the thread, only included on - /// certain API endpoints. - pub member: Option, /// Default duration for newly created threads, in minutes, to automatically archive the thread /// after recent activity. pub default_auto_archive_duration: Option, @@ -123,19 +107,11 @@ pub struct GuildChannel { /// **Note**: This is only available in forum channels. #[serde(default)] pub flags: ChannelFlags, - /// The number of messages ever sent in a thread, it's similar to `message_count` on message - /// creation, but will not decrement the number when a message is deleted. - pub total_message_sent: Option, /// The set of available tags. /// /// **Note**: This is only available in forum channels. #[serde(default)] pub available_tags: FixedArray, - /// The set of applied tags. - /// - /// **Note**: This is only available in a thread in a forum. - #[serde(default)] - pub applied_tags: FixedArray, /// The emoji to show in the add reaction button /// /// **Note**: This is only available in a forum. @@ -184,7 +160,7 @@ impl GuildChannel { #[must_use] pub fn is_text_based(&self) -> bool { matches!( - self.kind, + self.base.kind, ChannelType::Text | ChannelType::News | ChannelType::Voice @@ -205,7 +181,7 @@ impl GuildChannel { /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result { - let channel = self.id.delete(http, reason).await?; + let channel = self.id.widen().delete(http, reason).await?; channel.guild().ok_or(Error::Model(ModelError::InvalidChannelType)) } @@ -244,16 +220,6 @@ impl GuildChannel { Ok(()) } - /// Edits a thread. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission. - pub async fn edit_thread(&mut self, http: &Http, builder: EditThread<'_>) -> Result<()> { - *self = self.id.edit_thread(http, builder).await?; - Ok(()) - } - /// Edits the voice state of a given user in a stage channel. /// /// **Note**: Requires the [Request to Speak] permission. Also requires the [Mute Members] @@ -301,11 +267,11 @@ impl GuildChannel { user_id: UserId, builder: EditVoiceState, ) -> Result<()> { - if self.kind != ChannelType::Stage { + if self.base.kind != ChannelType::Stage { return Err(Error::from(ModelError::InvalidChannelType)); } - builder.execute(http, self.guild_id, self.id, Some(user_id)).await + builder.execute(http, self.base.guild_id, self.id, Some(user_id)).await } /// Edits the current user's voice state in a stage channel. @@ -355,13 +321,13 @@ impl GuildChannel { /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK /// [Mute Members]: Permissions::MUTE_MEMBERS pub async fn edit_own_voice_state(&self, http: &Http, builder: EditVoiceState) -> Result<()> { - builder.execute(http, self.guild_id, self.id, None).await + builder.execute(http, self.base.guild_id, self.id, None).await } /// Attempts to find this channel's guild in the Cache. #[cfg(feature = "cache")] pub fn guild<'a>(&self, cache: &'a Cache) -> Option> { - cache.guild(self.guild_id) + cache.guild(self.base.guild_id) } /// Calculates the permissions of a member. @@ -419,7 +385,7 @@ impl GuildChannel { /// See [`CreateMessage::execute`] for a list of possible errors, and their corresponding /// reasons. pub async fn send_message(&self, http: &Http, builder: CreateMessage<'_>) -> Result { - builder.execute(http, self.id, Some(self.guild_id)).await + builder.execute(http, self.id.widen(), Some(self.base.guild_id)).await } /// Retrieves [`Member`]s from the current channel. @@ -435,9 +401,9 @@ impl GuildChannel { /// [`ModelError::InvalidChannelType`]. #[cfg(feature = "cache")] pub fn members(&self, cache: &Cache) -> Result> { - let guild = cache.guild(self.guild_id).ok_or(ModelError::GuildNotFound)?; + let guild = cache.guild(self.base.guild_id).ok_or(ModelError::GuildNotFound)?; - match self.kind { + match self.base.kind { ChannelType::Voice | ChannelType::Stage => Ok(guild .voice_states .iter() @@ -477,7 +443,7 @@ impl GuildChannel { pub async fn create_webhook(&self, http: &Http, builder: CreateWebhook<'_>) -> Result { // forum channels are not text-based, but webhooks can be created in them // and used to send messages in their posts - if !self.is_text_based() && self.kind != ChannelType::Forum { + if !self.is_text_based() && self.base.kind != ChannelType::Forum { return Err(Error::Model(ModelError::InvalidChannelType)); } @@ -492,7 +458,7 @@ impl GuildChannel { /// /// Returns [`Error::Http`] if there is no stage instance currently. pub async fn get_stage_instance(&self, http: &Http) -> Result { - if self.kind != ChannelType::Stage { + if self.base.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } @@ -511,7 +477,7 @@ impl GuildChannel { http: &Http, builder: CreateStageInstance<'_>, ) -> Result { - if self.kind != ChannelType::Stage { + if self.base.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } @@ -531,7 +497,7 @@ impl GuildChannel { http: &Http, builder: EditStageInstance<'_>, ) -> Result { - if self.kind != ChannelType::Stage { + if self.base.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } @@ -546,7 +512,7 @@ impl GuildChannel { /// /// Returns [`Error::Http`] if there is no stage instance currently. pub async fn delete_stage_instance(&self, http: &Http, reason: Option<&str>) -> Result<()> { - if self.kind != ChannelType::Stage { + if self.base.kind != ChannelType::Stage { return Err(Error::Model(ModelError::InvalidChannelType)); } @@ -557,7 +523,7 @@ impl GuildChannel { impl fmt::Display for GuildChannel { /// Formats the channel, creating a mention of it. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.id.mention(), f) + fmt::Display::fmt(&self.mention(), f) } } @@ -566,22 +532,3 @@ impl ExtractKey for GuildChannel { &self.id } } - -/// A partial guild channel. -/// -/// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object), -/// [subset description](https://discord.com/developers/docs/topics/gateway#thread-delete) -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -pub struct PartialGuildChannel { - /// The channel Id. - pub id: ChannelId, - /// The channel guild Id. - pub guild_id: GuildId, - /// The channel category Id, or the parent text channel Id for a thread. - pub parent_id: ChannelId, - /// The channel type. - #[serde(rename = "type")] - pub kind: ChannelType, -} diff --git a/src/model/channel/interaction_channel.rs b/src/model/channel/interaction_channel.rs new file mode 100644 index 00000000000..acaf8ee9f46 --- /dev/null +++ b/src/model/channel/interaction_channel.rs @@ -0,0 +1,85 @@ +use serde::de::Error as _; + +use crate::model::prelude::*; +use crate::model::utils::deserialize_val; + +/// Represents the shared fields between a [`InteractionGuildChannel`] and a +/// [`InteractionGuildThread`]. +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct BaseInteractionChannel { + /// The channel name. + pub name: Option, + /// The channel type. + #[serde(rename = "type")] + pub kind: ChannelType, + /// The channel permissions. + pub permissions: Option, +} + +/// Represents a partial channel from an interaction. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object), +/// [subset specification](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct InteractionGuildChannel { + /// The shared fields between [`InteractionGuildChannel`] and [`InteractionGuildThread`]. + #[serde(flatten)] + pub base: BaseInteractionChannel, + /// The channel Id. + pub id: ChannelId, +} + +/// Represents a partial thread from an interaction. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object), +/// [subset specification](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct InteractionGuildThread { + /// The shared fields between [`InteractionGuildChannel`] and [`InteractionGuildThread`]. + #[serde(flatten)] + pub base: BaseInteractionChannel, + /// The thread Id. + pub id: ThreadId, + /// The thread metadata. + /// + /// **Note**: This is only available on thread channels. + pub thread_metadata: ThreadMetadata, + /// The parent text channel. + pub parent_id: ChannelId, +} + +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +#[non_exhaustive] +pub enum InteractionChannel { + Channel(InteractionGuildChannel), + Thread(InteractionGuildThread), +} + +impl ExtractKey for InteractionChannel { + fn extract_key(&self) -> &ChannelOrThreadId { + match self { + Self::Channel(channel) => ChannelOrThreadId::cast_from(&channel.id.0), + Self::Thread(thread) => ChannelOrThreadId::cast_from(&thread.id.0), + } + } +} + +impl<'de> serde::Deserialize<'de> for InteractionChannel { + fn deserialize>(deserializer: D) -> Result { + let map = JsonMap::deserialize(deserializer)?; + + match map.get("type").and_then(Value::as_u64) { + Some(10..=12) => deserialize_val(Value::from(map)).map(Self::Thread), + Some(_) => deserialize_val(Value::from(map)).map(Self::Channel), + _ => Err(D::Error::custom("expected type field to be an integer")), + } + } +} diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index dd59d988c5e..8e51d07e085 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -36,7 +36,7 @@ pub struct Message { /// The unique Id of the message. Can be used to calculate the creation date of the message. pub id: MessageId, /// The Id of the [`Channel`] that the message was sent to. - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, /// The user that sent the message. pub author: User, /// The content of the message. @@ -175,7 +175,7 @@ impl Message { } } - self.channel_id.crosspost(http, self.id).await + self.channel_id.expect_channel().crosspost(http, self.id).await } /// First attempts to find a [`Channel`] by its Id in the cache, upon failure requests it via @@ -557,24 +557,12 @@ impl Message { #[cfg(feature = "cache")] if let Some(cache) = cache_http.cache() { if let Some(guild) = cache.guild(self.guild_id?) { - let channel = guild.channels.get(&self.channel_id)?; - return if channel.thread_metadata.is_some() { - let thread_parent = guild.channels.get(&channel.parent_id?)?; - thread_parent.parent_id - } else { - channel.parent_id - }; + let channel = guild.channels.get(&self.channel_id.expect_channel())?; + return channel.parent_id; } } - let http = cache_http.http(); - let channel = http.get_channel(self.channel_id).await.ok()?.guild()?; - if channel.thread_metadata.is_some() { - let thread_parent = http.get_channel(channel.parent_id?).await.ok()?.guild()?; - thread_parent.parent_id - } else { - channel.parent_id - } + cache_http.http().get_channel(self.channel_id).await.ok()?.guild()?.parent_id } } @@ -785,7 +773,7 @@ pub struct MessageReference { /// ID of the originating message. pub message_id: Option, /// ID of the originating message's channel. - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, /// ID of the originating message's guild. pub guild_id: Option, /// When sending, whether to error if the referenced message doesn't exist instead of sending @@ -795,7 +783,7 @@ pub struct MessageReference { impl MessageReference { #[must_use] - pub fn new(kind: MessageReferenceKind, channel_id: ChannelId) -> Self { + pub fn new(kind: MessageReferenceKind, channel_id: ChannelOrThreadId) -> Self { Self { kind, channel_id, @@ -836,9 +824,9 @@ impl From<&Message> for MessageReference { } } -impl From<(ChannelId, MessageId)> for MessageReference { +impl From<(ChannelOrThreadId, MessageId)> for MessageReference { // TODO(next): Remove this - fn from(pair: (ChannelId, MessageId)) -> Self { + fn from(pair: (ChannelOrThreadId, MessageId)) -> Self { Self { kind: MessageReferenceKind::default(), message_id: Some(pair.1), @@ -913,7 +901,7 @@ impl MessageId { /// Returns a link referencing this message. When clicked, users will jump to the message. The /// link will be valid for messages in either private channels or guilds. #[must_use] - pub fn link(self, channel_id: ChannelId, guild_id: Option) -> String { + pub fn link(self, channel_id: ChannelOrThreadId, guild_id: Option) -> String { if let Some(guild_id) = guild_id { format!("https://discord.com/channels/{guild_id}/{channel_id}/{self}") } else { diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index 77f49ce45c6..e0490ea85e2 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -3,11 +3,13 @@ mod attachment; mod channel_id; mod embed; +mod followed_channel; mod guild_channel; +mod interaction_channel; mod message; -mod partial_channel; mod private_channel; mod reaction; +mod thread; use std::fmt; @@ -19,15 +21,16 @@ pub use self::attachment::*; #[cfg(feature = "model")] pub use self::channel_id::*; pub use self::embed::*; +pub use self::followed_channel::*; pub use self::guild_channel::*; +pub use self::interaction_channel::*; pub use self::message::*; -pub use self::partial_channel::*; pub use self::private_channel::*; pub use self::reaction::*; +pub use self::thread::*; #[cfg(feature = "model")] use crate::http::Http; use crate::model::prelude::*; -use crate::model::utils::is_false; /// A container for any channel. #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] @@ -37,6 +40,8 @@ use crate::model::utils::is_false; pub enum Channel { /// A channel within a [`Guild`]. Guild(GuildChannel), + /// A thread inside a [`Guild`]. + GuildThread(GuildThread), /// A private channel to another [`User`] (Direct Message). No other users may access the /// channel. Private(PrivateChannel), @@ -44,63 +49,29 @@ pub enum Channel { #[cfg(feature = "model")] impl Channel { - /// Converts from [`Channel`] to `Option`. - /// - /// Converts `self` into an `Option`, consuming `self`, and discarding a - /// [`PrivateChannel`] if any. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// # use serenity::model::channel::Channel; - /// # fn run(channel: Channel) { - /// match channel.guild() { - /// Some(guild_channel) => { - /// println!("It's a guild channel named {}!", guild_channel.name); - /// }, - /// None => { - /// println!("It's not in a guild!"); - /// }, - /// } - /// # } - /// ``` + /// If this is a guild channel, returns it. #[must_use] pub fn guild(self) -> Option { match self { - Self::Guild(lock) => Some(lock), + Self::Guild(channel) => Some(channel), _ => None, } } - /// Converts from [`Channel`] to `Option`. - /// - /// Converts `self` into an `Option`, consuming `self`, and discarding a - /// [`GuildChannel`], if any. - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```rust,no_run - /// # use serenity::model::channel::Channel; - /// # fn run(channel: Channel) { - /// # - /// match channel.private() { - /// Some(private) => { - /// println!("It's a private channel with {}!", &private.recipient); - /// }, - /// None => { - /// println!("It's not a private channel!"); - /// }, - /// } - /// # } - /// ``` + /// If this is a guild thread, returns it. + #[must_use] + pub fn thread(self) -> Option { + match self { + Self::GuildThread(thread) => Some(thread), + _ => None, + } + } + + /// If this is a private channel, returns it. #[must_use] pub fn private(self) -> Option { match self { - Self::Private(lock) => Some(lock), + Self::Private(channel) => Some(channel), _ => None, } } @@ -109,7 +80,7 @@ impl Channel { #[must_use] pub fn category(self) -> Option { match self { - Self::Guild(c) if c.kind == ChannelType::Category => Some(c), + Self::Guild(c) if c.base.kind == ChannelType::Category => Some(c), _ => None, } } @@ -124,6 +95,9 @@ impl Channel { Self::Guild(public_channel) => { public_channel.delete(http, reason).await?; }, + Self::GuildThread(thread) => { + thread.delete(http, reason).await?; + }, Self::Private(private_channel) => { private_channel.delete(http).await?; }, @@ -132,23 +106,13 @@ impl Channel { Ok(()) } - /// Retrieves the Id of the inner [`GuildChannel`], or [`PrivateChannel`]. + /// Retrieves the inner Id. #[must_use] - pub const fn id(&self) -> ChannelId { + pub fn id(&self) -> ChannelOrThreadId { match self { - Self::Guild(ch) => ch.id, - Self::Private(ch) => ch.id, - } - } - - /// Retrieves the position of the inner [`GuildChannel`]. - /// - /// In DMs (private channel) it will return None. - #[must_use] - pub const fn position(&self) -> Option { - match self { - Self::Guild(channel) => Some(channel.position), - Self::Private(_) => None, + Self::Guild(ch) => ch.id.widen(), + Self::GuildThread(ch) => ch.id.widen(), + Self::Private(ch) => ch.id.widen(), } } } @@ -170,7 +134,8 @@ impl<'de> Deserialize<'de> for Channel { let value = Value::from(map); match kind { - 0 | 2 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 => from_value(value).map(Channel::Guild), + 0 | 2 | 4 | 5 | 13 | 14 | 15 => from_value(value).map(Channel::Guild), + 10..=12 => from_value(value).map(Channel::GuildThread), 1 => from_value(value).map(Channel::Private), _ => return Err(DeError::custom("Unknown channel type")), } @@ -187,7 +152,8 @@ impl fmt::Display for Channel { /// click on. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Guild(ch) => fmt::Display::fmt(&ch.id.mention(), f), + Self::Guild(ch) => fmt::Display::fmt(&ch.id.widen().mention(), f), + Self::GuildThread(ch) => fmt::Display::fmt(&ch.id.widen().mention(), f), Self::Private(ch) => fmt::Display::fmt(&ch.recipient.name, f), } } @@ -374,7 +340,7 @@ enum_number! { /// See [`ThreadMetadata::auto_archive_duration`]. /// /// [Discord docs](https://discord.com/developers/docs/resources/channel#thread-metadata-object) - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] + #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[non_exhaustive] pub enum AutoArchiveDuration { @@ -408,35 +374,6 @@ pub struct StageInstance { pub guild_scheduled_event_id: Option, } -/// A thread data. -/// -/// [Discord docs](https://discord.com/developers/docs/resources/channel#thread-metadata-object). -#[bool_to_bitflags::bool_to_bitflags] -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] -#[non_exhaustive] -pub struct ThreadMetadata { - /// Whether the thread is archived. - pub archived: bool, - /// Duration in minutes to automatically archive the thread after recent activity. - pub auto_archive_duration: AutoArchiveDuration, - /// The last time the thread's archive status was last changed; used for calculating recent - /// activity. - pub archive_timestamp: Option, - /// When a thread is locked, only users with `MANAGE_THREADS` permission can unarchive it. - #[serde(default)] - pub locked: bool, - /// Timestamp when the thread was created. - /// - /// **Note**: only populated for threads created after 2022-01-09 - pub create_timestamp: Option, - /// Whether non-moderators can add other non-moderators to a thread. - /// - /// **Note**: Only available on private threads. - #[serde(default, skip_serializing_if = "is_false")] - pub invitable: bool, -} - /// A response to getting several threads channels. /// /// Discord docs: defined [multiple times](https://discord.com/developers/docs/topics/threads#enumerating-threads): diff --git a/src/model/channel/partial_channel.rs b/src/model/channel/partial_channel.rs deleted file mode 100644 index 16df1966d2e..00000000000 --- a/src/model/channel/partial_channel.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::model::prelude::*; - -/// A container for any partial channel. -/// -/// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object), -/// [subset specification](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure). -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -pub struct PartialChannel { - /// The channel Id. - pub id: ChannelId, - /// The channel name. - pub name: Option, - /// The channel type. - #[serde(rename = "type")] - pub kind: ChannelType, - /// The channel permissions. - pub permissions: Option, - /// The thread metadata. - /// - /// **Note**: This is only available on thread channels. - pub thread_metadata: Option, - /// The Id of the parent category for a channel, or of the parent text channel for a thread. - /// - /// **Note**: This is only available on thread channels. - pub parent_id: Option, -} - -impl ExtractKey for PartialChannel { - fn extract_key(&self) -> &ChannelId { - &self.id - } -} - -/// A container for the IDs returned by following a news channel. -/// -/// [Discord docs](https://discord.com/developers/docs/resources/channel#followed-channel-object). -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -pub struct FollowedChannel { - /// The source news channel - pub channel_id: ChannelId, - /// The created webhook ID in the target channel - pub webhook_id: WebhookId, -} diff --git a/src/model/channel/private_channel.rs b/src/model/channel/private_channel.rs index b722d77e14a..e281b72597f 100644 --- a/src/model/channel/private_channel.rs +++ b/src/model/channel/private_channel.rs @@ -36,7 +36,7 @@ impl PrivateChannel { /// closing a private channel on the client, which can be re-opened. #[expect(clippy::missing_errors_doc)] pub async fn delete(&self, http: &Http) -> Result { - let resp = self.id.delete(http, None).await?; + let resp = self.id.widen().delete(http, None).await?; resp.private().ok_or(Error::Model(ModelError::InvalidChannelType)) } } diff --git a/src/model/channel/reaction.rs b/src/model/channel/reaction.rs index 5ecd737768f..b3db671d221 100644 --- a/src/model/channel/reaction.rs +++ b/src/model/channel/reaction.rs @@ -32,7 +32,7 @@ pub struct Reaction { /// Set to [`None`] by [`Message::react`] when cache is not available. pub user_id: Option, /// The [`Channel`] of the associated [`Message`]. - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, /// The Id of the [`Message`] that was reacted to. pub message_id: MessageId, /// The optional Id of the [`Guild`] where the reaction was sent. diff --git a/src/model/channel/thread.rs b/src/model/channel/thread.rs new file mode 100644 index 00000000000..3eaf2dcb178 --- /dev/null +++ b/src/model/channel/thread.rs @@ -0,0 +1,273 @@ +use super::*; +use crate::builder::EditThread; +use crate::internal::prelude::*; +use crate::model::utils::is_false; + +impl From for ChannelOrThreadId { + fn from(val: ThreadId) -> Self { + Self::new(val.get()) + } +} + +impl From for ChannelOrThreadId { + fn from(val: ChannelId) -> Self { + Self::new(val.get()) + } +} + +impl ChannelOrThreadId { + #[must_use] + pub fn split(self) -> (ChannelId, ThreadId) { + (self.expect_channel(), self.expect_thread()) + } + + #[must_use] + pub fn expect_channel(self) -> ChannelId { + ChannelId::new(self.get()) + } + + #[must_use] + pub fn expect_thread(self) -> ThreadId { + ThreadId::new(self.get()) + } +} + +impl ThreadId { + #[must_use] + pub fn widen(self) -> ChannelOrThreadId { + self.into() + } + + /// Gets the thread members, if this channel is a thread. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the channel is not a thread channel + pub async fn get_thread_members(self, http: &Http) -> Result> { + http.get_channel_thread_members(self).await + } + + /// Joins the thread, if this channel is a thread. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the channel is not a thread channel + pub async fn join_thread(self, http: &Http) -> Result<()> { + http.join_thread_channel(self).await + } + + /// Edits the thread. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + pub async fn edit(self, http: &Http, builder: EditThread<'_>) -> Result { + builder.execute(http, self).await + } + + /// Leaves the thread, if this channel is a thread. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the channel is not a thread channel + pub async fn leave_thread(self, http: &Http) -> Result<()> { + http.leave_thread_channel(self).await + } + + /// Adds a thread member, if this channel is a thread. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the channel is not a thread channel + pub async fn add_thread_member(self, http: &Http, user_id: UserId) -> Result<()> { + http.add_thread_channel_member(self, user_id).await + } + + /// Removes a thread member, if this channel is a thread. + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the channel is not a thread channel + pub async fn remove_thread_member(self, http: &Http, user_id: UserId) -> Result<()> { + http.remove_thread_channel_member(self, user_id).await + } + + /// Gets a thread member, if this channel is a thread. + /// + /// `with_member` controls if ThreadMember::member should be `Some` + /// + /// # Errors + /// + /// It may return an [`Error::Http`] if the channel is not a thread channel + pub async fn get_thread_member( + self, + http: &Http, + user_id: UserId, + with_member: bool, + ) -> Result { + http.get_thread_channel_member(self, user_id, with_member).await + } +} + +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[non_exhaustive] +pub struct GuildThread { + /// The shared fields between [`GuildChannel`] and [`GuildThread`]. + #[serde(flatten)] + pub base: BaseGuildChannel, + /// The Id of the thread. + pub id: ThreadId, + /// The Id of the user who created this thread + pub owner_id: UserId, + /// An approximate count of users in a thread, stops counting at 50. + pub member_count: u8, + /// An approximate count of messages in the thread. + pub message_count: u32, + /// The thread metadata. + pub thread_metadata: ThreadMetadata, + /// Thread member object for the current user, if they have joined the thread. + /// + /// This is only included on certain API endpoints. + pub member: Option, + /// The number of messages ever sent in a thread, it's similar to `message_count` on message + /// creation, but will not decrement the number when a message is deleted. + pub total_message_sent: u32, + /// The set of applied tags. + /// + /// **Note**: This is only available in a thread in a forum. + #[serde(default)] + pub applied_tags: FixedArray, +} + +impl GuildThread { + /// Edits the thread. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + pub async fn edit(&mut self, http: &Http, builder: EditThread<'_>) -> Result<()> { + *self = self.id.edit(http, builder).await?; + Ok(()) + } + + /// Deletes this thread, returning the thread on a successful deletion. + /// + /// **Note**: Requires the [Manage Threads] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission. + /// + /// [Manage Threads]: Permissions::MANAGE_THREADS + pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result { + let channel = self.id.widen().delete(http, reason).await?; + channel.thread().ok_or(Error::Model(ModelError::InvalidChannelType)) + } +} + +impl ExtractKey for GuildThread { + fn extract_key(&self) -> &ThreadId { + &self.id + } +} + +/// A thread data. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/channel#thread-metadata-object). +#[bool_to_bitflags::bool_to_bitflags] +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Copy, Debug, Default, serde::Deserialize, serde::Serialize)] +#[non_exhaustive] +pub struct ThreadMetadata { + /// Whether the thread is archived. + pub archived: bool, + /// Duration in minutes to automatically archive the thread after recent activity. + pub auto_archive_duration: AutoArchiveDuration, + /// The last time the thread's archive status was last changed; used for calculating recent + /// activity. + pub archive_timestamp: Option, + /// When a thread is locked, only users with `MANAGE_THREADS` permission can unarchive it. + #[serde(default)] + pub locked: bool, + /// Timestamp when the thread was created. + /// + /// **Note**: only populated for threads created after 2022-01-09 + pub create_timestamp: Option, + /// Whether non-moderators can add other non-moderators to a thread. + /// + /// **Note**: Only available on private threads. + #[serde(default, skip_serializing_if = "is_false")] + pub invitable: bool, +} + +/// A partial guild thread. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/channel#channel-object), +/// [subset description](https://discord.com/developers/docs/topics/gateway#thread-delete) +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct PartialGuildThread { + /// The thread Id. + pub id: ThreadId, + /// The thread guild Id. + pub guild_id: GuildId, + /// The parent text channel Id. + pub parent_id: ChannelId, + /// The channel type. + #[serde(rename = "type")] + pub kind: ChannelType, +} + +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct PartialThreadMember { + /// The time the current user last joined the thread. + pub join_timestamp: Timestamp, + /// Any user-thread settings, currently only used for notifications + pub flags: ThreadMemberFlags, +} + +/// A model representing a user in a Guild Thread. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/channel#thread-member-object), +/// [extra fields](https://discord.com/developers/docs/topics/gateway-events#thread-member-update-thread-member-update-event-extra-fields). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct ThreadMember { + #[serde(flatten)] + pub inner: PartialThreadMember, + /// The id of the thread. + pub id: ChannelId, + /// The id of the user. + pub user_id: UserId, + /// Additional information about the user. + /// + /// This field is only present when `with_member` is set to `true` when calling + /// List Thread Members or Get Thread Member, or inside [`ThreadMembersUpdateEvent`]. + pub member: Option, + /// ID of the guild. + /// + /// Always present in [`ThreadMemberUpdateEvent`], otherwise `None`. + pub guild_id: Option, + // According to https://discord.com/developers/docs/topics/gateway-events#thread-members-update, + // > the thread member objects will also include the guild member and nullable presence objects + // > for each added thread member + // Which implies that ThreadMember has a presence field. But https://discord.com/developers/docs/resources/channel#thread-member-object + // says that's not true. I'm not adding the presence field here for now +} + +bitflags! { + /// Describes extra features of the message. + /// + /// Discord docs: flags field on [Thread Member](https://discord.com/developers/docs/resources/channel#thread-member-object). + #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] + #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)] + pub struct ThreadMemberFlags: u64 { + // Not documented. + const NOTIFICATIONS = 1 << 0; + } +} diff --git a/src/model/event.rs b/src/model/event.rs index 9d6157031cc..49220ea0a08 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -109,7 +109,7 @@ pub struct ChannelDeleteEvent { #[non_exhaustive] pub struct ChannelPinsUpdateEvent { pub guild_id: Option, - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, pub last_pin_timestamp: Option, } @@ -173,7 +173,8 @@ pub struct GuildCreateEvent { impl<'de> Deserialize<'de> for GuildCreateEvent { fn deserialize>(deserializer: D) -> StdResult { let mut guild: Guild = Guild::deserialize(deserializer)?; - lending_for_each!(guild.channels.iter_mut(), |x| x.guild_id = guild.id); + lending_for_each!(guild.channels.iter_mut(), |x| x.base.guild_id = guild.id); + lending_for_each!(guild.threads.iter_mut(), |x| x.base.guild_id = guild.id); lending_for_each!(guild.members.iter_mut(), |x| x.guild_id = guild.id); lending_for_each!(guild.roles.iter_mut(), |x| x.guild_id = guild.id); Ok(Self { @@ -452,7 +453,7 @@ pub struct MessageCreateEvent { #[non_exhaustive] pub struct MessageDeleteBulkEvent { pub guild_id: Option, - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, pub ids: FixedArray, } @@ -464,7 +465,7 @@ pub struct MessageDeleteBulkEvent { #[non_exhaustive] pub struct MessageDeleteEvent { pub guild_id: Option, - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, #[serde(rename = "id")] pub message_id: MessageId, } @@ -492,7 +493,7 @@ where #[non_exhaustive] pub struct MessageUpdateEvent { pub id: MessageId, - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, pub author: Option, pub content: Option>, pub timestamp: Option, @@ -664,7 +665,7 @@ pub struct ReactionRemoveEvent { #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct ReactionRemoveAllEvent { - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, pub message_id: MessageId, pub guild_id: Option, } @@ -710,7 +711,7 @@ pub struct ResumedEvent {} #[non_exhaustive] pub struct TypingStartEvent { /// ID of the channel. - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, /// ID of the guild. pub guild_id: Option, /// ID of the user. @@ -866,7 +867,7 @@ pub struct StageInstanceDeleteEvent { #[serde(transparent)] #[non_exhaustive] pub struct ThreadCreateEvent { - pub thread: GuildChannel, + pub thread: GuildThread, } /// Requires [`GatewayIntents::GUILDS`]. @@ -877,7 +878,7 @@ pub struct ThreadCreateEvent { #[serde(transparent)] #[non_exhaustive] pub struct ThreadUpdateEvent { - pub thread: GuildChannel, + pub thread: GuildThread, } /// Requires [`GatewayIntents::GUILDS`]. @@ -888,7 +889,7 @@ pub struct ThreadUpdateEvent { #[serde(transparent)] #[non_exhaustive] pub struct ThreadDeleteEvent { - pub thread: PartialGuildChannel, + pub thread: PartialGuildThread, } /// Requires [`GatewayIntents::GUILDS`]. @@ -1047,7 +1048,7 @@ pub struct EntitlementDeleteEvent { #[non_exhaustive] pub struct MessagePollVoteAddEvent { pub user_id: UserId, - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, pub message_id: MessageId, pub guild_id: Option, pub answer_id: AnswerId, @@ -1061,7 +1062,7 @@ pub struct MessagePollVoteAddEvent { #[non_exhaustive] pub struct MessagePollVoteRemoveEvent { pub user_id: UserId, - pub channel_id: ChannelId, + pub channel_id: ChannelOrThreadId, pub message_id: MessageId, pub guild_id: Option, pub answer_id: AnswerId, diff --git a/src/model/guild/audit_log/mod.rs b/src/model/guild/audit_log/mod.rs index 262f519ec21..68db3ed9fff 100644 --- a/src/model/guild/audit_log/mod.rs +++ b/src/model/guild/audit_log/mod.rs @@ -411,7 +411,7 @@ pub struct Options { pub members_removed: Option, /// Channel in which the messages were deleted #[serde(default)] - pub channel_id: Option, + pub channel_id: Option, /// Number of deleted messages. #[serde(default, with = "optional_string")] pub count: Option, diff --git a/src/model/guild/automod.rs b/src/model/guild/automod.rs index fd4e8405948..13e91c17d2a 100644 --- a/src/model/guild/automod.rs +++ b/src/model/guild/automod.rs @@ -333,7 +333,7 @@ pub enum Action { custom_message: Option>, }, /// Logs user content to a specified channel. - Alert(ChannelId), + Alert(ChannelOrThreadId), /// Timeout user for a specified duration. /// /// Maximum of 2419200 seconds (4 weeks). @@ -367,7 +367,7 @@ pub struct ActionExecution { /// ID of the user which generated the content which triggered the rule. pub user_id: UserId, /// ID of the channel in which user content was posted. - pub channel_id: Option, + pub channel_id: Option, /// ID of any user message which content belongs to. /// /// Will be `None` if message was blocked by automod or content was not part of any message. @@ -396,7 +396,7 @@ pub struct ActionExecution { #[derive(Default, Deserialize, Serialize)] struct RawActionMetadata { #[serde(skip_serializing_if = "Option::is_none")] - channel_id: Option, + channel_id: Option, #[serde(skip_serializing_if = "Option::is_none")] duration_seconds: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -579,7 +579,7 @@ mod tests { ); assert_json( - &Action::Alert(ChannelId::new(123)), + &Action::Alert(ChannelOrThreadId::new(123)), json!({"type": 2, "metadata": {"channel_id": "123"}}), ); diff --git a/src/model/guild/member.rs b/src/model/guild/member.rs index d297749849c..d8c8fd5ab19 100644 --- a/src/model/guild/member.rs +++ b/src/model/guild/member.rs @@ -175,7 +175,7 @@ impl Member { let member = guild.members.get(&self.user.id)?; for channel in &guild.channels { - if channel.kind != ChannelType::Category + if channel.base.kind != ChannelType::Category && guild.user_permissions_in(channel, member).view_channel() { return Some(channel.clone()); @@ -546,55 +546,3 @@ impl From for PartialMember { partial } } - -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -pub struct PartialThreadMember { - /// The time the current user last joined the thread. - pub join_timestamp: Timestamp, - /// Any user-thread settings, currently only used for notifications - pub flags: ThreadMemberFlags, -} - -/// A model representing a user in a Guild Thread. -/// -/// [Discord docs](https://discord.com/developers/docs/resources/channel#thread-member-object), -/// [extra fields](https://discord.com/developers/docs/topics/gateway-events#thread-member-update-thread-member-update-event-extra-fields). -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -pub struct ThreadMember { - #[serde(flatten)] - pub inner: PartialThreadMember, - /// The id of the thread. - pub id: ChannelId, - /// The id of the user. - pub user_id: UserId, - /// Additional information about the user. - /// - /// This field is only present when `with_member` is set to `true` when calling - /// List Thread Members or Get Thread Member, or inside [`ThreadMembersUpdateEvent`]. - pub member: Option, - /// ID of the guild. - /// - /// Always present in [`ThreadMemberUpdateEvent`], otherwise `None`. - pub guild_id: Option, - // According to https://discord.com/developers/docs/topics/gateway-events#thread-members-update, - // > the thread member objects will also include the guild member and nullable presence objects - // > for each added thread member - // Which implies that ThreadMember has a presence field. But https://discord.com/developers/docs/resources/channel#thread-member-object - // says that's not true. I'm not adding the presence field here for now -} - -bitflags! { - /// Describes extra features of the message. - /// - /// Discord docs: flags field on [Thread Member](https://discord.com/developers/docs/resources/channel#thread-member-object). - #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] - #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)] - pub struct ThreadMemberFlags: u64 { - // Not documented. - const NOTIFICATIONS = 1 << 0; - } -} diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index f853a1e9dcb..24c418157b8 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -245,7 +245,7 @@ pub struct Guild { /// from or connect to them). pub channels: ExtractMap, /// All active threads in this guild that current user has permission to view. - pub threads: FixedArray, + pub threads: ExtractMap, /// A mapping of [`User`]s' Ids to their current presences. /// /// **Note**: This will be empty unless the "guild presences" privileged intent is enabled. @@ -265,7 +265,7 @@ impl Guild { pub fn default_channel(&self, uid: UserId) -> Option<&GuildChannel> { let member = self.members.get(&uid)?; self.channels.iter().find(|&channel| { - channel.kind != ChannelType::Category + channel.base.kind != ChannelType::Category && self.user_permissions_in(channel, member).view_channel() }) } @@ -277,7 +277,7 @@ impl Guild { #[must_use] pub fn default_channel_guaranteed(&self) -> Option<&GuildChannel> { self.channels.iter().find(|&channel| { - channel.kind != ChannelType::Category + channel.base.kind != ChannelType::Category && self .members .iter() diff --git a/src/model/id.rs b/src/model/id.rs index 2bcca2d8818..9d3646c869f 100644 --- a/src/model/id.rs +++ b/src/model/id.rs @@ -1,4 +1,5 @@ //! A collection of newtypes defining type-strong IDs. +#![expect(clippy::unsafe_derive_deserialize)] // This is from `ref_cast` use std::fmt; @@ -44,8 +45,10 @@ macro_rules! id_u64 { ($($name:ident: $doc:literal;)*) => { $( #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] + #[derive(ref_cast::RefCastCustom)] + #[repr(transparent)] #[doc = $doc] - pub struct $name(InnerId); + pub struct $name(pub(crate) InnerId); impl $name { #[doc = concat!("Creates a new ", stringify!($name), " from a u64.")] @@ -60,6 +63,10 @@ macro_rules! id_u64 { } } + #[ref_cast::ref_cast_custom] + #[allow(unused, clippy::allow_attributes, reason = "Most IDs don't need casting like this")] + pub(crate) const fn cast_from(inner: &InnerId) -> &Self; + /// Retrieves the inner `id` as a [`u64`]. #[must_use] pub const fn get(self) -> u64 { @@ -179,6 +186,7 @@ id_u64! { AttachmentId: "An identifier for an attachment."; ApplicationId: "An identifier for an Application."; ChannelId: "An identifier for a Channel"; + ChannelOrThreadId: "An identifier for a GuildChannel or a GuildThread."; EmojiId: "An identifier for an Emoji"; GenericId: "An identifier for an unspecific entity."; GuildId: "An identifier for a Guild"; @@ -190,6 +198,7 @@ id_u64! { StickerPackId: "An identifier for a sticker pack."; StickerPackBannerId: "An identifier for a sticker pack banner."; SkuId: "An identifier for a SKU."; + ThreadId: "An identifier for a GuildThread."; UserId: "An identifier for a User"; WebhookId: "An identifier for a [`Webhook`]"; AuditLogEntryId: "An identifier for an audit log entry."; diff --git a/src/model/mention.rs b/src/model/mention.rs index 3e2027db1d0..32f81ba3fbd 100644 --- a/src/model/mention.rs +++ b/src/model/mention.rs @@ -85,7 +85,7 @@ pub trait Mentionable { /// ``` #[derive(Clone, Copy, Debug)] pub enum Mention { - Channel(ChannelId), + Channel(ChannelOrThreadId), Role(RoleId), User(UserId), } @@ -101,7 +101,7 @@ macro_rules! mention { } mention!(value: - ChannelId, Mention::Channel(value); + ChannelOrThreadId, Mention::Channel(value); RoleId, Mention::Role(value); UserId, Mention::User(value); ); @@ -186,13 +186,16 @@ macro_rules! mentionable { #[cfg(feature = "model")] mentionable!(value: Channel, value.id()); -mentionable!(value: GuildChannel, value.id); -mentionable!(value: PrivateChannel, value.id); +mentionable!(value: GuildChannel, value.id.widen()); +mentionable!(value: GuildThread, value.id.widen()); +mentionable!(value: PrivateChannel, value.id.widen()); mentionable!(value: Member, value.user.id); mentionable!(value: CurrentUser, value.id); mentionable!(value: User, value.id); mentionable!(value: Role, value.id); +mentionable!(value: ChannelId, value.widen()); + #[cfg(feature = "utils")] #[cfg(test)] mod test { diff --git a/src/model/user.rs b/src/model/user.rs index ef7f989afe7..6be3b47fc41 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -620,7 +620,12 @@ impl UserId { cache_http: impl CacheHttp, builder: CreateMessage<'_>, ) -> Result { - self.create_dm_channel(&cache_http).await?.id.send_message(cache_http.http(), builder).await + self.create_dm_channel(&cache_http) + .await? + .id + .widen() + .send_message(cache_http.http(), builder) + .await } /// This is an alias of [`Self::direct_message`]. diff --git a/src/model/webhook.rs b/src/model/webhook.rs index 22ed97d5efe..8e2af7e463f 100644 --- a/src/model/webhook.rs +++ b/src/model/webhook.rs @@ -349,7 +349,7 @@ impl Webhook { pub async fn get_message( &self, http: &Http, - thread_id: Option, + thread_id: Option, message_id: MessageId, ) -> Result { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret(); @@ -390,7 +390,7 @@ impl Webhook { pub async fn delete_message( &self, http: &Http, - thread_id: Option, + thread_id: Option, message_id: MessageId, ) -> Result<()> { let token = self.token.as_ref().ok_or(ModelError::NoTokenSet)?.expose_secret(); diff --git a/src/utils/argument_convert/channel.rs b/src/utils/argument_convert/channel.rs index e94879e5e4c..a2f02bafefd 100644 --- a/src/utils/argument_convert/channel.rs +++ b/src/utils/argument_convert/channel.rs @@ -35,7 +35,8 @@ impl fmt::Display for ChannelParseError { fn channel_belongs_to_guild(channel: &Channel, guild: GuildId) -> bool { match channel { - Channel::Guild(channel) => channel.guild_id == guild, + Channel::GuildThread(channel) => channel.base.guild_id == guild, + Channel::Guild(channel) => channel.base.guild_id == guild, Channel::Private(_channel) => false, } } @@ -54,7 +55,7 @@ async fn lookup_channel_global( #[cfg(feature = "cache")] if let Some(cache) = ctx.cache() { if let Some(guild) = cache.guild(guild_id) { - let channel = guild.channels.iter().find(|c| c.name.eq_ignore_ascii_case(s)); + let channel = guild.channels.iter().find(|c| c.base.name.eq_ignore_ascii_case(s)); if let Some(channel) = channel { return Ok(Channel::Guild(channel.clone())); } @@ -64,7 +65,7 @@ async fn lookup_channel_global( } let channels = ctx.http().get_channels(guild_id).await.map_err(ChannelParseError::Http)?; - if let Some(channel) = channels.into_iter().find(|c| c.name.eq_ignore_ascii_case(s)) { + if let Some(channel) = channels.into_iter().find(|c| c.base.name.eq_ignore_ascii_case(s)) { Ok(Channel::Guild(channel)) } else { Err(ChannelParseError::NotFoundOrMalformed) @@ -88,7 +89,7 @@ impl ArgumentConvert for Channel { async fn convert( ctx: impl CacheHttp, guild_id: Option, - _channel_id: Option, + _channel_id: Option, s: &str, ) -> Result { let channel = lookup_channel_global(&ctx, guild_id, s).await?; @@ -148,7 +149,7 @@ impl ArgumentConvert for GuildChannel { async fn convert( ctx: impl CacheHttp, guild_id: Option, - channel_id: Option, + channel_id: Option, s: &str, ) -> Result { match Channel::convert(&ctx, guild_id, channel_id, s).await { diff --git a/src/utils/argument_convert/emoji.rs b/src/utils/argument_convert/emoji.rs index 9a5d986ddb5..15a0e85db1d 100644 --- a/src/utils/argument_convert/emoji.rs +++ b/src/utils/argument_convert/emoji.rs @@ -44,7 +44,7 @@ impl ArgumentConvert for Emoji { async fn convert( ctx: impl CacheHttp, guild_id: Option, - _channel_id: Option, + _channel_id: Option, s: &str, ) -> Result { // Get Guild or PartialGuild diff --git a/src/utils/argument_convert/guild.rs b/src/utils/argument_convert/guild.rs index 77888f80eec..3afa59bda04 100644 --- a/src/utils/argument_convert/guild.rs +++ b/src/utils/argument_convert/guild.rs @@ -36,7 +36,7 @@ impl ArgumentConvert for Guild { async fn convert( ctx: impl CacheHttp, _guild_id: Option, - _channel_id: Option, + _channel_id: Option, s: &str, ) -> Result { let guilds = &ctx.cache().ok_or(GuildParseError::NoCache)?.guilds; diff --git a/src/utils/argument_convert/member.rs b/src/utils/argument_convert/member.rs index e3cea497ebd..ac09dd95104 100644 --- a/src/utils/argument_convert/member.rs +++ b/src/utils/argument_convert/member.rs @@ -46,7 +46,7 @@ impl ArgumentConvert for Member { async fn convert( ctx: impl CacheHttp, guild_id: Option, - _channel_id: Option, + _channel_id: Option, s: &str, ) -> Result { let guild_id = guild_id.ok_or(MemberParseError::OutsideGuild)?; diff --git a/src/utils/argument_convert/message.rs b/src/utils/argument_convert/message.rs index 6023402a39d..a718922a34a 100644 --- a/src/utils/argument_convert/message.rs +++ b/src/utils/argument_convert/message.rs @@ -53,7 +53,7 @@ impl ArgumentConvert for Message { async fn convert( ctx: impl CacheHttp, _guild_id: Option, - channel_id: Option, + channel_id: Option, s: &str, ) -> Result { let extract_from_message_id = || Some((channel_id?, s.parse().ok()?)); diff --git a/src/utils/argument_convert/mod.rs b/src/utils/argument_convert/mod.rs index bbe118a34d7..f9b93e49354 100644 --- a/src/utils/argument_convert/mod.rs +++ b/src/utils/argument_convert/mod.rs @@ -43,7 +43,7 @@ pub trait ArgumentConvert: Sized { async fn convert( ctx: impl CacheHttp, guild_id: Option, - channel_id: Option, + channel_id: Option, s: &str, ) -> Result; } @@ -55,7 +55,7 @@ impl ArgumentConvert for T { async fn convert( _: impl CacheHttp, _: Option, - _: Option, + _: Option, s: &str, ) -> Result { T::from_str(s) @@ -84,7 +84,7 @@ impl ArgumentConvert for T { /// ); /// ``` #[must_use] -pub fn parse_message_id_pair(s: &str) -> Option<(ChannelId, MessageId)> { +pub fn parse_message_id_pair(s: &str) -> Option<(ChannelOrThreadId, MessageId)> { let mut parts = s.splitn(2, '-'); let channel_id = parts.next()?.parse().ok()?; let message_id = parts.next()?.parse().ok()?; @@ -123,7 +123,7 @@ pub fn parse_message_id_pair(s: &str) -> Option<(ChannelId, MessageId)> { /// assert_eq!(parse_message_url("https://google.com"), None); /// ``` #[must_use] -pub fn parse_message_url(s: &str) -> Option<(GuildId, ChannelId, MessageId)> { +pub fn parse_message_url(s: &str) -> Option<(GuildId, ChannelOrThreadId, MessageId)> { use aformat::{aformat, CapStr}; for domain in DOMAINS { diff --git a/src/utils/argument_convert/role.rs b/src/utils/argument_convert/role.rs index c18319d9a9a..7823612799c 100644 --- a/src/utils/argument_convert/role.rs +++ b/src/utils/argument_convert/role.rs @@ -54,7 +54,7 @@ impl ArgumentConvert for Role { async fn convert( ctx: impl CacheHttp, guild_id: Option, - _channel_id: Option, + _channel_id: Option, s: &str, ) -> Result { let guild_id = guild_id.ok_or(RoleParseError::NotInGuild)?; diff --git a/src/utils/argument_convert/user.rs b/src/utils/argument_convert/user.rs index c49db78a360..8e36e3e5d7a 100644 --- a/src/utils/argument_convert/user.rs +++ b/src/utils/argument_convert/user.rs @@ -39,7 +39,7 @@ impl ArgumentConvert for User { async fn convert( ctx: impl CacheHttp, guild_id: Option, - channel_id: Option, + channel_id: Option, s: &str, ) -> Result { // Convert as a Member which uses HTTP endpoints instead of cache diff --git a/src/utils/content_safe.rs b/src/utils/content_safe.rs index 41f61a01c76..d4c23c9af3c 100644 --- a/src/utils/content_safe.rs +++ b/src/utils/content_safe.rs @@ -163,8 +163,11 @@ fn clean_mention( ) -> Cow<'static, str> { match mention { Mention::Channel(id) => { - if let Some(channel) = guild.channels.get(&id) { - format!("#{}", channel.name).into() + let (channel_id, thread_id) = id.split(); + if let Some(channel) = guild.channels.get(&channel_id) { + format!("#{}", channel.base.name).into() + } else if let Some(thread) = guild.threads.get(&thread_id) { + format!("#{}", thread.base.name).into() } else { "#deleted-channel".into() } @@ -232,7 +235,10 @@ mod tests { let channel = GuildChannel { id: ChannelId::new(111880193700067777), - name: FixedString::from_static_trunc("general"), + base: BaseGuildChannel { + name: FixedString::from_static_trunc("general"), + ..Default::default() + }, ..Default::default() }; diff --git a/src/utils/custom_message.rs b/src/utils/custom_message.rs index 98c7ccb52df..6c20330da6b 100644 --- a/src/utils/custom_message.rs +++ b/src/utils/custom_message.rs @@ -49,8 +49,8 @@ impl CustomMessage { /// Assign the dummy message its origin channel's ID. /// - /// If not used, the default value is `ChannelId::new(1)`. - pub fn channel_id(&mut self, channel_id: ChannelId) -> &mut Self { + /// If not used, the default value is `ChannelOrThreadId::new(1)`. + pub fn channel_id(&mut self, channel_id: ChannelOrThreadId) -> &mut Self { self.msg.channel_id = channel_id; self diff --git a/src/utils/message_builder.rs b/src/utils/message_builder.rs index aaa71343887..0619432dd74 100644 --- a/src/utils/message_builder.rs +++ b/src/utils/message_builder.rs @@ -2,7 +2,7 @@ use std::fmt::{self as fmt, Write}; use std::ops::Add; use crate::model::guild::Emoji; -use crate::model::id::{ChannelId, RoleId, UserId}; +use crate::model::id::{ChannelOrThreadId, RoleId, UserId}; use crate::model::mention::Mentionable; /// The Message Builder is an ergonomic utility to easily build a message, by adding text and @@ -112,7 +112,7 @@ impl MessageBuilder { /// [`Channel`]: crate::model::channel::Channel /// [`GuildChannel`]: crate::model::channel::GuildChannel /// [Display implementation]: ChannelId#impl-Display - pub fn channel(mut self, channel: ChannelId) -> Self { + pub fn channel(mut self, channel: ChannelOrThreadId) -> Self { self.push_(&channel.mention()); self } @@ -1145,7 +1145,7 @@ mod test { let content_emoji = MessageBuilder::new().emoji(&emoji).build(); let content_mentions = MessageBuilder::new() - .channel(ChannelId::new(1)) + .channel(ChannelOrThreadId::new(1)) .mention(&UserId::new(2)) .role(RoleId::new(3)) .user(UserId::new(4)) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 197941b4cc2..3320b7d4fec 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -211,7 +211,7 @@ pub fn parse_role_mention(mention: &str) -> Option { /// /// [`Channel`]: crate::model::channel::Channel #[must_use] -pub fn parse_channel_mention(mention: &str) -> Option { +pub fn parse_channel_mention(mention: &str) -> Option { if mention.len() < 4 { return None; }