From bfdceb53bd8f35c85ef43bec8cb450ecd8d60fea Mon Sep 17 00:00:00 2001 From: raventhyme <206758110+raventhyme@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:22:03 +0000 Subject: [PATCH 1/2] feat(gateway, http, model): channel obfuscation Implement Discord's new Channel Obfuscation API. Channel Obfuscation obfuscates most details about guild channels that a user does not have access to when received over the gateway; when received over the HTTP API the user does not receive the channel at all. Channels that have been obfuscated have a new "CHANNEL_OBFUSCATED" channel flag set on them. Documentation has been added to the model crate's `Channel` and `ChannelUpdate` types. Channel Obfuscation may be opted in to via a new "capabilities" field on gateway identification. To opt-in when connecting to the gateway users may use the new `capabilities` method on the gateway's `Config`. Closes issue #2573. --- twilight-gateway/src/config.rs | 12 ++- twilight-gateway/src/shard.rs | 1 + .../src/request/guild/get_guild_channels.rs | 6 ++ twilight-model/src/channel/flags.rs | 6 ++ twilight-model/src/channel/mod.rs | 25 +++++ twilight-model/src/gateway/capabilities.rs | 99 +++++++++++++++++++ twilight-model/src/gateway/mod.rs | 2 + .../payload/incoming/channel_update.rs | 8 ++ .../src/gateway/payload/outgoing/identify.rs | 3 +- 9 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 twilight-model/src/gateway/capabilities.rs diff --git a/twilight-gateway/src/config.rs b/twilight-gateway/src/config.rs index 41240807cf..61a166b299 100644 --- a/twilight-gateway/src/config.rs +++ b/twilight-gateway/src/config.rs @@ -7,7 +7,7 @@ use std::{ }; use tokio_websockets::Connector; use twilight_model::gateway::{ - Intents, + Capabilities, Intents, payload::outgoing::{identify::IdentifyProperties, update_presence::UpdatePresencePayload}, }; @@ -40,6 +40,8 @@ impl Debug for Token { /// [`From`] implementation and then rebuilding it into a rew config. #[derive(Clone, Debug)] pub struct Config { + /// Capabilities that the shard opts into when identifying with the gateway. + capabilities: Capabilities, /// Identification properties the shard will use. identify_properties: Option, /// Intents that the shard requests when identifying with the gateway. @@ -84,6 +86,11 @@ impl Config { } impl Config { + /// Capabilities that the shard opts into when identifying with the gateway. + pub const fn capabilities(&self) -> Capabilities { + self.capabilities + } + /// Immutable reference to the identification properties the shard will use. pub const fn identify_properties(&self) -> Option<&IdentifyProperties> { self.identify_properties.as_ref() @@ -166,6 +173,7 @@ impl ConfigBuilder { Self { inner: Config { + capabilities: Capabilities::empty(), identify_properties: None, intents, large_threshold: 50, @@ -324,6 +332,7 @@ impl ConfigBuilder { /// turns itself into a no-op. pub fn queue(self, queue: NewQ) -> ConfigBuilder { let Config { + capabilities, identify_properties, intents, large_threshold, @@ -339,6 +348,7 @@ impl ConfigBuilder { ConfigBuilder { inner: Config { + capabilities, identify_properties, intents, large_threshold, diff --git a/twilight-gateway/src/shard.rs b/twilight-gateway/src/shard.rs index 74d0fd648b..df1be157f2 100644 --- a/twilight-gateway/src/shard.rs +++ b/twilight-gateway/src/shard.rs @@ -691,6 +691,7 @@ impl Shard { self.pending = Pending::event( Identify::new(IdentifyInfo { + capabilities: self.config.capabilities(), compress: false, intents: self.config.intents(), large_threshold: self.config.large_threshold(), diff --git a/twilight-http/src/request/guild/get_guild_channels.rs b/twilight-http/src/request/guild/get_guild_channels.rs index 11351399d9..344a08e690 100644 --- a/twilight-http/src/request/guild/get_guild_channels.rs +++ b/twilight-http/src/request/guild/get_guild_channels.rs @@ -12,6 +12,12 @@ use twilight_model::{ }; /// Get the channels in a guild. +/// +/// # Channel Obfuscation +/// +/// Channels the user does not have access to will not be returned via this +/// endpoint once Discord finalizes the rollout of Channel Obfuscation on or +/// around November 16th, 2026. #[must_use = "requests must be configured and executed"] pub struct GetGuildChannels<'a> { guild_id: Id, diff --git a/twilight-model/src/channel/flags.rs b/twilight-model/src/channel/flags.rs index a5a743ddab..125f758be2 100644 --- a/twilight-model/src/channel/flags.rs +++ b/twilight-model/src/channel/flags.rs @@ -13,6 +13,12 @@ bitflags! { const REQUIRE_TAG = 1 << 4; /// Hide the download options for this post in a media channel. const HIDE_MEDIA_DOWNLOAD_OPTIONS = 1 << 15; + /// This channel's metadata has been obfuscated because the current user + /// cannot view it. + /// + /// Only ever set on channels received over the Gateway; the HTTP API + /// never sets this flag. + const CHANNEL_OBFUSCATED = 1 << 17; } } diff --git a/twilight-model/src/channel/mod.rs b/twilight-model/src/channel/mod.rs index 641fd6fcf3..c42173862f 100644 --- a/twilight-model/src/channel/mod.rs +++ b/twilight-model/src/channel/mod.rs @@ -54,7 +54,32 @@ use serde::{Deserialize, Serialize}; /// /// For Discord's documentation on channels, refer to [Discord Docs/Channel]. /// +/// # Obfuscated channels +/// +/// Guild channels which a user can not access are referred to as "obfuscated +/// channels". Obfuscated channels can be opted in to via the +/// [`CHANNEL_OBFUSCATION`][crate::gateway::Capabilities::CHANNEL_OBFUSCATION] +/// capability prior to its enforcement by Discord on November 16th, 2026; once +/// released, obfuscated channels will be enforced for all users. +/// +/// Via the HTTP API obfuscated channels aren't received. +/// +/// Via the Gateway obfuscated channels are received with the +/// [`ChannelFlags::CHANNEL_OBFUSCATED`] flag set in the [`flags`][Self::flags] +/// field. Fields such as [`name`][Self::name] will be obfuscated with values +/// such as `"___hidden___"`. The [`id`][Self::id], [`kind`][Self::kind], +/// [`position`][Self::position], and [`parent_id`][Self::parent_id] fields +/// won't be obfuscated and fields other than these should not be relied upon. +/// Obfuscated channels will have a single +/// [permission overwrite][permission_overwrite::PermissionOverwrite] denying +/// the [`VIEW_CHANNEL`][`Permissions::VIEW_CHANNEL`] permission to the guild's +/// `@everyone` role. +/// +/// Refer to [Discord Docs/Channel Obfuscation]. +/// +/// [`Permissions::VIEW_CHANNEL`]: crate::guild::Permissions::VIEW_CHANNEL /// [Discord Docs/Channel]: https://discord.com/developers/docs/resources/channel +/// [Discord Docs/Channel Obfuscation]: https://docs.discord.com/developers/resources/channel#channel-object-obfuscated-channels #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Channel { /// ID of the application that created the channel. diff --git a/twilight-model/src/gateway/capabilities.rs b/twilight-model/src/gateway/capabilities.rs new file mode 100644 index 0000000000..7ab1fb3d44 --- /dev/null +++ b/twilight-model/src/gateway/capabilities.rs @@ -0,0 +1,99 @@ +use bitflags::bitflags; +use serde::{ + de::{Deserialize, Deserializer}, + ser::{Serialize, Serializer}, +}; + +bitflags! { + /// Gateway capabilities. + /// + /// Developers may specify capabilities when connecting to the gateway. + /// Capabilities allow applications to opt-in to gateway behavior. To + /// specify multiple capabilities, create a union using the `|` operator. See + /// [Discord Docs/Capabilities]. + /// + /// [Discord Docs/Capabilities]: https://discord.com/developers/docs/topics/gateway#identify-gateway-capabilities + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct Capabilities: u64 { + /// Opts the client into receiving obfuscated channel metadata over the + /// Gateway for channels it can't view. + /// + /// Discord plans to fully roll out Channel Obfuscation for all users on + /// November 16th, 2026, at which point specifying this capability would + /// have no effect. + /// + /// See [Discord Docs/Channel Obfuscation]. + /// + /// [Discord Docs/Channel Obfuscation]: https://docs.discord.com/developers/resources/channel#channel-object-obfuscated-channels + const CHANNEL_OBFUSCATION = 1 << 15; + } +} + +impl<'de> Deserialize<'de> for Capabilities { + fn deserialize>(deserializer: D) -> Result { + Ok(Self::from_bits_truncate(u64::deserialize(deserializer)?)) + } +} + +impl Serialize for Capabilities { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u64(self.bits()) + } +} + +#[cfg(test)] +mod tests { + use super::Capabilities; + use serde::{Deserialize, Serialize}; + use serde_test::Token; + use static_assertions::{assert_impl_all, const_assert_eq}; + use std::{ + fmt::{Binary, Debug, LowerHex, Octal, UpperHex}, + hash::Hash, + ops::{ + BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, Sub, SubAssign, + }, + }; + + assert_impl_all!( + Capabilities: Binary, + BitAnd, + BitAndAssign, + BitOr, + BitOrAssign, + BitXor, + BitXorAssign, + Clone, + Copy, + Debug, + Deserialize<'static>, + Eq, + Extend, + FromIterator, + Hash, + LowerHex, + Not, + Octal, + PartialEq, + Send, + Serialize, + Sub, + SubAssign, + Sync, + UpperHex + ); + const_assert_eq!(Capabilities::CHANNEL_OBFUSCATION.bits(), 1 << 15); + + #[test] + fn serde() { + serde_test::assert_tokens( + &Capabilities::CHANNEL_OBFUSCATION, + &[Token::U64(Capabilities::CHANNEL_OBFUSCATION.bits())], + ); + // Deserialization truncates unknown bits. + serde_test::assert_de_tokens(&Capabilities::empty(), &[Token::U64(1 << 63)]); + } +} diff --git a/twilight-model/src/gateway/mod.rs b/twilight-model/src/gateway/mod.rs index 327b0d265e..776eef9b32 100644 --- a/twilight-model/src/gateway/mod.rs +++ b/twilight-model/src/gateway/mod.rs @@ -3,6 +3,7 @@ pub mod event; pub mod payload; pub mod presence; +mod capabilities; mod close_code; mod frame; mod id; @@ -12,6 +13,7 @@ mod reaction; mod session_start_limit; pub use self::{ + capabilities::Capabilities, close_code::{CloseCode, CloseCodeConversionError}, frame::CloseFrame, id::{ShardId, ShardIdParseError, ShardIdParseErrorType}, diff --git a/twilight-model/src/gateway/payload/incoming/channel_update.rs b/twilight-model/src/gateway/payload/incoming/channel_update.rs index b82e874ad3..67c6eaa19c 100644 --- a/twilight-model/src/gateway/payload/incoming/channel_update.rs +++ b/twilight-model/src/gateway/payload/incoming/channel_update.rs @@ -2,6 +2,14 @@ use crate::channel::Channel; use serde::{Deserialize, Serialize}; use std::ops::{Deref, DerefMut}; +/// A channel has been updated. +/// +/// # Channel obfuscation +/// +/// A guild channel which a user does not have access to may be obfuscated. Once +/// the current user has access to a guild channel the previously obfuscated +/// fields will have their true values revealed. Refer to the documentation for +/// [`Channel`] for more information on channel obfuscation. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ChannelUpdate(pub Channel); diff --git a/twilight-model/src/gateway/payload/outgoing/identify.rs b/twilight-model/src/gateway/payload/outgoing/identify.rs index 9479a4d418..dc9de632d2 100644 --- a/twilight-model/src/gateway/payload/outgoing/identify.rs +++ b/twilight-model/src/gateway/payload/outgoing/identify.rs @@ -1,5 +1,5 @@ use super::update_presence::UpdatePresencePayload; -use crate::gateway::{ShardId, intents::Intents, opcode::OpCode}; +use crate::gateway::{Capabilities, ShardId, intents::Intents, opcode::OpCode}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] @@ -19,6 +19,7 @@ impl Identify { #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct IdentifyInfo { + pub capabilities: Capabilities, pub compress: bool, pub intents: Intents, pub large_threshold: u64, From 6020c827e9047c8dac7e9624276908688e6118a3 Mon Sep 17 00:00:00 2001 From: raventhyme <206758110+raventhyme@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:27:35 +0000 Subject: [PATCH 2/2] test for new channel flag --- twilight-model/src/channel/flags.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/twilight-model/src/channel/flags.rs b/twilight-model/src/channel/flags.rs index 125f758be2..943a7d050e 100644 --- a/twilight-model/src/channel/flags.rs +++ b/twilight-model/src/channel/flags.rs @@ -80,6 +80,7 @@ mod tests { ); const_assert_eq!(ChannelFlags::PINNED.bits(), 1 << 1); const_assert_eq!(ChannelFlags::REQUIRE_TAG.bits(), 1 << 4); + const_assert_eq!(ChannelFlags::CHANNEL_OBFUSCATED.bits(), 1 << 17); #[test] fn serde() {