diff --git a/examples/e01_basic_ping_bot/src/main.rs b/examples/e01_basic_ping_bot/src/main.rs index 86a41bcff5f..5954373c7d9 100644 --- a/examples/e01_basic_ping_bot/src/main.rs +++ b/examples/e01_basic_ping_bot/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -37,7 +35,8 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Set gateway intents, which decides what events the bot will be notified about let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES @@ -46,7 +45,7 @@ async fn main() { // Create a new instance of the Client, logging in as a bot. This will automatically prepend // your bot token with "Bot ", which is a requirement by Discord for bot users. let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // Finally, start a single shard, and start listening to events. // diff --git a/examples/e02_transparent_guild_sharding/src/main.rs b/examples/e02_transparent_guild_sharding/src/main.rs index 288323e8b29..96215d41817 100644 --- a/examples/e02_transparent_guild_sharding/src/main.rs +++ b/examples/e02_transparent_guild_sharding/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -42,12 +40,13 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // The total number of shards to use. The "current shard number" of a shard - that is, the // shard it is assigned to - is indexed at 0, while the total shard count is indexed at 1. diff --git a/examples/e03_struct_utilities/src/main.rs b/examples/e03_struct_utilities/src/main.rs index ce012279026..cdbcea2f90f 100644 --- a/examples/e03_struct_utilities/src/main.rs +++ b/examples/e03_struct_utilities/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::builder::CreateMessage; use serenity::model::channel::Message; @@ -35,12 +33,13 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); if let Err(why) = client.start().await { println!("Client error: {why:?}"); diff --git a/examples/e04_message_builder/src/main.rs b/examples/e04_message_builder/src/main.rs index 3ce9a5cb0f6..03ff0feb6b0 100644 --- a/examples/e04_message_builder/src/main.rs +++ b/examples/e04_message_builder/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::Ready; @@ -46,12 +44,13 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); if let Err(why) = client.start().await { println!("Client error: {why:?}"); diff --git a/examples/e05_sample_bot_structure/src/main.rs b/examples/e05_sample_bot_structure/src/main.rs index 76026564090..83c6ff8ebb7 100644 --- a/examples/e05_sample_bot_structure/src/main.rs +++ b/examples/e05_sample_bot_structure/src/main.rs @@ -72,10 +72,11 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Build our client. - let mut client = Client::builder(&token, GatewayIntents::empty()) + let mut client = Client::builder(token, GatewayIntents::empty()) .event_handler(Handler) .await .expect("Error creating client"); diff --git a/examples/e06_env_logging/src/main.rs b/examples/e06_env_logging/src/main.rs index 0e1a674a29b..2c70eae2f43 100644 --- a/examples/e06_env_logging/src/main.rs +++ b/examples/e06_env_logging/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::event::ResumedEvent; use serenity::model::gateway::Ready; @@ -42,14 +40,15 @@ async fn main() { tracing_subscriber::fmt::init(); // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); if let Err(why) = client.start().await { error!("Client error: {:?}", why); diff --git a/examples/e07_shard_manager/src/main.rs b/examples/e07_shard_manager/src/main.rs index 53ddcedfd35..9a3c8719706 100644 --- a/examples/e07_shard_manager/src/main.rs +++ b/examples/e07_shard_manager/src/main.rs @@ -19,7 +19,6 @@ //! //! Note that it may take a minute or more for a latency to be recorded or to update, depending on //! how often Discord tells the client to send a heartbeat. -use std::env; use std::time::Duration; use serenity::async_trait; @@ -44,13 +43,14 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); // Here we clone a lock to the Shard Manager, and then move it into a new thread. The thread // will unlock the manager and print shards' status on a loop. diff --git a/examples/e08_create_message_builder/src/main.rs b/examples/e08_create_message_builder/src/main.rs index 712713e4e50..3432a42a5b9 100644 --- a/examples/e08_create_message_builder/src/main.rs +++ b/examples/e08_create_message_builder/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::builder::{CreateAttachment, CreateEmbed, CreateEmbedFooter, CreateMessage}; use serenity::model::channel::Message; @@ -51,12 +49,13 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); if let Err(why) = client.start().await { println!("Client error: {why:?}"); diff --git a/examples/e09_collectors/src/main.rs b/examples/e09_collectors/src/main.rs index e1336c29bc7..4803697bb51 100644 --- a/examples/e09_collectors/src/main.rs +++ b/examples/e09_collectors/src/main.rs @@ -1,7 +1,6 @@ //! This example will showcase the beauty of collectors. They allow to await messages or reactions //! from a user in the middle of a control flow, one being a command. use std::collections::HashSet; -use std::env; use std::time::Duration; use serenity::async_trait; @@ -136,7 +135,8 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES @@ -144,7 +144,7 @@ async fn main() { | GatewayIntents::GUILD_MESSAGE_REACTIONS; let mut client = - Client::builder(&token, intents).event_handler(Handler).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(Handler).await.expect("Err creating client"); if let Err(why) = client.start().await { println!("Client error: {why:?}"); diff --git a/examples/e10_gateway_intents/src/main.rs b/examples/e10_gateway_intents/src/main.rs index ea1ffeca732..7e3e0c8bcd2 100644 --- a/examples/e10_gateway_intents/src/main.rs +++ b/examples/e10_gateway_intents/src/main.rs @@ -1,5 +1,3 @@ -use std::env; - use serenity::async_trait; use serenity::model::channel::Message; use serenity::model::gateway::{Presence, Ready}; @@ -33,13 +31,14 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Intents are a bitflag, bitwise operations can be used to dictate which intents to use let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; // Build our client. - let mut client = Client::builder(&token, intents) + let mut client = Client::builder(token, intents) .event_handler(Handler) .await .expect("Error creating client"); diff --git a/examples/e11_global_data/src/main.rs b/examples/e11_global_data/src/main.rs index e4c2ad81d49..7de92c9e879 100644 --- a/examples/e11_global_data/src/main.rs +++ b/examples/e11_global_data/src/main.rs @@ -1,7 +1,6 @@ //! In this example, you will be shown how to share data between events. use std::borrow::Cow; -use std::env; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -61,7 +60,8 @@ impl EventHandler for Handler { #[tokio::main] async fn main() { - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // We setup the initial value for our user data, which we will use throughout the rest of our // program. @@ -72,7 +72,7 @@ async fn main() { let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) + let mut client = Client::builder(token, intents) // Specifying the data type as a type argument here is optional, but if done, you can // guarantee that Context::data will not panic if the same type is given, as providing the // incorrect type will lead to a compiler error, rather than a runtime panic. diff --git a/examples/e12_parallel_loops/src/main.rs b/examples/e12_parallel_loops/src/main.rs index b0bca910064..73a17b67ecf 100644 --- a/examples/e12_parallel_loops/src/main.rs +++ b/examples/e12_parallel_loops/src/main.rs @@ -1,4 +1,3 @@ -use std::env; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; @@ -100,13 +99,14 @@ fn set_activity_to_current_time(ctx: &Context) { #[tokio::main] async fn main() { - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::GUILDS | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) + let mut client = Client::builder(token, intents) .event_handler(Handler { is_loop_running: AtomicBool::new(false), }) diff --git a/examples/e13_sqlite_database/src/main.rs b/examples/e13_sqlite_database/src/main.rs index f8bf5d4d2e9..d41cc479dc9 100644 --- a/examples/e13_sqlite_database/src/main.rs +++ b/examples/e13_sqlite_database/src/main.rs @@ -72,7 +72,8 @@ impl EventHandler for Bot { #[tokio::main] async fn main() { // Configure the client with your Discord bot token in the environment. - let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Initiate a connection to the database file, creating the file if required. let database = sqlx::sqlite::SqlitePoolOptions::new() @@ -96,6 +97,6 @@ async fn main() { | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; let mut client = - Client::builder(&token, intents).event_handler(bot).await.expect("Err creating client"); + Client::builder(token, intents).event_handler(bot).await.expect("Err creating client"); client.start().await.unwrap(); } diff --git a/examples/e14_message_components/src/main.rs b/examples/e14_message_components/src/main.rs index 2ae085b5049..97413a52789 100644 --- a/examples/e14_message_components/src/main.rs +++ b/examples/e14_message_components/src/main.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::env; use std::time::Duration; use dotenv::dotenv; @@ -140,13 +139,14 @@ impl EventHandler for Handler { async fn main() { dotenv().ok(); // Configure the client with your Discord bot token in the environment. - let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); // Build our client. let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::DIRECT_MESSAGES | GatewayIntents::MESSAGE_CONTENT; - let mut client = Client::builder(&token, intents) + let mut client = Client::builder(token, intents) .event_handler(Handler) .await .expect("Error creating client"); diff --git a/examples/e15_webhook/src/main.rs b/examples/e15_webhook/src/main.rs index 946c18ba665..3ea35063e97 100644 --- a/examples/e15_webhook/src/main.rs +++ b/examples/e15_webhook/src/main.rs @@ -5,7 +5,7 @@ use serenity::model::webhook::Webhook; #[tokio::main] async fn main() { // You don't need a token when you are only dealing with webhooks. - let http = Http::new(""); + let http = Http::without_token(); let webhook = Webhook::from_url(&http, "https://discord.com/api/webhooks/133742013374206969/hello-there-oPNtRN5UY5DVmBe7m1N0HE-replace-me-Dw9LRkgq3zI7LoW3Rb-k-q") .await .expect("Replace the webhook with your own"); diff --git a/examples/testing/src/main.rs b/examples/testing/src/main.rs index 909ecf436b9..cf0b88e32d5 100644 --- a/examples/testing/src/main.rs +++ b/examples/testing/src/main.rs @@ -415,7 +415,8 @@ async fn main() -> Result<(), serenity::Error> { } env_logger::init(); - let token = std::env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let token = + Token::from_env("DISCORD_TOKEN").expect("Expected a valid token in the environment"); let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; - Client::builder(&token, intents).event_handler(Handler).await?.start().await + Client::builder(token, intents).event_handler(Handler).await?.start().await } diff --git a/src/error.rs b/src/error.rs index 5f944606a07..d090cb860e6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,7 @@ use crate::gateway::GatewayError; use crate::http::HttpError; use crate::internal::prelude::*; use crate::model::ModelError; +use crate::secrets::TokenError; /// The common result type between most library functions. /// @@ -46,6 +47,10 @@ pub enum Error { /// An error from the `tungstenite` crate. #[cfg(feature = "gateway")] Tungstenite(Box), + /// An error from the [`secrets`] module. + /// + /// [`secrets`]: crate::secrets + Token(TokenError), } #[cfg(feature = "gateway")] @@ -87,6 +92,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: TokenError) -> Error { + Error::Token(e) + } +} + #[cfg(feature = "http")] impl From for Error { fn from(e: InvalidHeaderValue) -> Error { @@ -113,6 +124,7 @@ impl fmt::Display for Error { Self::Http(inner) => fmt::Display::fmt(&inner, f), #[cfg(feature = "gateway")] Self::Tungstenite(inner) => fmt::Display::fmt(&inner, f), + Self::Token(inner) => fmt::Display::fmt(&inner, f), } } } @@ -130,6 +142,7 @@ impl StdError for Error { Self::Http(inner) => Some(inner), #[cfg(feature = "gateway")] Self::Tungstenite(inner) => Some(inner), + Self::Token(inner) => Some(inner), } } } diff --git a/src/gateway/client/context.rs b/src/gateway/client/context.rs index 5287dcefa88..6341e87de35 100644 --- a/src/gateway/client/context.rs +++ b/src/gateway/client/context.rs @@ -4,7 +4,7 @@ use std::sync::Arc; #[cfg(feature = "cache")] pub use crate::cache::Cache; use crate::gateway::{ActivityData, ShardMessenger, ShardRunner}; -use crate::http::Http; +use crate::http::{CacheHttp, Http}; use crate::model::prelude::*; /// A general utility struct provided on event dispatches. @@ -48,6 +48,16 @@ impl fmt::Debug for Context { } } +impl CacheHttp for Context { + fn http(&self) -> &Http { + &self.http + } + #[cfg(feature = "cache")] + fn cache(&self) -> Option<&Arc> { + Some(&self.cache) + } +} + impl Context { /// Create a new Context to be passed to an event handler. pub(crate) fn new( diff --git a/src/gateway/client/mod.rs b/src/gateway/client/mod.rs index 109e3aa14ac..138ad3f73af 100644 --- a/src/gateway/client/mod.rs +++ b/src/gateway/client/mod.rs @@ -70,6 +70,7 @@ use crate::model::user::OnlineStatus; /// A builder implementing [`IntoFuture`] building a [`Client`] to interact with Discord. #[must_use = "Builders do nothing unless they are awaited"] pub struct ClientBuilder { + token: Token, data: Option>, http: Arc, intents: GatewayIntents, @@ -93,8 +94,8 @@ impl ClientBuilder { /// **Panic**: If you have enabled the `framework`-feature (on by default), you must specify a /// framework via the [`Self::framework`] method, otherwise awaiting the builder will cause a /// panic. - pub fn new(token: &str, intents: GatewayIntents) -> Self { - Self::new_with_http(Arc::new(Http::new(token)), intents) + pub fn new(token: Token, intents: GatewayIntents) -> Self { + Self::new_with_http(token.clone(), Arc::new(Http::new(token)), intents) } /// Construct a new builder with a [`Http`] instance to calls methods on for the client @@ -103,8 +104,9 @@ impl ClientBuilder { /// **Panic**: If you have enabled the `framework`-feature (on by default), you must specify a /// framework via the [`Self::framework`] method, otherwise awaiting the builder will cause a /// panic. - pub fn new_with_http(http: Arc, intents: GatewayIntents) -> Self { + pub fn new_with_http(token: Token, http: Arc, intents: GatewayIntents) -> Self { Self { + token, http, intents, data: None, @@ -335,6 +337,7 @@ impl IntoFuture for ClientBuilder { #[cfg(feature = "framework")] let framework_cell = Arc::new(OnceLock::new()); let (shard_manager, shard_manager_ret_value) = ShardManager::new(ShardManagerOptions { + token: self.token, data: Arc::clone(&data), event_handler: self.event_handler, raw_event_handler: self.raw_event_handler, @@ -413,8 +416,9 @@ impl IntoFuture for ClientBuilder { /// } /// /// # async fn run() -> Result<(), Box> { +/// let token = Token::from_env("DISCORD_TOKEN")?; /// let mut client = -/// Client::builder("my token here", GatewayIntents::default()).event_handler(Handler).await?; +/// Client::builder(token, GatewayIntents::default()).event_handler(Handler).await?; /// /// client.start().await?; /// # Ok(()) @@ -497,7 +501,7 @@ pub struct Client { } impl Client { - pub fn builder(token: &str, intents: GatewayIntents) -> ClientBuilder { + pub fn builder(token: Token, intents: GatewayIntents) -> ClientBuilder { ClientBuilder::new(token, intents) } @@ -540,8 +544,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = Token::from_env("DISCORD_TOKEN")?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start().await { /// println!("Err with client: {:?}", why); @@ -583,8 +587,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = Token::from_env("DISCORD_TOKEN")?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_autosharded().await { /// println!("Err with client: {:?}", why); @@ -630,8 +634,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = Token::from_env("DISCORD_TOKEN")?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shard(3, 5).await { /// println!("Err with client: {:?}", why); @@ -649,8 +653,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = Token::from_env("DISCORD_TOKEN")?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shard(0, 1).await { /// println!("Err with client: {:?}", why); @@ -692,8 +696,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = Token::from_env("DISCORD_TOKEN")?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shards(8).await { /// println!("Err with client: {:?}", why); @@ -735,8 +739,8 @@ impl Client { /// use serenity::Client; /// /// # async fn run() -> Result<(), Box> { - /// let token = std::env::var("DISCORD_TOKEN")?; - /// let mut client = Client::builder(&token, GatewayIntents::default()).await?; + /// let token = Token::from_env("DISCORD_TOKEN")?; + /// let mut client = Client::builder(token, GatewayIntents::default()).await?; /// /// if let Err(why) = client.start_shard_range(4..7, 10).await { /// println!("Err with client: {:?}", why); diff --git a/src/gateway/sharding/mod.rs b/src/gateway/sharding/mod.rs index de23d3e96e1..76b5547db16 100644 --- a/src/gateway/sharding/mod.rs +++ b/src/gateway/sharding/mod.rs @@ -47,6 +47,7 @@ use std::time::{Duration as StdDuration, Instant}; #[cfg(feature = "transport_compression_zlib")] use aformat::aformat_into; use aformat::{aformat, ArrayString, CapStr}; +use serde::Deserialize; use tokio_tungstenite::tungstenite::error::Error as TungsteniteError; use tokio_tungstenite::tungstenite::protocol::frame::CloseFrame; use tracing::{debug, error, info, trace, warn}; @@ -112,7 +113,7 @@ pub struct Shard { // This acts as a timeout to determine if the shard has - for some reason - not started within // a decent amount of time. pub started: Instant, - token: SecretString, + token: Token, ws_url: Arc, resume_ws_url: Option, compression: TransportCompression, @@ -135,14 +136,14 @@ impl Shard { /// use serenity::gateway::{Shard, TransportCompression}; /// use serenity::model::gateway::{GatewayIntents, ShardInfo}; /// use serenity::model::id::ShardId; - /// use serenity::secret_string::SecretString; + /// use serenity::secrets::Token; /// use tokio::sync::Mutex; /// # /// # use serenity::http::Http; /// # /// # async fn run() -> Result<(), Box> { /// # let http: Arc = unimplemented!(); - /// let token = SecretString::new(Arc::from(std::env::var("DISCORD_BOT_TOKEN")?)); + /// let token = Token::from_env("DISCORD_TOKEN")?; /// let shard_info = ShardInfo { /// id: ShardId(0), /// total: NonZeroU16::MIN, @@ -172,7 +173,7 @@ impl Shard { /// TLS error. pub async fn new( ws_url: Arc, - token: SecretString, + token: Token, shard_info: ShardInfo, intents: GatewayIntents, presence: Option, @@ -330,7 +331,7 @@ impl Shard { } self.seq = seq; - let event = Event::deserialize_and_log(event, original_str)?; + let event = deserialize_and_log_event(event, original_str)?; match &event { Event::Ready(ready) => { @@ -835,6 +836,22 @@ async fn connect(base_url: &str, compression: TransportCompression) -> Result Result { + Event::deserialize(Value::Object(map)).map_err(|err| { + let err = serde::de::Error::custom(err); + let err_dbg = format!("{err:?}"); + if let Some((variant_name, _)) = + err_dbg.strip_prefix(r#"Error("unknown variant `"#).and_then(|s| s.split_once('`')) + { + debug!("Unknown event: {variant_name}"); + } else { + warn!("Err deserializing text: {err_dbg}"); + } + debug!("Failing text: {original_str}"); + Error::Json(err) + }) +} + #[derive(Debug)] #[non_exhaustive] pub enum ShardAction { diff --git a/src/gateway/sharding/shard_manager.rs b/src/gateway/sharding/shard_manager.rs index a80aa835879..b0cc2e986fa 100644 --- a/src/gateway/sharding/shard_manager.rs +++ b/src/gateway/sharding/shard_manager.rs @@ -83,8 +83,10 @@ pub const DEFAULT_WAIT_BETWEEN_SHARD_START: Duration = Duration::from_secs(5); /// let ws_url = Arc::from(gateway_info.url); /// let event_handler = Arc::new(Handler); /// let max_concurrency = std::num::NonZeroU16::MIN; +/// let token = Token::from_env("DISCORD_TOKEN")?; /// /// ShardManager::new(ShardManagerOptions { +/// token, /// data, /// event_handler: Some(event_handler), /// raw_event_handler: None, @@ -144,6 +146,7 @@ impl ShardManager { }); let mut shard_queuer = ShardQueuer { + token: opt.token, data: opt.data, event_handler: opt.event_handler, raw_event_handler: opt.raw_event_handler, @@ -376,6 +379,7 @@ impl Drop for ShardManager { } pub struct ShardManagerOptions { + pub token: Token, pub data: Arc, pub event_handler: Option>, pub raw_event_handler: Option>, diff --git a/src/gateway/sharding/shard_queuer.rs b/src/gateway/sharding/shard_queuer.rs index 27935992c00..0fc202aae94 100644 --- a/src/gateway/sharding/shard_queuer.rs +++ b/src/gateway/sharding/shard_queuer.rs @@ -37,6 +37,7 @@ use crate::model::gateway::{GatewayIntents, ShardInfo}; /// A shard queuer instance _should_ be run in its own thread, due to the blocking nature of the /// loop itself as well as a 5 second thread sleep between shard starts. pub struct ShardQueuer { + pub(super) token: Token, /// A copy of [`Client::data`] to be given to runners for contextual dispatching. /// /// [`Client::data`]: crate::Client::data @@ -215,7 +216,7 @@ impl ShardQueuer { }; let mut shard = Shard::new( Arc::clone(&self.ws_url), - self.http.token(), + self.token.clone(), shard_info, self.intents, self.presence.clone(), diff --git a/src/http/client.rs b/src/http/client.rs index a22462a6b21..b55fd1fe1b0 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -3,9 +3,7 @@ use std::borrow::Cow; use std::cell::Cell; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use aformat::{aformat, CapStr}; use arrayvec::ArrayVec; use nonmax::{NonMaxU16, NonMaxU8}; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; @@ -71,8 +69,10 @@ where /// ```rust /// # use serenity::http::HttpBuilder; /// # fn run() { -/// let http = -/// HttpBuilder::new("token").proxy("http://127.0.0.1:3000").ratelimiter_disabled(true).build(); +/// let http = HttpBuilder::without_token() +/// .proxy("http://127.0.0.1:3000") +/// .ratelimiter_disabled(true) +/// .build(); /// # } /// ``` #[must_use] @@ -80,21 +80,36 @@ pub struct HttpBuilder { client: Option, ratelimiter: Option, ratelimiter_disabled: bool, - token: Arc, + token: Option, proxy: Option>, application_id: Option, default_allowed_mentions: Option>, } impl HttpBuilder { - /// Construct a new builder to call methods on for the HTTP construction. The `token` will - /// automatically be prefixed "Bot " if not already. - pub fn new(token: &str) -> Self { + /// Construct a new builder. + pub fn new(token: Token) -> Self { Self { client: None, ratelimiter: None, ratelimiter_disabled: false, - token: parse_token(token), + token: Some(token), + proxy: None, + application_id: None, + default_allowed_mentions: None, + } + } + + /// Construct a new builder without a token set. + /// + /// Most Discord functionality requires a logged-in Bot token, but there are some exceptions + /// such as webhook endpoints. + pub fn without_token() -> Self { + Self { + client: None, + ratelimiter: None, + ratelimiter_disabled: false, + token: None, proxy: None, application_id: None, default_allowed_mentions: None, @@ -107,13 +122,6 @@ impl HttpBuilder { self } - /// Sets a token for the bot. If the token is not prefixed "Bot ", this method will - /// automatically do so. - pub fn token(mut self, token: &str) -> Self { - self.token = parse_token(token); - self - } - /// Sets the [`reqwest::Client`]. If one isn't provided, a default one will be used. pub fn client(mut self, client: Client) -> Self { self.client = Some(client); @@ -192,31 +200,20 @@ impl HttpBuilder { }); let ratelimiter = (!self.ratelimiter_disabled).then(|| { - self.ratelimiter - .unwrap_or_else(|| Ratelimiter::new(client.clone(), Arc::clone(&self.token))) + self.ratelimiter.unwrap_or_else(|| Ratelimiter::new(client.clone(), self.token.clone())) }); Http { client, ratelimiter, proxy: self.proxy, - token: SecretString::new(self.token), + token: self.token, application_id, default_allowed_mentions: self.default_allowed_mentions, } } } -fn parse_token(token: &str) -> Arc { - let token = token.trim(); - - if token.starts_with("Bot ") || token.starts_with("Bearer ") { - Arc::from(token) - } else { - Arc::from(aformat!("Bot {}", CapStr::<128>(token)).as_str()) - } -} - fn reason_into_header(reason: &str) -> Headers { let mut headers = Headers::new(); @@ -241,17 +238,27 @@ pub struct Http { pub(crate) client: Client, pub ratelimiter: Option, pub proxy: Option>, - token: SecretString, + token: Option, application_id: AtomicU64, pub default_allowed_mentions: Option>, } impl Http { + /// Construct an authorized HTTP client. #[must_use] - pub fn new(token: &str) -> Self { + pub fn new(token: Token) -> Self { HttpBuilder::new(token).build() } + /// Construct an unauthorized HTTP client, with no token. + /// + /// Most Discord functionality requires a logged-in Bot token, but there are some exceptions + /// such as webhook endpoints. + #[must_use] + pub fn without_token() -> Self { + HttpBuilder::without_token().build() + } + pub fn application_id(&self) -> Option { let application_id = self.application_id.load(Ordering::Relaxed); if application_id == u64::MAX { @@ -269,11 +276,6 @@ impl Http { self.application_id.store(application_id.get(), Ordering::Relaxed); } - #[cfg(feature = "gateway")] - pub(crate) fn token(&self) -> SecretString { - self.token.clone() - } - /// Adds a [`User`] to a [`Guild`] with a valid OAuth2 access token. /// /// Returns the created [`Member`] object, or nothing if the user is already a guild member. @@ -4397,7 +4399,11 @@ impl Http { ratelimiter.perform(req).await? } else { let request = req - .build(&self.client, self.token.expose_secret(), self.proxy.as_deref())? + .build( + &self.client, + self.token.as_ref().map(Token::expose_secret), + self.proxy.as_deref(), + )? .build()?; self.client.execute(request).await? }; diff --git a/src/http/mod.rs b/src/http/mod.rs index 89d969c3d69..62d74d5682a 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -41,23 +41,13 @@ pub use self::routing::*; pub use self::typing::*; #[cfg(feature = "cache")] use crate::cache::Cache; -#[cfg(feature = "gateway")] -use crate::gateway::client::Context; use crate::model::prelude::*; /// This trait will be required by functions that need [`Http`] and can optionally use a [`Cache`] /// to potentially avoid REST-requests. /// -/// The types [`Context`] and [`Http`] implement this trait and thus passing these to functions -/// expecting `impl CacheHttp` is possible. For the full list of implementations, see the -/// Implementors and Implementations on Foreign Types section in the generated docs. -/// -/// In a situation where you have the `cache`-feature enabled but you do not pass a cache, the -/// function will behave as if no `cache`-feature is active. -/// -/// If you are calling a function that expects `impl CacheHttp` as argument and you wish to utilise -/// the `cache`-feature but you got no access to a [`Context`], you can pass a tuple of -/// `(&Arc, &Http)`. +/// If the `cache` feature is enabled, but an implementing type does not have access to a cache, +/// the [`CacheHttp::cache`] method will simply return `None`. pub trait CacheHttp: Send + Sync { fn http(&self) -> &Http; @@ -94,17 +84,6 @@ where } } -#[cfg(feature = "gateway")] -impl CacheHttp for Context { - fn http(&self) -> &Http { - &self.http - } - #[cfg(feature = "cache")] - fn cache(&self) -> Option<&Arc> { - Some(&self.cache) - } -} - #[cfg(feature = "cache")] impl CacheHttp for (Option<&Arc>, &Http) { fn cache(&self) -> Option<&Arc> { diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs index 5bd48da4df0..a5dba221516 100644 --- a/src/http/ratelimiting.rs +++ b/src/http/ratelimiting.rs @@ -38,7 +38,6 @@ use std::borrow::Cow; use std::fmt; use std::str::{self, FromStr}; -use std::sync::Arc; use std::time::SystemTime; use dashmap::DashMap; @@ -85,7 +84,7 @@ pub struct Ratelimiter { client: Client, global: Mutex<()>, routes: DashMap, - token: SecretString, + token: Option, absolute_ratelimits: bool, ratelimit_callback: parking_lot::RwLock>, } @@ -105,13 +104,11 @@ impl fmt::Debug for Ratelimiter { impl Ratelimiter { /// Creates a new ratelimiter, with a shared [`reqwest`] client and the bot's token. - /// - /// The bot token must be prefixed with `"Bot "`. The ratelimiter does not prefix it. #[must_use] - pub fn new(client: Client, token: Arc) -> Self { + pub fn new(client: Client, token: Option) -> Self { Self { client, - token: SecretString::new(token), + token, global: Mutex::default(), routes: DashMap::new(), absolute_ratelimits: false, @@ -193,7 +190,11 @@ impl Ratelimiter { sleep(delay_time).await; } - let request = req.clone().build(&self.client, self.token.expose_secret(), None)?; + let request = req.clone().build( + &self.client, + self.token.as_ref().map(Token::expose_secret), + None, + )?; let response = self.client.execute(request.build()?).await?; // Check if the request got ratelimited by checking for status 429, and if so, sleep diff --git a/src/http/request.rs b/src/http/request.rs index 67137b35e09..218831a72ae 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -70,7 +70,7 @@ impl<'a> Request<'a> { pub fn build( self, client: &Client, - token: &str, + token: Option<&str>, proxy: Option<&str>, ) -> Result { let mut path = self.route.path().into_owned(); @@ -91,8 +91,12 @@ impl<'a> Request<'a> { let mut headers = self.headers.unwrap_or_default(); headers.insert(USER_AGENT, HeaderValue::from_static(constants::USER_AGENT)); - headers - .insert(AUTHORIZATION, HeaderValue::from_str(token).map_err(HttpError::InvalidHeader)?); + if let Some(token) = token { + headers.insert( + AUTHORIZATION, + HeaderValue::from_str(token).map_err(HttpError::InvalidHeader)?, + ); + } if let Some(multipart) = self.multipart { // Setting multipart adds the content-length header. diff --git a/src/internal/prelude.rs b/src/internal/prelude.rs index 981ce8a86a4..1daefde6f75 100644 --- a/src/internal/prelude.rs +++ b/src/internal/prelude.rs @@ -13,6 +13,6 @@ pub use super::utils::join_to_string; #[cfg(feature = "http")] pub use crate::error::Error; pub use crate::error::Result; -pub use crate::secret_string::SecretString; +pub use crate::secrets::{SecretString, Token}; pub type JsonMap = serde_json::Map; diff --git a/src/lib.rs b/src/lib.rs index 9bede7efd1f..c31d83ce019 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,7 +103,7 @@ pub mod gateway; pub mod http; #[cfg(feature = "interactions_endpoint")] pub mod interactions_endpoint; -pub mod secret_string; +pub mod secrets; #[cfg(feature = "utils")] pub mod utils; @@ -142,10 +142,7 @@ pub mod all { pub use crate::interactions_endpoint::*; #[cfg(feature = "utils")] #[doc(no_inline)] - pub use crate::utils::{ - token::{validate as validate_token, InvalidToken}, - *, - }; + pub use crate::utils::*; // #[doc(no_inline)] // pub use crate::*; #[doc(no_inline)] diff --git a/src/model/channel/attachment.rs b/src/model/channel/attachment.rs index 150799d19ba..ea113a8f9a7 100644 --- a/src/model/channel/attachment.rs +++ b/src/model/channel/attachment.rs @@ -94,54 +94,47 @@ impl Attachment { /// use std::io::Write; /// use std::path::Path; /// + /// # use serenity::http::Http; /// use serenity::model::prelude::*; /// use serenity::prelude::*; /// use tokio::fs::File; /// use tokio::io::AsyncWriteExt; /// - /// # struct Handler; - /// - /// #[serenity::async_trait] - /// # #[cfg(feature = "gateway")] - /// impl EventHandler for Handler { - /// async fn message(&self, context: Context, message: Message) { - /// for attachment in message.attachments { - /// let content = match attachment.download().await { - /// Ok(content) => content, - /// Err(why) => { - /// println!("Error downloading attachment: {:?}", why); - /// let _ = message - /// .channel_id - /// .say(&context.http, "Error downloading attachment") - /// .await; - /// - /// return; - /// }, - /// }; - /// - /// let mut file = match File::create(&attachment.filename).await { - /// Ok(file) => file, - /// Err(why) => { - /// println!("Error creating file: {:?}", why); - /// let _ = message.channel_id.say(&context.http, "Error creating file").await; - /// - /// return; - /// }, - /// }; - /// - /// if let Err(why) = file.write_all(&content).await { - /// println!("Error writing to file: {:?}", why); - /// - /// return; - /// } - /// - /// let _ = message - /// .channel_id - /// .say(&context.http, format!("Saved {:?}", attachment.filename)) - /// .await; - /// } + /// # async fn run() { + /// # let http: Http = unimplemented!(); + /// # let message: Message = unimplemented!(); + /// + /// for attachment in message.attachments { + /// let content = match attachment.download().await { + /// Ok(content) => content, + /// Err(why) => { + /// println!("Error downloading attachment: {:?}", why); + /// let _ = message.channel_id.say(&http, "Error downloading attachment").await; + /// + /// return; + /// }, + /// }; + /// + /// let mut file = match File::create(&attachment.filename).await { + /// Ok(file) => file, + /// Err(why) => { + /// println!("Error creating file: {:?}", why); + /// let _ = message.channel_id.say(&http, "Error creating file").await; + /// + /// return; + /// }, + /// }; + /// + /// if let Err(why) = file.write_all(&content).await { + /// println!("Error writing to file: {:?}", why); + /// + /// return; /// } + /// + /// let _ = message.channel_id.say(&http, format!("Saved {:?}", attachment.filename)).await; /// } + /// + /// # } /// ``` /// /// # Errors diff --git a/src/model/error.rs b/src/model/error.rs index 29192d178db..f97f4dad0b5 100644 --- a/src/model/error.rs +++ b/src/model/error.rs @@ -111,27 +111,28 @@ impl fmt::Display for Minimum { /// use serenity::prelude::*; /// use serenity::Error; /// -/// # struct Handler; +/// # #[cfg(feature = "http")] +/// # async fn run() -> Result<(), Box> { +/// # let http: serenity::http::Http = unimplemented!(); +/// # let guild_id: GuildId = unimplemented!(); +/// # let user: User = unimplemented!(); /// -/// # #[cfg(feature = "gateway")] -/// #[serenity::async_trait] -/// impl EventHandler for Handler { -/// async fn guild_ban_removal(&self, ctx: Context, guild_id: GuildId, user: User) { -/// match guild_id.ban(&ctx.http, user.id, 8, Some("No unbanning people!")).await { -/// Ok(()) => { -/// // Ban successful. -/// }, -/// Err(Error::Model(ModelError::TooLarge { -/// value, .. -/// })) => { -/// println!("Failed deleting {value} days' worth of messages"); -/// }, -/// Err(why) => { -/// println!("Unexpected error: {why:?}"); -/// }, -/// } -/// } +/// match guild_id.ban(&http, user.id, 8, Some("No unbanning people!")).await { +/// Ok(()) => { +/// // Ban successful. +/// }, +/// Err(Error::Model(ModelError::TooLarge { +/// value, .. +/// })) => { +/// println!("Failed deleting {value} days' worth of messages"); +/// }, +/// Err(why) => { +/// println!("Unexpected error: {why:?}"); +/// }, /// } +/// +/// # Ok(()) +/// # } /// ``` /// /// [`Error`]: crate::Error diff --git a/src/model/event.rs b/src/model/event.rs index e70997b76a7..afa7c4e5e00 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -10,13 +10,11 @@ use nonmax::NonMaxU64; use serde::de::Error as DeError; use serde::Serialize; use strum::{EnumCount, IntoStaticStr, VariantNames}; -#[cfg(feature = "gateway")] -use tracing::{debug, warn}; use crate::constants::Opcode; use crate::internal::utils::lending_for_each; use crate::model::prelude::*; -use crate::model::utils::{deserialize_val, remove_from_map}; +use crate::model::utils::remove_from_map; /// Requires no gateway intents. /// @@ -1333,20 +1331,4 @@ impl Event { pub fn name(&self) -> &'static str { self.into() } - - #[cfg(feature = "gateway")] - pub(crate) fn deserialize_and_log(map: JsonMap, original_str: &str) -> Result { - deserialize_val(Value::Object(map)).map_err(|err| { - let err_dbg = format!("{err:?}"); - if let Some((variant_name, _)) = - err_dbg.strip_prefix(r#"Error("unknown variant `"#).and_then(|s| s.split_once('`')) - { - debug!("Unknown event: {variant_name}"); - } else { - warn!("Err deserializing text: {err_dbg}"); - } - debug!("Failing text: {original_str}"); - Error::Json(err) - }) - } } diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index c1d061a51d9..4ae68ef6bf1 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -926,21 +926,22 @@ impl Guild { /// ```rust,no_run /// # use serenity::model::prelude::*; /// # use serenity::prelude::*; - /// # struct Handler; - /// - /// # #[cfg(all(feature = "cache", feature = "gateway"))] - /// #[serenity::async_trait] - /// impl EventHandler for Handler { - /// async fn message(&self, ctx: Context, msg: Message) { - /// if let Some(guild_id) = msg.guild_id { - /// if let Some(guild) = guild_id.to_guild_cached(&ctx.cache) { - /// if let Some(role) = guild.role_by_name("role_name") { - /// println!("{:?}", role); - /// } - /// } + /// + /// # #[cfg(feature = "cache")] + /// # async fn run() -> Result<(), Box> { + /// # let cache: serenity::cache::Cache = unimplemented!(); + /// # let msg: Message = unimplemented!(); + /// + /// if let Some(guild_id) = msg.guild_id { + /// if let Some(guild) = guild_id.to_guild_cached(&cache) { + /// if let Some(role) = guild.role_by_name("role_name") { + /// println!("{:?}", role); /// } /// } /// } + /// + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn role_by_name(&self, role_name: &str) -> Option<&Role> { diff --git a/src/model/guild/partial_guild.rs b/src/model/guild/partial_guild.rs index 5dbcf96971c..af805a43f9b 100644 --- a/src/model/guild/partial_guild.rs +++ b/src/model/guild/partial_guild.rs @@ -277,21 +277,22 @@ impl PartialGuild { /// ```rust,no_run /// # use serenity::model::prelude::*; /// # use serenity::prelude::*; - /// # struct Handler; /// - /// # #[cfg(all(feature = "cache", feature = "gateway"))] - /// #[serenity::async_trait] - /// impl EventHandler for Handler { - /// async fn message(&self, ctx: Context, msg: Message) { - /// if let Some(guild_id) = msg.guild_id { - /// if let Some(guild) = guild_id.to_guild_cached(&ctx.cache) { - /// if let Some(role) = guild.role_by_name("role_name") { - /// println!("Obtained role's reference: {:?}", role); - /// } - /// } + /// # #[cfg(feature = "cache")] + /// # async fn run() -> Result<(), Box> { + /// # let cache: serenity::cache::Cache = unimplemented!(); + /// # let msg: Message = unimplemented!(); + /// + /// if let Some(guild_id) = msg.guild_id { + /// if let Some(guild) = guild_id.to_guild_cached(&cache) { + /// if let Some(role) = guild.role_by_name("role_name") { + /// println!("Obtained role's reference: {:?}", role); /// } /// } /// } + /// + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn role_by_name(&self, role_name: &str) -> Option<&Role> { diff --git a/src/model/user.rs b/src/model/user.rs index ef7f989afe7..e9b464e1e8f 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -482,18 +482,18 @@ impl User { /// ```rust,no_run /// # use serenity::prelude::*; /// # use serenity::model::prelude::*; - /// # struct Handler; - /// - /// #[serenity::async_trait] - /// # #[cfg(feature = "gateway")] - /// impl EventHandler for Handler { - /// async fn message(&self, context: Context, msg: Message) { - /// if msg.content == "!mytag" { - /// let content = format!("Your tag is: {}", msg.author.tag()); - /// let _ = msg.channel_id.say(&context.http, &content).await; - /// } - /// } + /// # use serenity::http::Http; + /// # async fn run() -> Result<(), Box> { + /// # let http: Http = unimplemented!(); + /// # let msg: Message = unimplemented!(); + /// + /// if msg.content == "!mytag" { + /// let content = format!("Your tag is: {}", msg.author.tag()); + /// let _ = msg.channel_id.say(&http, &content).await; /// } + /// + /// # Ok(()) + /// # } /// ``` #[must_use] pub fn tag(&self) -> String { @@ -589,23 +589,24 @@ impl UserId { /// ```rust,no_run /// # use serenity::prelude::*; /// # use serenity::model::prelude::*; - /// # struct Handler; + /// # use serenity::http::Http; /// use serenity::builder::CreateMessage; /// - /// #[serenity::async_trait] - /// # #[cfg(feature = "gateway")] - /// impl EventHandler for Handler { - /// async fn message(&self, ctx: Context, msg: Message) { - /// if msg.content == "~help" { - /// let builder = CreateMessage::new().content("Helpful info here."); - /// - /// if let Err(why) = msg.author.id.direct_message(&ctx.http, builder).await { - /// println!("Err sending help: {why:?}"); - /// let _ = msg.reply(&ctx.http, "There was an error DMing you help.").await; - /// }; - /// } - /// } + /// # async fn run() -> Result<(), Box> { + /// # let http: Http = unimplemented!(); + /// # let msg: Message = unimplemented!(); + /// + /// if msg.content == "~help" { + /// let builder = CreateMessage::new().content("Helpful info here."); + /// + /// if let Err(why) = msg.author.id.direct_message(&http, builder).await { + /// println!("Err sending help: {why:?}"); + /// let _ = msg.reply(&http, "There was an error DMing you help.").await; + /// }; /// } + /// + /// # Ok(()) + /// # } /// ``` /// /// # Errors diff --git a/src/prelude.rs b/src/prelude.rs index c8811c5748b..42aa5a70c89 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -27,3 +27,4 @@ pub use crate::http::HttpError; pub use crate::model::mention::Mentionable; #[cfg(feature = "model")] pub use crate::model::{gateway::GatewayIntents, ModelError}; +pub use crate::secrets::Token; diff --git a/src/secret_string.rs b/src/secret_string.rs deleted file mode 100644 index 9e6be5653f0..00000000000 --- a/src/secret_string.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::sync::Arc; - -/// A cheaply clonable, zeroed on drop, String. -/// -/// This is a simple newtype of `Arc` that uses [`zeroize::Zeroize`] on last drop to avoid -/// keeping it around in memory. -#[derive(Clone, serde::Deserialize, serde::Serialize)] -pub struct SecretString(Arc); - -impl SecretString { - #[must_use] - pub fn new(inner: Arc) -> Self { - Self(inner) - } - - #[must_use] - pub fn expose_secret(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Debug for SecretString { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - fmt.debug_tuple(std::any::type_name::()).field(&"").finish() - } -} - -impl zeroize::Zeroize for SecretString { - fn zeroize(&mut self) { - if let Some(string) = Arc::get_mut(&mut self.0) { - string.zeroize(); - } - } -} - -#[cfg(feature = "typesize")] -impl typesize::TypeSize for SecretString { - fn extra_size(&self) -> usize { - self.0.len() + (size_of::() * 2) - } -} diff --git a/src/secrets.rs b/src/secrets.rs new file mode 100644 index 00000000000..e7f7fac8d9a --- /dev/null +++ b/src/secrets.rs @@ -0,0 +1,145 @@ +use std::env::{self, VarError}; +use std::ffi::OsStr; +use std::fmt; +use std::str::FromStr; +use std::sync::Arc; + +use aformat::{aformat, CapStr}; + +/// A cheaply clonable, zeroed on drop, String. +/// +/// This is a simple newtype of `Arc` that uses [`zeroize::Zeroize`] on last drop to avoid +/// keeping it around in memory. +#[derive(Clone, Deserialize, Serialize)] +pub struct SecretString(Arc); + +impl SecretString { + #[must_use] + pub fn new(inner: Arc) -> Self { + Self(inner) + } + + #[must_use] + pub fn expose_secret(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Debug for SecretString { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt.debug_tuple(std::any::type_name::()).field(&"").finish() + } +} + +impl zeroize::Zeroize for SecretString { + fn zeroize(&mut self) { + if let Some(string) = Arc::get_mut(&mut self.0) { + string.zeroize(); + } + } +} + +#[cfg(feature = "typesize")] +impl typesize::TypeSize for SecretString { + fn extra_size(&self) -> usize { + self.0.len() + (size_of::() * 2) + } +} + +/// A type for securely storing and passing around a Discord token. +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Token(SecretString); + +impl Token { + /// Fetch and parses the token out of the given environment variable. + /// + /// # Errors + /// + /// Returns [`TokenError::Env`] if fetching the variable fails (see [`std::env::var`] for + /// details). May also return [`TokenError::InvalidToken`] if the token is in an invalid + /// format (see [`Token::from_str`]). + pub fn from_env>(key: K) -> Result { + env::var(key).map_err(TokenError::Env).and_then(|token| token.parse()) + } + + #[must_use] + pub fn expose_secret(&self) -> &str { + self.0.expose_secret() + } +} + +/// Parses a token and validates that is is likely in a valid format. +/// +/// This performs the following checks on a given token: +/// - Is not empty; +/// - Is optionally prefixed with `"Bot "` or `"Bearer "`; +/// - Contains 3 parts (split by the period char `'.'`); +/// +/// Note that a token prefixed with `"Bearer "` will have its prefix changed to `"Bot "` when +/// parsed. +/// +/// # Examples +/// +/// Validate that a token is valid and that a number of malformed tokens are actually invalid: +/// +/// ``` +/// use serenity::secrets::Token; +/// +/// // ensure a valid token is in fact a valid format: +/// assert!("Mjg4NzYwMjQxMzYzODc3ODg4.C_ikow.j3VupLBuE1QWZng3TMGH0z_UAwg".parse::().is_ok()); +/// +/// assert!("Mjg4NzYwMjQxMzYzODc3ODg4".parse::().is_err()); +/// assert!("".parse::().is_err()); +/// ``` +/// +/// # Errors +/// +/// Returns a [`TokenError::InvalidToken`] when one of the above checks fail. The type of failure is +/// not specified. +impl FromStr for Token { + type Err = TokenError; + + fn from_str(s: &str) -> Result { + let token = s.trim().trim_start_matches("Bot ").trim_start_matches("Bearer "); + + let mut parts = token.split('.'); + let is_valid = parts.next().is_some_and(|p| !p.is_empty()) + && parts.next().is_some_and(|p| !p.is_empty()) + && parts.next().is_some_and(|p| !p.is_empty()) + && parts.next().is_none(); + + if is_valid { + Ok(Self(SecretString::new(Arc::from( + aformat!("Box {}", CapStr::<128>(token)).as_str(), + )))) + } else { + Err(TokenError::InvalidToken) + } + } +} + +/// Error that can be returned by [`Token::from_str`] or [`Token::from_env`]. +#[derive(Debug)] +pub enum TokenError { + Env(VarError), + InvalidToken, +} + +impl std::error::Error for TokenError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Env(inner) => Some(inner), + Self::InvalidToken => None, + } + } +} + +impl fmt::Display for TokenError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Env(inner) => fmt::Display::fmt(&inner, f), + Self::InvalidToken => f.write_str("The provided token was invalid"), + } + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 197941b4cc2..7d4e2e15460 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -11,8 +11,6 @@ mod message_builder; #[cfg(feature = "collector")] mod quick_modal; -pub mod token; - use std::num::NonZeroU16; #[cfg(feature = "gateway")] @@ -26,8 +24,6 @@ use url::Url; pub use self::custom_message::CustomMessage; pub use self::message_builder::{Content, ContentModifier, EmbedMessageBuilding, MessageBuilder}; -#[doc(inline)] -pub use self::token::validate as validate_token; use crate::model::prelude::*; /// Retrieves the "code" part of an invite out of a URL. diff --git a/src/utils/token.rs b/src/utils/token.rs deleted file mode 100644 index 6059ebe9296..00000000000 --- a/src/utils/token.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Utilities to parse and validate Discord tokens. - -use std::{fmt, str}; - -/// Validates that a token is likely in a valid format. -/// -/// This performs the following checks on a given token: -/// - Is not empty; -/// - Contains 3 parts (split by the period char `'.'`); -/// - The second part of the token is at least 6 characters long; -/// -/// # Examples -/// -/// Validate that a token is valid and that a number of malformed tokens are actually invalid: -/// -/// ``` -/// use serenity::utils::token::validate; -/// -/// // ensure a valid token is in fact a valid format: -/// assert!(validate("Mjg4NzYwMjQxMzYzODc3ODg4.C_ikow.j3VupLBuE1QWZng3TMGH0z_UAwg").is_ok()); -/// -/// assert!(validate("Mjg4NzYwMjQxMzYzODc3ODg4").is_err()); -/// assert!(validate("").is_err()); -/// ``` -/// -/// # Errors -/// -/// Returns a [`InvalidToken`] when one of the above checks fail. The type of failure is not -/// specified. -pub fn validate(token: &str) -> Result<(), InvalidToken> { - // Tokens can be preceded by "Bot " (that's how the Discord API expects them) - let mut parts = token.trim_start_matches("Bot ").split('.'); - - let is_valid = parts.next().is_some_and(|p| !p.is_empty()) - && parts.next().is_some_and(|p| !p.is_empty()) - && parts.next().is_some_and(|p| !p.is_empty()) - && parts.next().is_none(); - - if is_valid { - Ok(()) - } else { - Err(InvalidToken) - } -} - -/// Error that can be return by [`validate`]. -#[derive(Debug)] -pub struct InvalidToken; - -impl std::error::Error for InvalidToken {} - -impl fmt::Display for InvalidToken { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("The provided token was invalid") - } -}