diff --git a/Cargo.toml b/Cargo.toml index 7acbd87a9e4..769ae37cb09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ edition.workspace = true rust-version.workspace = true [workspace] -members = ["examples/*"] +members = ["examples/*", "serenity-core"] [workspace.package] documentation = "https://docs.rs/serenity" @@ -66,6 +66,7 @@ typesize = { version = "0.1.6", optional = true, features = ["url", "time", "ser # serde feature only allows for serialisation, # Serenity workspace crates serenity-voice-model = { version = "0.2.0", path = "./voice-model", optional = true } +serenity-core = { path = "./serenity-core" } [dev-dependencies.http_crate] version = "1.1.0" @@ -131,7 +132,7 @@ full = ["default", "collector", "voice", "voice_model", "interactions_endpoint"] # Enables temporary caching in functions that retrieve data via the HTTP API. temp_cache = ["cache", "mini-moka", "typesize?/mini_moka"] -typesize = ["dep:typesize", "dashmap/typesize", "small-fixed-array/typesize", "bool_to_bitflags/typesize"] +typesize = ["dep:typesize", "serenity-core/typesize", "dashmap/typesize", "small-fixed-array/typesize", "bool_to_bitflags/typesize"] # Enables compile-time heavy instrument macros from tracing tracing_instrument = ["tracing/attributes"] diff --git a/examples/testing/src/model_type_sizes.rs b/examples/testing/src/model_type_sizes.rs index b4527230d6a..f88e8ceba68 100644 --- a/examples/testing/src/model_type_sizes.rs +++ b/examples/testing/src/model_type_sizes.rs @@ -1,3 +1,4 @@ +use serenity::gateway::GatewayEvent; use serenity::model::prelude::*; pub fn print_ranking() { diff --git a/serenity-core/Cargo.toml b/serenity-core/Cargo.toml new file mode 100644 index 00000000000..ab8d3ed5605 --- /dev/null +++ b/serenity-core/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "serenity-core" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitflags = "2.4.2" +base64 = { version = "0.22.0" } +serde_json = "1.0.108" +serde = { version = "1.0.192", features = ["derive", "rc"] } +nonmax = { version = "0.5.5", features = ["serde"] } +to-arraystring = "0.2.0" +tokio = { version = "1.34.0", features = ["macros", "rt", "sync", "time", "io-util"] } +zeroize = { version = "1.7" } # Not used in serenity, but bumps the minimal version from secrecy +strum = { version = "0.26", features = ["derive"] } +fxhash = { version = "0.2.1", optional = true } +chrono = { version = "0.4.31", default-features = false, features = ["clock", "serde"], optional = true } +dashmap = { version = "6.1.0", features = ["serde"], optional = true } +mime_guess = { version = "2.0.4", optional = true } +percent-encoding = { version = "2.3.0", optional = true } +small-fixed-array = { version = "0.4", features = ["serde"] } +bool_to_bitflags = { version = "0.1.2" } +futures = { version = "0.3.29", default-features = false, features = ["std"] } +reqwest = { version = "0.12.2", default-features = false, features = ["multipart", "stream", "json"], optional = true } +tokio-tungstenite = { version = "0.21.0", optional = true } +typesize = { version = "0.1.6", optional = true, features = ["url", "time", "serde_json", "secrecy", "parking_lot", "nonmax", "extract_map_01"] } +extract_map = { version = "0.1.0", features = ["serde", "iter_mut"] } +serde_cow = { version = "0.1.0" } +aformat = "0.1.3" +arrayvec = { version = "0.7.4", features = ["serde"] } +url = { version = "2.4.1", features = ["serde"] } +bytes = "1.5.0" +parking_lot = { version = "0.12.1"} +mini-moka = { version = "0.10.2", optional = true } +tracing = { version = "0.1.40", features = ["log"] } +serenity-voice-model = { version = "0.2.0", path = "../voice-model", optional = true } + +[features] +default = ["cache", "chrono", "model", "rustls_backend"] + +builder = ["tokio/fs"] +cache = ["fxhash", "dashmap"] +http = ["dashmap", "mime_guess", "percent-encoding"] +model = ["builder", "http", "utils"] +utils = [] +chrono = ["dep:chrono", "typesize?/chrono"] +typesize = ["dep:typesize", "dashmap/typesize", "small-fixed-array/typesize", "bool_to_bitflags/typesize"] +temp_cache = ["cache", "mini-moka", "typesize?/mini_moka"] +unstable = [] +tracing_instrument = ["tracing/attributes"] +tokio_task_builder = ["tokio/tracing"] +voice_model = ["serenity-voice-model"] + +rustls_backend = [ + "reqwest/rustls-tls", + "tokio-tungstenite/rustls-tls-webpki-roots", +] + +native_tls_backend = [ + "reqwest/native-tls", + "tokio-tungstenite/native-tls", +] diff --git a/serenity-core/build.rs b/serenity-core/build.rs new file mode 100644 index 00000000000..0b97e08c250 --- /dev/null +++ b/serenity-core/build.rs @@ -0,0 +1,13 @@ +#[cfg(all(feature = "http", not(any(feature = "rustls_backend", feature = "native_tls_backend"))))] +compile_error!( + "You have the `http` feature enabled; either the `rustls_backend` or `native_tls_backend` \ + feature must be enabled to let Serenity make requests over the network.\n\ + - `rustls_backend` uses Rustls, a pure Rust TLS-implemenation.\n\ + - `native_tls_backend` uses SChannel on Windows, Secure Transport on macOS, and OpenSSL on \ + other platforms.\n\ + If you are unsure, go with `rustls_backend`." +); + +fn main() { + println!("cargo:rustc-check-cfg=cfg(tokio_unstable, ignore_serenity_deprecated)"); +} diff --git a/src/builder/add_member.rs b/serenity-core/src/builder/add_member.rs similarity index 100% rename from src/builder/add_member.rs rename to serenity-core/src/builder/add_member.rs diff --git a/src/builder/bot_auth_parameters.rs b/serenity-core/src/builder/bot_auth_parameters.rs similarity index 100% rename from src/builder/bot_auth_parameters.rs rename to serenity-core/src/builder/bot_auth_parameters.rs diff --git a/src/builder/create_allowed_mentions.rs b/serenity-core/src/builder/create_allowed_mentions.rs similarity index 100% rename from src/builder/create_allowed_mentions.rs rename to serenity-core/src/builder/create_allowed_mentions.rs diff --git a/src/builder/create_attachment.rs b/serenity-core/src/builder/create_attachment.rs similarity index 100% rename from src/builder/create_attachment.rs rename to serenity-core/src/builder/create_attachment.rs diff --git a/src/builder/create_channel.rs b/serenity-core/src/builder/create_channel.rs similarity index 100% rename from src/builder/create_channel.rs rename to serenity-core/src/builder/create_channel.rs diff --git a/src/builder/create_command.rs b/serenity-core/src/builder/create_command.rs similarity index 100% rename from src/builder/create_command.rs rename to serenity-core/src/builder/create_command.rs diff --git a/src/builder/create_command_permission.rs b/serenity-core/src/builder/create_command_permission.rs similarity index 100% rename from src/builder/create_command_permission.rs rename to serenity-core/src/builder/create_command_permission.rs diff --git a/src/builder/create_components.rs b/serenity-core/src/builder/create_components.rs similarity index 100% rename from src/builder/create_components.rs rename to serenity-core/src/builder/create_components.rs diff --git a/src/builder/create_embed.rs b/serenity-core/src/builder/create_embed.rs similarity index 100% rename from src/builder/create_embed.rs rename to serenity-core/src/builder/create_embed.rs diff --git a/src/builder/create_forum_post.rs b/serenity-core/src/builder/create_forum_post.rs similarity index 100% rename from src/builder/create_forum_post.rs rename to serenity-core/src/builder/create_forum_post.rs diff --git a/src/builder/create_forum_tag.rs b/serenity-core/src/builder/create_forum_tag.rs similarity index 100% rename from src/builder/create_forum_tag.rs rename to serenity-core/src/builder/create_forum_tag.rs diff --git a/src/builder/create_interaction_response.rs b/serenity-core/src/builder/create_interaction_response.rs similarity index 100% rename from src/builder/create_interaction_response.rs rename to serenity-core/src/builder/create_interaction_response.rs diff --git a/src/builder/create_interaction_response_followup.rs b/serenity-core/src/builder/create_interaction_response_followup.rs similarity index 100% rename from src/builder/create_interaction_response_followup.rs rename to serenity-core/src/builder/create_interaction_response_followup.rs diff --git a/src/builder/create_invite.rs b/serenity-core/src/builder/create_invite.rs similarity index 100% rename from src/builder/create_invite.rs rename to serenity-core/src/builder/create_invite.rs diff --git a/src/builder/create_message.rs b/serenity-core/src/builder/create_message.rs similarity index 100% rename from src/builder/create_message.rs rename to serenity-core/src/builder/create_message.rs diff --git a/src/builder/create_poll.rs b/serenity-core/src/builder/create_poll.rs similarity index 100% rename from src/builder/create_poll.rs rename to serenity-core/src/builder/create_poll.rs diff --git a/src/builder/create_scheduled_event.rs b/serenity-core/src/builder/create_scheduled_event.rs similarity index 100% rename from src/builder/create_scheduled_event.rs rename to serenity-core/src/builder/create_scheduled_event.rs diff --git a/src/builder/create_stage_instance.rs b/serenity-core/src/builder/create_stage_instance.rs similarity index 100% rename from src/builder/create_stage_instance.rs rename to serenity-core/src/builder/create_stage_instance.rs diff --git a/src/builder/create_sticker.rs b/serenity-core/src/builder/create_sticker.rs similarity index 100% rename from src/builder/create_sticker.rs rename to serenity-core/src/builder/create_sticker.rs diff --git a/src/builder/create_thread.rs b/serenity-core/src/builder/create_thread.rs similarity index 100% rename from src/builder/create_thread.rs rename to serenity-core/src/builder/create_thread.rs diff --git a/src/builder/create_webhook.rs b/serenity-core/src/builder/create_webhook.rs similarity index 100% rename from src/builder/create_webhook.rs rename to serenity-core/src/builder/create_webhook.rs diff --git a/src/builder/edit_automod_rule.rs b/serenity-core/src/builder/edit_automod_rule.rs similarity index 100% rename from src/builder/edit_automod_rule.rs rename to serenity-core/src/builder/edit_automod_rule.rs diff --git a/src/builder/edit_channel.rs b/serenity-core/src/builder/edit_channel.rs similarity index 100% rename from src/builder/edit_channel.rs rename to serenity-core/src/builder/edit_channel.rs diff --git a/src/builder/edit_command.rs b/serenity-core/src/builder/edit_command.rs similarity index 100% rename from src/builder/edit_command.rs rename to serenity-core/src/builder/edit_command.rs diff --git a/src/builder/edit_guild.rs b/serenity-core/src/builder/edit_guild.rs similarity index 100% rename from src/builder/edit_guild.rs rename to serenity-core/src/builder/edit_guild.rs diff --git a/src/builder/edit_guild_welcome_screen.rs b/serenity-core/src/builder/edit_guild_welcome_screen.rs similarity index 100% rename from src/builder/edit_guild_welcome_screen.rs rename to serenity-core/src/builder/edit_guild_welcome_screen.rs diff --git a/src/builder/edit_guild_widget.rs b/serenity-core/src/builder/edit_guild_widget.rs similarity index 100% rename from src/builder/edit_guild_widget.rs rename to serenity-core/src/builder/edit_guild_widget.rs diff --git a/src/builder/edit_interaction_response.rs b/serenity-core/src/builder/edit_interaction_response.rs similarity index 100% rename from src/builder/edit_interaction_response.rs rename to serenity-core/src/builder/edit_interaction_response.rs diff --git a/src/builder/edit_member.rs b/serenity-core/src/builder/edit_member.rs similarity index 100% rename from src/builder/edit_member.rs rename to serenity-core/src/builder/edit_member.rs diff --git a/src/builder/edit_message.rs b/serenity-core/src/builder/edit_message.rs similarity index 100% rename from src/builder/edit_message.rs rename to serenity-core/src/builder/edit_message.rs diff --git a/src/builder/edit_profile.rs b/serenity-core/src/builder/edit_profile.rs similarity index 100% rename from src/builder/edit_profile.rs rename to serenity-core/src/builder/edit_profile.rs diff --git a/src/builder/edit_role.rs b/serenity-core/src/builder/edit_role.rs similarity index 100% rename from src/builder/edit_role.rs rename to serenity-core/src/builder/edit_role.rs diff --git a/src/builder/edit_scheduled_event.rs b/serenity-core/src/builder/edit_scheduled_event.rs similarity index 100% rename from src/builder/edit_scheduled_event.rs rename to serenity-core/src/builder/edit_scheduled_event.rs diff --git a/src/builder/edit_stage_instance.rs b/serenity-core/src/builder/edit_stage_instance.rs similarity index 100% rename from src/builder/edit_stage_instance.rs rename to serenity-core/src/builder/edit_stage_instance.rs diff --git a/src/builder/edit_sticker.rs b/serenity-core/src/builder/edit_sticker.rs similarity index 100% rename from src/builder/edit_sticker.rs rename to serenity-core/src/builder/edit_sticker.rs diff --git a/src/builder/edit_thread.rs b/serenity-core/src/builder/edit_thread.rs similarity index 100% rename from src/builder/edit_thread.rs rename to serenity-core/src/builder/edit_thread.rs diff --git a/src/builder/edit_voice_state.rs b/serenity-core/src/builder/edit_voice_state.rs similarity index 100% rename from src/builder/edit_voice_state.rs rename to serenity-core/src/builder/edit_voice_state.rs diff --git a/src/builder/edit_webhook.rs b/serenity-core/src/builder/edit_webhook.rs similarity index 100% rename from src/builder/edit_webhook.rs rename to serenity-core/src/builder/edit_webhook.rs diff --git a/src/builder/edit_webhook_message.rs b/serenity-core/src/builder/edit_webhook_message.rs similarity index 100% rename from src/builder/edit_webhook_message.rs rename to serenity-core/src/builder/edit_webhook_message.rs diff --git a/src/builder/execute_webhook.rs b/serenity-core/src/builder/execute_webhook.rs similarity index 100% rename from src/builder/execute_webhook.rs rename to serenity-core/src/builder/execute_webhook.rs diff --git a/src/builder/get_entitlements.rs b/serenity-core/src/builder/get_entitlements.rs similarity index 100% rename from src/builder/get_entitlements.rs rename to serenity-core/src/builder/get_entitlements.rs diff --git a/src/builder/get_messages.rs b/serenity-core/src/builder/get_messages.rs similarity index 100% rename from src/builder/get_messages.rs rename to serenity-core/src/builder/get_messages.rs diff --git a/src/builder/mod.rs b/serenity-core/src/builder/mod.rs similarity index 100% rename from src/builder/mod.rs rename to serenity-core/src/builder/mod.rs diff --git a/src/cache/cache_update.rs b/serenity-core/src/cache/cache_update.rs similarity index 100% rename from src/cache/cache_update.rs rename to serenity-core/src/cache/cache_update.rs diff --git a/src/cache/event.rs b/serenity-core/src/cache/event.rs similarity index 100% rename from src/cache/event.rs rename to serenity-core/src/cache/event.rs diff --git a/src/cache/mod.rs b/serenity-core/src/cache/mod.rs similarity index 100% rename from src/cache/mod.rs rename to serenity-core/src/cache/mod.rs diff --git a/src/cache/settings.rs b/serenity-core/src/cache/settings.rs similarity index 100% rename from src/cache/settings.rs rename to serenity-core/src/cache/settings.rs diff --git a/src/cache/wrappers.rs b/serenity-core/src/cache/wrappers.rs similarity index 100% rename from src/cache/wrappers.rs rename to serenity-core/src/cache/wrappers.rs diff --git a/serenity-core/src/constants.rs b/serenity-core/src/constants.rs new file mode 100644 index 00000000000..ba15a0e433d --- /dev/null +++ b/serenity-core/src/constants.rs @@ -0,0 +1,30 @@ +//! A set of constants used by the library. + +use nonmax::NonMaxU16; + +/// The maximum length of the textual size of an embed. +pub const EMBED_MAX_LENGTH: usize = 6000; + +/// The maximum number of embeds in a message. +pub const EMBED_MAX_COUNT: usize = 10; + +/// The maximum number of stickers in a message. +pub const STICKER_MAX_COUNT: usize = 3; + +/// The maximum unicode code points allowed within a message by Discord. +pub const MESSAGE_CODE_LIMIT: usize = 2000; + +/// The maximum number of members the bot can fetch at once +pub const MEMBER_FETCH_LIMIT: NonMaxU16 = match NonMaxU16::new(1000) { + Some(m) => m, + None => unreachable!(), +}; + +/// The [UserAgent] sent along with every request. +/// +/// [UserAgent]: ::reqwest::header::USER_AGENT +pub const USER_AGENT: &str = concat!( + "DiscordBot (https://github.com/serenity-rs/serenity, ", + env!("CARGO_PKG_VERSION"), + ")" +); diff --git a/serenity-core/src/error.rs b/serenity-core/src/error.rs new file mode 100644 index 00000000000..f911360ac9d --- /dev/null +++ b/serenity-core/src/error.rs @@ -0,0 +1,114 @@ +use std::error::Error as StdError; +use std::fmt; +use std::io::Error as IoError; + +#[cfg(feature = "http")] +use reqwest::{header::InvalidHeaderValue, Error as ReqwestError}; + +#[cfg(feature = "http")] +use crate::http::HttpError; +use crate::internal::prelude::*; +use crate::model::ModelError; +use crate::secrets::TokenError; + +/// The common result type between most library functions. +/// +/// The library exposes functions which, for a result type, exposes only one type, rather than the +/// usual 2 (`Result`). This is because all functions that return a result return +/// serenity's [`Error`], so this is implied, and a "simpler" result is used. +pub type Result = StdResult; + +/// A common error enum returned by most of the library's functionality within a custom [`Result`]. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// An [`std::io`] error. + Io(IoError), + /// An error from the [`serde_json`] crate. + Json(serde_json::Error), + /// An error from the [`model`] module. + /// + /// [`model`]: crate::model + Model(ModelError), + /// An error from the [`http`] module. + /// + /// [`http`]: crate::http + #[cfg(feature = "http")] + Http(HttpError), + /// An error from the [`secrets`] module. + /// + /// [`secrets`]: crate::secrets + Token(TokenError), +} + +impl From for Error { + fn from(e: IoError) -> Error { + Error::Io(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Error { + Error::Json(e) + } +} + +impl From for Error { + fn from(e: ModelError) -> Error { + Error::Model(e) + } +} + +#[cfg(feature = "http")] +impl From for Error { + fn from(e: HttpError) -> Error { + Error::Http(e) + } +} + +impl From for Error { + fn from(e: TokenError) -> Error { + Error::Token(e) + } +} + +#[cfg(feature = "http")] +impl From for Error { + fn from(e: InvalidHeaderValue) -> Error { + HttpError::InvalidHeader(e).into() + } +} + +#[cfg(feature = "http")] +impl From for Error { + fn from(e: ReqwestError) -> Error { + HttpError::Request(e).into() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(inner) => fmt::Display::fmt(&inner, f), + Self::Json(inner) => fmt::Display::fmt(&inner, f), + Self::Model(inner) => fmt::Display::fmt(&inner, f), + #[cfg(feature = "http")] + Self::Http(inner) => fmt::Display::fmt(&inner, f), + Self::Token(inner) => fmt::Display::fmt(&inner, f), + } + } +} + +impl StdError for Error { + #[cfg_attr(feature = "tracing_instrument", instrument)] + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + Self::Io(inner) => Some(inner), + Self::Json(inner) => Some(inner), + Self::Model(inner) => Some(inner), + #[cfg(feature = "http")] + Self::Http(inner) => Some(inner), + Self::Token(inner) => Some(inner), + } + } +} diff --git a/src/http/client.rs b/serenity-core/src/http/client.rs similarity index 100% rename from src/http/client.rs rename to serenity-core/src/http/client.rs diff --git a/src/http/error.rs b/serenity-core/src/http/error.rs similarity index 100% rename from src/http/error.rs rename to serenity-core/src/http/error.rs diff --git a/src/http/mod.rs b/serenity-core/src/http/mod.rs similarity index 100% rename from src/http/mod.rs rename to serenity-core/src/http/mod.rs diff --git a/src/http/multipart.rs b/serenity-core/src/http/multipart.rs similarity index 100% rename from src/http/multipart.rs rename to serenity-core/src/http/multipart.rs diff --git a/src/http/ratelimiting.rs b/serenity-core/src/http/ratelimiting.rs similarity index 100% rename from src/http/ratelimiting.rs rename to serenity-core/src/http/ratelimiting.rs diff --git a/src/http/request.rs b/serenity-core/src/http/request.rs similarity index 100% rename from src/http/request.rs rename to serenity-core/src/http/request.rs diff --git a/src/http/routing.rs b/serenity-core/src/http/routing.rs similarity index 100% rename from src/http/routing.rs rename to serenity-core/src/http/routing.rs diff --git a/src/http/typing.rs b/serenity-core/src/http/typing.rs similarity index 100% rename from src/http/typing.rs rename to serenity-core/src/http/typing.rs diff --git a/src/internal/macros.rs b/serenity-core/src/internal/macros.rs similarity index 100% rename from src/internal/macros.rs rename to serenity-core/src/internal/macros.rs diff --git a/src/internal/mod.rs b/serenity-core/src/internal/mod.rs similarity index 100% rename from src/internal/mod.rs rename to serenity-core/src/internal/mod.rs diff --git a/src/internal/prelude.rs b/serenity-core/src/internal/prelude.rs similarity index 100% rename from src/internal/prelude.rs rename to serenity-core/src/internal/prelude.rs diff --git a/src/internal/tokio.rs b/serenity-core/src/internal/tokio.rs similarity index 100% rename from src/internal/tokio.rs rename to serenity-core/src/internal/tokio.rs diff --git a/src/internal/utils.rs b/serenity-core/src/internal/utils.rs similarity index 100% rename from src/internal/utils.rs rename to serenity-core/src/internal/utils.rs diff --git a/serenity-core/src/lib.rs b/serenity-core/src/lib.rs new file mode 100644 index 00000000000..e4615efb697 --- /dev/null +++ b/serenity-core/src/lib.rs @@ -0,0 +1,15 @@ +#[macro_use] +extern crate serde; + +#[macro_use] +mod internal; + +mod constants; +pub mod secrets; + +pub mod builder; +pub mod cache; +pub mod error; +pub mod http; +pub mod model; +pub mod utils; diff --git a/src/model/application/command.rs b/serenity-core/src/model/application/command.rs similarity index 100% rename from src/model/application/command.rs rename to serenity-core/src/model/application/command.rs diff --git a/src/model/application/command_interaction.rs b/serenity-core/src/model/application/command_interaction.rs similarity index 100% rename from src/model/application/command_interaction.rs rename to serenity-core/src/model/application/command_interaction.rs diff --git a/src/model/application/component.rs b/serenity-core/src/model/application/component.rs similarity index 100% rename from src/model/application/component.rs rename to serenity-core/src/model/application/component.rs diff --git a/src/model/application/component_interaction.rs b/serenity-core/src/model/application/component_interaction.rs similarity index 100% rename from src/model/application/component_interaction.rs rename to serenity-core/src/model/application/component_interaction.rs diff --git a/src/model/application/interaction.rs b/serenity-core/src/model/application/interaction.rs similarity index 100% rename from src/model/application/interaction.rs rename to serenity-core/src/model/application/interaction.rs diff --git a/src/model/application/mod.rs b/serenity-core/src/model/application/mod.rs similarity index 100% rename from src/model/application/mod.rs rename to serenity-core/src/model/application/mod.rs diff --git a/src/model/application/modal_interaction.rs b/serenity-core/src/model/application/modal_interaction.rs similarity index 100% rename from src/model/application/modal_interaction.rs rename to serenity-core/src/model/application/modal_interaction.rs diff --git a/src/model/application/oauth.rs b/serenity-core/src/model/application/oauth.rs similarity index 100% rename from src/model/application/oauth.rs rename to serenity-core/src/model/application/oauth.rs diff --git a/src/model/application/ping_interaction.rs b/serenity-core/src/model/application/ping_interaction.rs similarity index 100% rename from src/model/application/ping_interaction.rs rename to serenity-core/src/model/application/ping_interaction.rs diff --git a/src/model/channel/attachment.rs b/serenity-core/src/model/channel/attachment.rs similarity index 100% rename from src/model/channel/attachment.rs rename to serenity-core/src/model/channel/attachment.rs diff --git a/src/model/channel/channel_id.rs b/serenity-core/src/model/channel/channel_id.rs similarity index 100% rename from src/model/channel/channel_id.rs rename to serenity-core/src/model/channel/channel_id.rs diff --git a/src/model/channel/embed.rs b/serenity-core/src/model/channel/embed.rs similarity index 100% rename from src/model/channel/embed.rs rename to serenity-core/src/model/channel/embed.rs diff --git a/src/model/channel/guild_channel.rs b/serenity-core/src/model/channel/guild_channel.rs similarity index 100% rename from src/model/channel/guild_channel.rs rename to serenity-core/src/model/channel/guild_channel.rs diff --git a/src/model/channel/message.rs b/serenity-core/src/model/channel/message.rs similarity index 100% rename from src/model/channel/message.rs rename to serenity-core/src/model/channel/message.rs diff --git a/src/model/channel/mod.rs b/serenity-core/src/model/channel/mod.rs similarity index 100% rename from src/model/channel/mod.rs rename to serenity-core/src/model/channel/mod.rs diff --git a/src/model/channel/partial_channel.rs b/serenity-core/src/model/channel/partial_channel.rs similarity index 100% rename from src/model/channel/partial_channel.rs rename to serenity-core/src/model/channel/partial_channel.rs diff --git a/src/model/channel/private_channel.rs b/serenity-core/src/model/channel/private_channel.rs similarity index 100% rename from src/model/channel/private_channel.rs rename to serenity-core/src/model/channel/private_channel.rs diff --git a/src/model/channel/reaction.rs b/serenity-core/src/model/channel/reaction.rs similarity index 100% rename from src/model/channel/reaction.rs rename to serenity-core/src/model/channel/reaction.rs diff --git a/src/model/colour.rs b/serenity-core/src/model/colour.rs similarity index 100% rename from src/model/colour.rs rename to serenity-core/src/model/colour.rs diff --git a/src/model/connection.rs b/serenity-core/src/model/connection.rs similarity index 100% rename from src/model/connection.rs rename to serenity-core/src/model/connection.rs diff --git a/src/model/error.rs b/serenity-core/src/model/error.rs similarity index 100% rename from src/model/error.rs rename to serenity-core/src/model/error.rs diff --git a/src/model/event.rs b/serenity-core/src/model/event.rs similarity index 96% rename from src/model/event.rs rename to serenity-core/src/model/event.rs index afa7c4e5e00..ebb511c735c 100644 --- a/src/model/event.rs +++ b/serenity-core/src/model/event.rs @@ -7,14 +7,11 @@ #![allow(clippy::option_option)] use nonmax::NonMaxU64; -use serde::de::Error as DeError; use serde::Serialize; use strum::{EnumCount, IntoStaticStr, VariantNames}; -use crate::constants::Opcode; use crate::internal::utils::lending_for_each; use crate::model::prelude::*; -use crate::model::utils::remove_from_map; /// Requires no gateway intents. /// @@ -1065,59 +1062,6 @@ pub struct MessagePollVoteRemoveEvent { pub answer_id: AnswerId, } -/// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#payload-structure). -#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] -#[derive(Debug, Clone, Serialize)] -#[non_exhaustive] -#[serde(untagged)] -pub enum GatewayEvent { - Dispatch { - seq: u64, - // Avoid deserialising straight away to handle errors and get access to `seq`. - data: JsonMap, - // Used for debugging, if the data cannot be deserialised. - original_str: FixedString, - }, - Heartbeat(u64), - Reconnect, - /// Whether the session can be resumed. - InvalidateSession(bool), - Hello(u64), - HeartbeatAck, -} - -// Manual impl needed to emulate integer enum tags -impl<'de> Deserialize<'de> for GatewayEvent { - fn deserialize>(deserializer: D) -> StdResult { - let mut map = JsonMap::deserialize(deserializer)?; - - Ok(match remove_from_map(&mut map, "op")? { - Opcode::Dispatch => { - Self::Dispatch { - seq: remove_from_map(&mut map, "s")?, - // Filled in in recv_event - original_str: FixedString::new(), - data: map, - } - }, - Opcode::Heartbeat => Self::Heartbeat(remove_from_map(&mut map, "s")?), - Opcode::InvalidSession => Self::InvalidateSession(remove_from_map(&mut map, "d")?), - Opcode::Hello => { - #[derive(Deserialize)] - struct HelloPayload { - heartbeat_interval: u64, - } - - let inner: HelloPayload = remove_from_map(&mut map, "d")?; - Self::Hello(inner.heartbeat_interval) - }, - Opcode::Reconnect => Self::Reconnect, - Opcode::HeartbeatAck => Self::HeartbeatAck, - _ => return Err(DeError::custom("invalid opcode")), - }) - } -} - /// Event received over a websocket connection /// /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#receive-events). diff --git a/src/model/gateway.rs b/serenity-core/src/model/gateway.rs similarity index 100% rename from src/model/gateway.rs rename to serenity-core/src/model/gateway.rs diff --git a/src/model/guild/audit_log/change.rs b/serenity-core/src/model/guild/audit_log/change.rs similarity index 100% rename from src/model/guild/audit_log/change.rs rename to serenity-core/src/model/guild/audit_log/change.rs diff --git a/src/model/guild/audit_log/mod.rs b/serenity-core/src/model/guild/audit_log/mod.rs similarity index 100% rename from src/model/guild/audit_log/mod.rs rename to serenity-core/src/model/guild/audit_log/mod.rs diff --git a/src/model/guild/audit_log/utils.rs b/serenity-core/src/model/guild/audit_log/utils.rs similarity index 100% rename from src/model/guild/audit_log/utils.rs rename to serenity-core/src/model/guild/audit_log/utils.rs diff --git a/src/model/guild/automod.rs b/serenity-core/src/model/guild/automod.rs similarity index 100% rename from src/model/guild/automod.rs rename to serenity-core/src/model/guild/automod.rs diff --git a/src/model/guild/emoji.rs b/serenity-core/src/model/guild/emoji.rs similarity index 100% rename from src/model/guild/emoji.rs rename to serenity-core/src/model/guild/emoji.rs diff --git a/src/model/guild/guild_id.rs b/serenity-core/src/model/guild/guild_id.rs similarity index 100% rename from src/model/guild/guild_id.rs rename to serenity-core/src/model/guild/guild_id.rs diff --git a/src/model/guild/guild_preview.rs b/serenity-core/src/model/guild/guild_preview.rs similarity index 100% rename from src/model/guild/guild_preview.rs rename to serenity-core/src/model/guild/guild_preview.rs diff --git a/src/model/guild/integration.rs b/serenity-core/src/model/guild/integration.rs similarity index 100% rename from src/model/guild/integration.rs rename to serenity-core/src/model/guild/integration.rs diff --git a/src/model/guild/member.rs b/serenity-core/src/model/guild/member.rs similarity index 100% rename from src/model/guild/member.rs rename to serenity-core/src/model/guild/member.rs diff --git a/src/model/guild/mod.rs b/serenity-core/src/model/guild/mod.rs similarity index 100% rename from src/model/guild/mod.rs rename to serenity-core/src/model/guild/mod.rs diff --git a/src/model/guild/partial_guild.rs b/serenity-core/src/model/guild/partial_guild.rs similarity index 100% rename from src/model/guild/partial_guild.rs rename to serenity-core/src/model/guild/partial_guild.rs diff --git a/src/model/guild/premium_tier.rs b/serenity-core/src/model/guild/premium_tier.rs similarity index 100% rename from src/model/guild/premium_tier.rs rename to serenity-core/src/model/guild/premium_tier.rs diff --git a/src/model/guild/role.rs b/serenity-core/src/model/guild/role.rs similarity index 100% rename from src/model/guild/role.rs rename to serenity-core/src/model/guild/role.rs diff --git a/src/model/guild/scheduled_event.rs b/serenity-core/src/model/guild/scheduled_event.rs similarity index 100% rename from src/model/guild/scheduled_event.rs rename to serenity-core/src/model/guild/scheduled_event.rs diff --git a/src/model/guild/system_channel.rs b/serenity-core/src/model/guild/system_channel.rs similarity index 100% rename from src/model/guild/system_channel.rs rename to serenity-core/src/model/guild/system_channel.rs diff --git a/src/model/guild/welcome_screen.rs b/serenity-core/src/model/guild/welcome_screen.rs similarity index 100% rename from src/model/guild/welcome_screen.rs rename to serenity-core/src/model/guild/welcome_screen.rs diff --git a/src/model/id.rs b/serenity-core/src/model/id.rs similarity index 100% rename from src/model/id.rs rename to serenity-core/src/model/id.rs diff --git a/src/model/invite.rs b/serenity-core/src/model/invite.rs similarity index 100% rename from src/model/invite.rs rename to serenity-core/src/model/invite.rs diff --git a/src/model/mention.rs b/serenity-core/src/model/mention.rs similarity index 100% rename from src/model/mention.rs rename to serenity-core/src/model/mention.rs diff --git a/src/model/misc.rs b/serenity-core/src/model/misc.rs similarity index 100% rename from src/model/misc.rs rename to serenity-core/src/model/misc.rs diff --git a/src/model/mod.rs b/serenity-core/src/model/mod.rs similarity index 100% rename from src/model/mod.rs rename to serenity-core/src/model/mod.rs diff --git a/src/model/monetization.rs b/serenity-core/src/model/monetization.rs similarity index 100% rename from src/model/monetization.rs rename to serenity-core/src/model/monetization.rs diff --git a/src/model/permissions.rs b/serenity-core/src/model/permissions.rs similarity index 100% rename from src/model/permissions.rs rename to serenity-core/src/model/permissions.rs diff --git a/src/model/sticker.rs b/serenity-core/src/model/sticker.rs similarity index 100% rename from src/model/sticker.rs rename to serenity-core/src/model/sticker.rs diff --git a/src/model/timestamp.rs b/serenity-core/src/model/timestamp.rs similarity index 100% rename from src/model/timestamp.rs rename to serenity-core/src/model/timestamp.rs diff --git a/src/model/user.rs b/serenity-core/src/model/user.rs similarity index 100% rename from src/model/user.rs rename to serenity-core/src/model/user.rs diff --git a/src/model/utils.rs b/serenity-core/src/model/utils.rs similarity index 100% rename from src/model/utils.rs rename to serenity-core/src/model/utils.rs diff --git a/src/model/voice.rs b/serenity-core/src/model/voice.rs similarity index 100% rename from src/model/voice.rs rename to serenity-core/src/model/voice.rs diff --git a/src/model/webhook.rs b/serenity-core/src/model/webhook.rs similarity index 100% rename from src/model/webhook.rs rename to serenity-core/src/model/webhook.rs diff --git a/src/secrets.rs b/serenity-core/src/secrets.rs similarity index 100% rename from src/secrets.rs rename to serenity-core/src/secrets.rs diff --git a/src/utils/content_safe.rs b/serenity-core/src/utils/content_safe.rs similarity index 100% rename from src/utils/content_safe.rs rename to serenity-core/src/utils/content_safe.rs diff --git a/src/utils/custom_message.rs b/serenity-core/src/utils/custom_message.rs similarity index 100% rename from src/utils/custom_message.rs rename to serenity-core/src/utils/custom_message.rs diff --git a/src/utils/formatted_timestamp.rs b/serenity-core/src/utils/formatted_timestamp.rs similarity index 100% rename from src/utils/formatted_timestamp.rs rename to serenity-core/src/utils/formatted_timestamp.rs diff --git a/src/utils/message_builder.rs b/serenity-core/src/utils/message_builder.rs similarity index 100% rename from src/utils/message_builder.rs rename to serenity-core/src/utils/message_builder.rs diff --git a/src/utils/mod.rs b/serenity-core/src/utils/mod.rs similarity index 99% rename from src/utils/mod.rs rename to serenity-core/src/utils/mod.rs index d1b9acda80f..a1a9d4a9327 100644 --- a/src/utils/mod.rs +++ b/serenity-core/src/utils/mod.rs @@ -1,8 +1,6 @@ //! A set of utilities to help with common use cases that are not required to fully use the //! library. -#[cfg(feature = "gateway")] -mod argument_convert; #[cfg(feature = "cache")] mod content_safe; mod custom_message; @@ -11,8 +9,6 @@ mod message_builder; use std::num::NonZeroU16; -#[cfg(feature = "gateway")] -pub use argument_convert::*; #[cfg(feature = "cache")] pub use content_safe::*; pub use formatted_timestamp::*; diff --git a/src/collector/mod.rs b/src/collector/mod.rs index 3015ac5ce13..754f93592be 100644 --- a/src/collector/mod.rs +++ b/src/collector/mod.rs @@ -169,7 +169,7 @@ make_specific_collector!( CollectComponentInteractions, collect_component_interactions, // This defines the extractor pattern, which extracts the data we want to collect from an Event. Event::InteractionCreate(InteractionCreateEvent { - interaction: Interaction::Component(interaction), + interaction: Interaction::Component(interaction), .. }) => interaction, // All following lines define built-in filters of the collector. // Each line consists of: @@ -186,7 +186,7 @@ make_specific_collector!( ModalInteractionCollector, ModalInteraction, CollectModalInteractions, collect_modal_interactions, Event::InteractionCreate(InteractionCreateEvent { - interaction: Interaction::Modal(interaction), + interaction: Interaction::Modal(interaction), .. }) => interaction, author_id: UserId => interaction.user.id == *author_id, channel_id: ChannelId => interaction.channel_id == *channel_id, @@ -197,7 +197,7 @@ make_specific_collector!( make_specific_collector!( ReactionCollector, Reaction, CollectReactions, collect_reactions, - Event::ReactionAdd(ReactionAddEvent { reaction }) => reaction, + Event::ReactionAdd(ReactionAddEvent { reaction, .. }) => reaction, author_id: UserId => reaction.user_id.is_none_or(|a| a == *author_id), channel_id: ChannelId => reaction.channel_id == *channel_id, guild_id: GuildId => reaction.guild_id.is_none_or(|g| g == *guild_id), @@ -206,7 +206,7 @@ make_specific_collector!( make_specific_collector!( MessageCollector, Message, CollectMessages, collect_messages, - Event::MessageCreate(MessageCreateEvent { message }) => message, + Event::MessageCreate(MessageCreateEvent { message, .. }) => message, author_id: UserId => message.author.id == *author_id, channel_id: ChannelId => message.channel_id == *channel_id, guild_id: GuildId => message.guild_id.is_none_or(|g| g == *guild_id), diff --git a/src/constants.rs b/src/constants.rs index 86a9d925cf6..ee526a8bdc4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,40 +1,9 @@ -//! A set of constants used by the library. - -use nonmax::NonMaxU16; - -/// The maximum length of the textual size of an embed. -pub const EMBED_MAX_LENGTH: usize = 6000; - -/// The maximum number of embeds in a message. -pub const EMBED_MAX_COUNT: usize = 10; - -/// The maximum number of stickers in a message. -pub const STICKER_MAX_COUNT: usize = 3; - /// The gateway version used by the library. The gateway URL is retrieved via the REST API. pub const GATEWAY_VERSION: u8 = 10; /// The large threshold to send on identify. pub const LARGE_THRESHOLD: u8 = 250; -/// The maximum unicode code points allowed within a message by Discord. -pub const MESSAGE_CODE_LIMIT: usize = 2000; - -/// The maximum number of members the bot can fetch at once -pub const MEMBER_FETCH_LIMIT: NonMaxU16 = match NonMaxU16::new(1000) { - Some(m) => m, - None => unreachable!(), -}; - -/// The [UserAgent] sent along with every request. -/// -/// [UserAgent]: ::reqwest::header::USER_AGENT -pub const USER_AGENT: &str = concat!( - "DiscordBot (https://github.com/serenity-rs/serenity, ", - env!("CARGO_PKG_VERSION"), - ")" -); - enum_number! { /// An enum representing the [gateway opcodes]. /// diff --git a/src/error.rs b/src/error.rs index d090cb860e6..2e5d0ea43b0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,56 +1,24 @@ -use std::error::Error as StdError; -use std::fmt; -use std::io::Error as IoError; - -#[cfg(feature = "http")] -use reqwest::{header::InvalidHeaderValue, Error as ReqwestError}; +pub use serenity_core::error::Error as CoreError; #[cfg(feature = "gateway")] use tokio_tungstenite::tungstenite::error::Error as TungsteniteError; #[cfg(feature = "gateway")] use crate::gateway::GatewayError; -#[cfg(feature = "http")] -use crate::http::HttpError; -use crate::internal::prelude::*; -use crate::model::ModelError; -use crate::secrets::TokenError; -/// The common result type between most library functions. -/// -/// The library exposes functions which, for a result type, exposes only one type, rather than the -/// usual 2 (`Result`). This is because all functions that return a result return -/// serenity's [`Error`], so this is implied, and a "simpler" result is used. -pub type Result = StdResult; +pub type Result = std::result::Result; -/// A common error enum returned by most of the library's functionality within a custom [`Result`]. #[derive(Debug)] -#[non_exhaustive] pub enum Error { - /// An [`std::io`] error. - Io(IoError), - /// An error from the [`serde_json`] crate. - Json(serde_json::Error), - /// An error from the [`model`] module. - /// - /// [`model`]: crate::model - Model(ModelError), /// An error from the [`gateway`] module. /// /// [`gateway`]: crate::gateway #[cfg(feature = "gateway")] Gateway(GatewayError), - /// An error from the [`http`] module. - /// - /// [`http`]: crate::http - #[cfg(feature = "http")] - Http(HttpError), /// An error from the `tungstenite` crate. #[cfg(feature = "gateway")] Tungstenite(Box), - /// An error from the [`secrets`] module. - /// - /// [`secrets`]: crate::secrets - Token(TokenError), + /// An error from serenity's core. + Core(CoreError), } #[cfg(feature = "gateway")] @@ -60,24 +28,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: IoError) -> Error { - Error::Io(e) - } -} - -impl From for Error { - fn from(e: serde_json::Error) -> Error { - Error::Json(e) - } -} - -impl From for Error { - fn from(e: ModelError) -> Error { - Error::Model(e) - } -} - #[cfg(feature = "gateway")] impl From for Error { fn from(e: TungsteniteError) -> Error { @@ -85,64 +35,8 @@ impl From for Error { } } -#[cfg(feature = "http")] -impl From for Error { - fn from(e: HttpError) -> Error { - Error::Http(e) - } -} - -impl From for Error { - fn from(e: TokenError) -> Error { - Error::Token(e) - } -} - -#[cfg(feature = "http")] -impl From for Error { - fn from(e: InvalidHeaderValue) -> Error { - HttpError::InvalidHeader(e).into() - } -} - -#[cfg(feature = "http")] -impl From for Error { - fn from(e: ReqwestError) -> Error { - HttpError::Request(e).into() - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Io(inner) => fmt::Display::fmt(&inner, f), - Self::Json(inner) => fmt::Display::fmt(&inner, f), - Self::Model(inner) => fmt::Display::fmt(&inner, f), - #[cfg(feature = "gateway")] - Self::Gateway(inner) => fmt::Display::fmt(&inner, f), - #[cfg(feature = "http")] - 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), - } - } -} - -impl StdError for Error { - #[cfg_attr(feature = "tracing_instrument", instrument)] - fn source(&self) -> Option<&(dyn StdError + 'static)> { - match self { - Self::Io(inner) => Some(inner), - Self::Json(inner) => Some(inner), - Self::Model(inner) => Some(inner), - #[cfg(feature = "gateway")] - Self::Gateway(inner) => Some(inner), - #[cfg(feature = "http")] - Self::Http(inner) => Some(inner), - #[cfg(feature = "gateway")] - Self::Tungstenite(inner) => Some(inner), - Self::Token(inner) => Some(inner), - } +impl From for Error { + fn from(e: CoreError) -> Error { + Error::Core(e) } } diff --git a/src/gateway/client/context.rs b/src/gateway/client/context.rs index 6341e87de35..902b2ca919b 100644 --- a/src/gateway/client/context.rs +++ b/src/gateway/client/context.rs @@ -5,6 +5,7 @@ use std::sync::Arc; pub use crate::cache::Cache; use crate::gateway::{ActivityData, ShardMessenger, ShardRunner}; use crate::http::{CacheHttp, Http}; +use crate::internal::prelude::*; use crate::model::prelude::*; /// A general utility struct provided on event dispatches. @@ -328,7 +329,7 @@ impl Context { /// /// Returns an error if the Application ID is not known. pub async fn get_application_emojis(&self) -> Result> { - self.http.get_application_emojis().await + Ok(self.http.get_application_emojis().await?) } /// Gets information about an application emoji. @@ -337,7 +338,7 @@ impl Context { /// /// Returns an error if the emoji does not exist. pub async fn get_application_emoji(&self, emoji_id: EmojiId) -> Result { - self.http.get_application_emoji(emoji_id).await + Ok(self.http.get_application_emoji(emoji_id).await?) } /// Creates an application emoji with a name and base64-encoded image. @@ -358,7 +359,7 @@ impl Context { image, }; - self.http.create_application_emoji(&body).await + Ok(self.http.create_application_emoji(&body).await?) } /// Changes the name of an application emoji. @@ -376,7 +377,7 @@ impl Context { name, }; - self.http.edit_application_emoji(emoji_id, &body).await + Ok(self.http.edit_application_emoji(emoji_id, &body).await?) } /// Deletes an application emoji. @@ -385,6 +386,6 @@ impl Context { /// /// Returns an error if the emoji does not exist. pub async fn delete_application_emoji(&self, emoji_id: EmojiId) -> Result<()> { - self.http.delete_application_emoji(emoji_id).await + Ok(self.http.delete_application_emoji(emoji_id).await?) } } diff --git a/src/gateway/client/dispatch.rs b/src/gateway/client/dispatch.rs index d5314d2ff10..c2ba703feec 100644 --- a/src/gateway/client/dispatch.rs +++ b/src/gateway/client/dispatch.rs @@ -458,6 +458,7 @@ fn update_cache_with_event( Event::MessagePollVoteRemove(event) => FullEvent::MessagePollVoteRemove { event, }, + _ => todo!(), }; (event, extra_event) diff --git a/src/gateway/client/event_handler.rs b/src/gateway/client/event_handler.rs index fa5d61a050f..10669bd811e 100644 --- a/src/gateway/client/event_handler.rs +++ b/src/gateway/client/event_handler.rs @@ -3,6 +3,7 @@ use std::collections::VecDeque; use std::num::NonZeroU16; use async_trait::async_trait; +use extract_map::ExtractMap; use strum::{EnumCount, IntoStaticStr, VariantNames}; use super::context::Context; diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 3bf106bc3fc..5001c5fa456 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -21,12 +21,15 @@ mod ws; #[cfg(feature = "http")] use reqwest::IntoUrl; use reqwest::Url; +use serde::de::{Deserialize, Deserializer, Error as DeError}; pub use self::error::Error as GatewayError; pub use self::sharding::*; #[cfg(feature = "voice")] pub use self::voice::VoiceGatewayManager; pub use self::ws::WsClient; +use crate::constants::Opcode; +use crate::error::CoreError; use crate::internal::prelude::*; use crate::model::gateway::{Activity, ActivityType}; use crate::model::id::UserId; @@ -78,7 +81,7 @@ impl ActivityData { name: name.into().trunc_into(), kind: ActivityType::Streaming, state: None, - url: Some(url.into_url()?), + url: Some(url.into_url().map_err(CoreError::from)?), }) } @@ -154,3 +157,72 @@ pub enum ChunkGuildFilter { /// Will return a maximum of 100 members. UserIds(Vec), } + +/// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#payload-structure). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Debug, Clone, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum GatewayEvent { + Dispatch { + seq: u64, + // Avoid deserialising straight away to handle errors and get access to `seq`. + data: JsonMap, + // Used for debugging, if the data cannot be deserialised. + original_str: FixedString, + }, + Heartbeat(u64), + Reconnect, + /// Whether the session can be resumed. + InvalidateSession(bool), + Hello(u64), + HeartbeatAck, +} + +// Manual impl needed to emulate integer enum tags +impl<'de> Deserialize<'de> for GatewayEvent { + fn deserialize>(deserializer: D) -> Result { + let mut map = JsonMap::deserialize(deserializer)?; + + Ok(match remove_from_map(&mut map, "op")? { + Opcode::Dispatch => { + Self::Dispatch { + seq: remove_from_map(&mut map, "s")?, + // Filled in in recv_event + original_str: FixedString::new(), + data: map, + } + }, + Opcode::Heartbeat => Self::Heartbeat(remove_from_map(&mut map, "s")?), + Opcode::InvalidSession => Self::InvalidateSession(remove_from_map(&mut map, "d")?), + Opcode::Hello => { + #[derive(Deserialize)] + struct HelloPayload { + heartbeat_interval: u64, + } + + let inner: HelloPayload = remove_from_map(&mut map, "d")?; + Self::Hello(inner.heartbeat_interval) + }, + Opcode::Reconnect => Self::Reconnect, + Opcode::HeartbeatAck => Self::HeartbeatAck, + _ => return Err(DeError::custom("invalid opcode")), + }) + } +} + +fn remove_from_map(map: &mut JsonMap, key: &'static str) -> Result +where + T: serde::de::DeserializeOwned, + E: serde::de::Error, +{ + map.remove(key).ok_or_else(|| serde::de::Error::missing_field(key)).and_then(deserialize_val) +} + +fn deserialize_val(val: Value) -> Result +where + T: serde::de::DeserializeOwned, + E: serde::de::Error, +{ + T::deserialize(val).map_err(serde::de::Error::custom) +} diff --git a/src/gateway/sharding/mod.rs b/src/gateway/sharding/mod.rs index 80451a274bf..fa7f9e8ff61 100644 --- a/src/gateway/sharding/mod.rs +++ b/src/gateway/sharding/mod.rs @@ -61,10 +61,11 @@ pub use self::shard_manager::{ pub use self::shard_messenger::ShardMessenger; pub use self::shard_queuer::{ShardQueue, ShardQueuer, ShardQueuerMessage}; pub use self::shard_runner::{ShardRunner, ShardRunnerMessage, ShardRunnerOptions}; -use super::{ActivityData, ChunkGuildFilter, GatewayError, PresenceData, WsClient}; +use super::{ActivityData, ChunkGuildFilter, GatewayError, GatewayEvent, PresenceData, WsClient}; use crate::constants::{self, close_codes}; +use crate::error::CoreError; use crate::internal::prelude::*; -use crate::model::event::{Event, GatewayEvent}; +use crate::model::event::Event; use crate::model::gateway::{GatewayIntents, ShardInfo}; use crate::model::id::{ApplicationId, GuildId, ShardId}; use crate::model::user::OnlineStatus; @@ -848,7 +849,7 @@ fn deserialize_and_log_event(map: JsonMap, original_str: &str) -> Result warn!("Err deserializing text: {err_dbg}"); } debug!("Failing text: {original_str}"); - Error::Json(err) + Error::Core(CoreError::Json(err)) }) } diff --git a/src/gateway/sharding/shard_runner.rs b/src/gateway/sharding/shard_runner.rs index f5b3a8c28ac..5f0a22f8904 100644 --- a/src/gateway/sharding/shard_runner.rs +++ b/src/gateway/sharding/shard_runner.rs @@ -13,17 +13,18 @@ use super::CollectorCallback; use super::{ReconnectType, Shard, ShardAction, ShardId, ShardManager, ShardStageUpdateEvent}; #[cfg(feature = "cache")] use crate::cache::Cache; +use crate::error::CoreError; #[cfg(feature = "framework")] use crate::framework::Framework; use crate::gateway::client::dispatch::dispatch_model; use crate::gateway::client::{Context, EventHandler, RawEventHandler}; #[cfg(feature = "voice")] use crate::gateway::VoiceGatewayManager; -use crate::gateway::{ActivityData, ChunkGuildFilter, GatewayError}; +use crate::gateway::{ActivityData, ChunkGuildFilter, GatewayError, GatewayEvent}; use crate::http::Http; use crate::internal::prelude::*; use crate::internal::tokio::spawn_named; -use crate::model::event::{Event, GatewayEvent}; +use crate::model::event::Event; use crate::model::id::GuildId; use crate::model::user::OnlineStatus; @@ -463,7 +464,7 @@ impl ShardRunner { self.manager.return_with_value(Err(why_clone)).await; return Err(Error::Gateway(why)); }, - Err(Error::Json(_)) => return Ok((None, None, true)), + Err(Error::Core(CoreError::Json(_))) => return Ok((None, None, true)), Err(why) => { error!("Shard handler recieved err: {why:?}"); return Ok((None, None, true)); diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index d88a1b569f7..7f117a9ca15 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -18,9 +18,16 @@ use url::Url; #[cfg(feature = "transport_compression_zstd")] use zstd_safe::{DStream as ZstdInflater, InBuffer, OutBuffer}; -use super::{ActivityData, ChunkGuildFilter, GatewayError, PresenceData, TransportCompression}; +use super::{ + ActivityData, + ChunkGuildFilter, + GatewayError, + GatewayEvent, + PresenceData, + TransportCompression, +}; use crate::constants::{self, Opcode}; -use crate::model::event::GatewayEvent; +use crate::error::CoreError; use crate::model::gateway::{GatewayIntents, ShardInfo}; use crate::model::id::{GuildId, UserId}; use crate::{Error, Result}; @@ -113,12 +120,15 @@ impl Compression { decompressed.clear(); decompressed.reserve(slice.len() * DECOMPRESSION_MULTIPLIER); - ZlibDecoder::new(slice).read_to_end(decompressed).map_err(|why| { - warn!("Err decompressing bytes: {why:?}"); - debug!("Failing bytes: {slice:?}"); + ZlibDecoder::new(slice) + .read_to_end(decompressed) + .map_err(|why| { + warn!("Err decompressing bytes: {why:?}"); + debug!("Failing bytes: {slice:?}"); - why - })?; + why + }) + .map_err(CoreError::from)?; Ok(Some(decompressed.as_slice())) }, @@ -278,13 +288,13 @@ impl WsClient { }, Err(err) => { debug!("Failing text: {}", json_str()); - Err(Error::Json(err)) + Err(Error::Core(CoreError::Json(err))) }, } } pub(crate) async fn send_json(&mut self, value: &impl serde::Serialize) -> Result<()> { - let message = serde_json::to_string(value).map(Message::Text)?; + let message = serde_json::to_string(value).map(Message::Text).map_err(CoreError::from)?; self.stream.send(message).await?; Ok(()) diff --git a/src/internal.rs b/src/internal.rs new file mode 100644 index 00000000000..e9bdfe50c0f --- /dev/null +++ b/src/internal.rs @@ -0,0 +1,74 @@ +pub mod prelude { + pub use serde_json::Value; + pub use serenity_core::secrets::Token; + pub use small_fixed_array::{FixedArray, FixedString, TruncatingInto}; + pub use to_arraystring::ToArrayString; + + pub use crate::error::{Error, Result}; + + pub type JsonMap = serde_json::Map; +} + +pub mod tokio { + use std::future::Future; + + pub fn spawn_named(_name: &str, future: F) -> tokio::task::JoinHandle + where + F: Future + Send + 'static, + T: Send + 'static, + { + #[cfg(all(tokio_unstable, feature = "tokio_task_builder"))] + let handle = tokio::task::Builder::new() + .name(&*format!("serenity::{}", _name)) + .spawn(future) + .expect("called outside tokio runtime"); + #[cfg(not(all(tokio_unstable, feature = "tokio_task_builder")))] + let handle = tokio::spawn(future); + handle + } +} + +#[macro_use] +pub mod macros { + macro_rules! enum_number { + ( + $(#[$outer:meta])* + $(#[ = $default:literal])? + $vis:vis enum $Enum:ident { + $( + $(#[doc = $doc:literal])* + $(#[cfg $($cfg:tt)*])? + $Variant:ident = $value:literal, + )* + _ => Unknown($T:ty), + } + ) => { + $(#[$outer])* + $vis struct $Enum (pub $T); + + $( + impl Default for $Enum { + fn default() -> Self { + Self($default) + } + } + )? + + #[allow(non_snake_case, non_upper_case_globals)] + #[allow(clippy::allow_attributes, reason = "Does not always trigger due to macro")] + impl $Enum { + $( + $(#[doc = $doc])* + $(#[cfg $($cfg)*])? + $vis const $Variant: Self = Self($value); + )* + + /// Variant value is unknown. + #[must_use] + $vis const fn Unknown(val: $T) -> Self { + Self(val) + } + } + }; + } +} diff --git a/src/lib.rs b/src/lib.rs index 40897739a28..111fe74df2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,13 +86,13 @@ extern crate serde; mod internal; pub mod constants; -pub mod model; +pub use serenity_core::model; pub mod prelude; #[cfg(feature = "builder")] -pub mod builder; +pub use serenity_core::builder; #[cfg(feature = "cache")] -pub mod cache; +pub use serenity_core::cache; #[cfg(feature = "collector")] pub mod collector; #[cfg(feature = "framework")] @@ -100,12 +100,12 @@ pub mod framework; #[cfg(feature = "gateway")] pub mod gateway; #[cfg(feature = "http")] -pub mod http; +pub use serenity_core::http; #[cfg(feature = "interactions_endpoint")] pub mod interactions_endpoint; -pub mod secrets; +pub use serenity_core::secrets; #[cfg(feature = "utils")] -pub mod utils; +pub use serenity_core::utils; mod error; diff --git a/src/utils/argument_convert/_template.rs b/src/utils/argument_convert/_template.rs deleted file mode 100644 index 422406e9466..00000000000 --- a/src/utils/argument_convert/_template.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::fmt; -use super::ArgumentConvert; -use crate::{model::prelude::*, prelude::*}; - -/// Error that can be returned from [`PLACEHOLDER::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum PLACEHOLDERParseError { -} - -impl std::error::Error for PLACEHOLDERParseError {} - -impl fmt::Display for PLACEHOLDERParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - } - } -} - -/// Look up a [`PLACEHOLDER`] by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by PLACEHOLDER -/// 2. [Lookup by PLACEHOLDER](`crate::utils::parse_PLACEHOLDER`). -#[async_trait::async_trait] -impl ArgumentConvert for PLACEHOLDER { - type Err = PLACEHOLDERParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let lookup_by_PLACEHOLDER = || PLACEHOLDER; - - lookup_by_PLACEHOLDER() - .or_else(lookup_by_PLACEHOLDER) - .or_else(lookup_by_PLACEHOLDER) - .cloned() - .ok_or(PLACEHOLDERParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/channel.rs b/src/utils/argument_convert/channel.rs deleted file mode 100644 index debba293bf7..00000000000 --- a/src/utils/argument_convert/channel.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Channel::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum ChannelParseError { - /// When channel retrieval via HTTP failed - Http(SerenityError), - /// The provided channel string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, -} - -impl std::error::Error for ChannelParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::NotFoundOrMalformed => None, - } - } -} - -impl fmt::Display for ChannelParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Http(_) => f.write_str("Failed to request channel via HTTP"), - Self::NotFoundOrMalformed => f.write_str("Channel not found or unknown format"), - } - } -} - -fn channel_belongs_to_guild(channel: &Channel, guild: GuildId) -> bool { - match channel { - Channel::Guild(channel) => channel.guild_id == guild, - Channel::Private(_channel) => false, - } -} - -async fn lookup_channel_global( - ctx: impl CacheHttp, - guild_id: Option, - s: &str, -) -> Result { - if let Some(channel_id) = s - .parse() - .ok() - .or_else(|| crate::utils::parse_channel_mention(s)) - .or_else(|| crate::utils::parse_channel_url(s).map(|(_, channel_id)| channel_id)) - { - return channel_id.to_channel(ctx, guild_id).await.map_err(ChannelParseError::Http); - } - - let guild_id = guild_id.ok_or(ChannelParseError::NotFoundOrMalformed)?; - - #[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)); - if let Some(channel) = channel { - return Ok(Channel::Guild(channel.clone())); - } - } - - return Err(ChannelParseError::NotFoundOrMalformed); - } - - 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)) { - Ok(Channel::Guild(channel)) - } else { - Err(ChannelParseError::NotFoundOrMalformed) - } -} - -/// Look up a Channel by a string case-insensitively. -/// -/// Lookup are done via local guild. If in DMs, the global cache is used instead. -/// -/// The cache feature needs to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_channel_mention`). -/// 3. Lookup by name. -#[async_trait::async_trait] -impl ArgumentConvert for Channel { - type Err = ChannelParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let channel = lookup_channel_global(&ctx, guild_id, s).await?; - - // Don't yield for other guilds' channels - if let Some(guild_id) = guild_id { - if !channel_belongs_to_guild(&channel, guild_id) { - return Err(ChannelParseError::NotFoundOrMalformed); - } - }; - - Ok(channel) - } -} - -/// Error that can be returned from [`GuildChannel::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum GuildChannelParseError { - /// When channel retrieval via HTTP failed - Http(SerenityError), - /// The provided channel string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, - /// When the referenced channel is not a guild channel - NotAGuildChannel, -} - -impl std::error::Error for GuildChannelParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::NotFoundOrMalformed | Self::NotAGuildChannel => None, - } - } -} - -impl fmt::Display for GuildChannelParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Http(_) => f.write_str("Failed to request channel via HTTP"), - Self::NotFoundOrMalformed => f.write_str("Channel not found or unknown format"), - Self::NotAGuildChannel => f.write_str("Channel is not a guild channel"), - } - } -} - -/// Look up a GuildChannel by a string case-insensitively. -/// -/// Lookup is done by the global cache, hence the cache feature needs to be enabled. -/// -/// For more information, see the ArgumentConvert implementation for [`Channel`] -#[async_trait::async_trait] -impl ArgumentConvert for GuildChannel { - type Err = GuildChannelParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result { - match Channel::convert(&ctx, guild_id, channel_id, s).await { - Ok(Channel::Guild(channel)) => Ok(channel), - Ok(_) => Err(GuildChannelParseError::NotAGuildChannel), - Err(ChannelParseError::Http(e)) => Err(GuildChannelParseError::Http(e)), - Err(ChannelParseError::NotFoundOrMalformed) => { - Err(GuildChannelParseError::NotFoundOrMalformed) - }, - } - } -} diff --git a/src/utils/argument_convert/emoji.rs b/src/utils/argument_convert/emoji.rs deleted file mode 100644 index 9a5d986ddb5..00000000000 --- a/src/utils/argument_convert/emoji.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Emoji::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum EmojiParseError { - /// Parser was invoked outside a guild. - OutsideGuild, - /// Guild was not in cache, or guild HTTP request failed. - FailedToRetrieveGuild, - /// The provided emoji string failed to parse, or the parsed result cannot be found in the - /// guild roles. - NotFoundOrMalformed, -} - -impl std::error::Error for EmojiParseError {} - -impl fmt::Display for EmojiParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::OutsideGuild => f.write_str("Tried to find emoji outside a guild"), - Self::FailedToRetrieveGuild => f.write_str("Could not retrieve guild data"), - Self::NotFoundOrMalformed => f.write_str("Emoji not found or unknown format"), - } - } -} - -/// Look up a [`Emoji`]. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by extracting ID from the emoji](`crate::utils::parse_emoji`). -/// 3. Lookup by name. -#[async_trait::async_trait] -impl ArgumentConvert for Emoji { - type Err = EmojiParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - // Get Guild or PartialGuild - let guild_id = guild_id.ok_or(EmojiParseError::OutsideGuild)?; - let guild = guild_id - .to_partial_guild(&ctx) - .await - .map_err(|_| EmojiParseError::FailedToRetrieveGuild)?; - - let direct_id = s.parse().ok(); - let id_from_mention = crate::utils::parse_emoji(s).map(|e| e.id); - - if let Some(emoji_id) = direct_id.or(id_from_mention) { - if let Some(emoji) = guild.emojis.get(&emoji_id).cloned() { - return Ok(emoji); - } - } - - if let Some(emoji) = - guild.emojis.iter().find(|emoji| emoji.name.eq_ignore_ascii_case(s)).cloned() - { - return Ok(emoji); - } - - Err(EmojiParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/guild.rs b/src/utils/argument_convert/guild.rs deleted file mode 100644 index 77888f80eec..00000000000 --- a/src/utils/argument_convert/guild.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Guild::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum GuildParseError { - /// The provided guild string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, - /// No cache, so no guild search could be done. - NoCache, -} - -impl std::error::Error for GuildParseError {} - -impl fmt::Display for GuildParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotFoundOrMalformed => f.write_str("Guild not found or unknown format"), - Self::NoCache => f.write_str("No cached list of guilds was provided"), - } - } -} - -/// Look up a Guild, either by ID or by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -#[async_trait::async_trait] -impl ArgumentConvert for Guild { - type Err = GuildParseError; - - async fn convert( - ctx: impl CacheHttp, - _guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let guilds = &ctx.cache().ok_or(GuildParseError::NoCache)?.guilds; - - let lookup_by_id = || guilds.get(&s.parse().ok()?).map(|g| g.clone()); - - let lookup_by_name = || { - guilds.iter().find_map(|m| { - let guild = m.value(); - guild.name.eq_ignore_ascii_case(s).then(|| guild.clone()) - }) - }; - - lookup_by_id().or_else(lookup_by_name).ok_or(GuildParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/member.rs b/src/utils/argument_convert/member.rs deleted file mode 100644 index e3cea497ebd..00000000000 --- a/src/utils/argument_convert/member.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Member::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum MemberParseError { - /// Parser was invoked outside a guild. - OutsideGuild, - /// The guild in which the parser was invoked is not in cache. - GuildNotInCache, - /// The provided member string failed to parse, or the parsed result cannot be found in the - /// guild cache data. - NotFoundOrMalformed, -} - -impl std::error::Error for MemberParseError {} - -impl fmt::Display for MemberParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::OutsideGuild => f.write_str("Tried to find member outside a guild"), - Self::GuildNotInCache => f.write_str("Guild is not in cache"), - Self::NotFoundOrMalformed => f.write_str("Member not found or unknown format"), - } - } -} - -/// Look up a guild member by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_user_mention`). -/// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). -/// 4. Lookup by name -/// 5. Lookup by nickname -#[async_trait::async_trait] -impl ArgumentConvert for Member { - type Err = MemberParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let guild_id = guild_id.ok_or(MemberParseError::OutsideGuild)?; - - // DON'T use guild.members: it's only populated when guild presences intent is enabled! - - // If string is a raw user ID or a mention - if let Some(user_id) = s.parse().ok().or_else(|| crate::utils::parse_user_mention(s)) { - if let Ok(member) = guild_id.member(&ctx, user_id).await { - return Ok(member); - } - } - - // Following code is inspired by discord.py's MemberConvert::query_member_named - - // If string is a username+discriminator - let limit = nonmax::NonMaxU16::new(100); - if let Some((name, discrim)) = crate::utils::parse_user_tag(s) { - if let Ok(member_results) = guild_id.search_members(ctx.http(), name, limit).await { - if let Some(member) = member_results.into_iter().find(|m| { - m.user.name.eq_ignore_ascii_case(name) && m.user.discriminator == discrim - }) { - return Ok(member); - } - } - } - - // If string is username or nickname - if let Ok(member_results) = guild_id.search_members(ctx.http(), s, limit).await { - if let Some(member) = member_results.into_iter().find(|m| { - m.user.name.eq_ignore_ascii_case(s) - || m.nick.as_ref().is_some_and(|nick| nick.eq_ignore_ascii_case(s)) - }) { - return Ok(member); - } - } - - Err(MemberParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/message.rs b/src/utils/argument_convert/message.rs deleted file mode 100644 index 6023402a39d..00000000000 --- a/src/utils/argument_convert/message.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Message::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum MessageParseError { - /// When the provided string does not adhere to any known guild message format - Malformed, - /// When message data retrieval via HTTP failed - Http(SerenityError), - /// When the `gateway` feature is disabled and the required information was not in cache. - HttpNotAvailable, -} - -impl std::error::Error for MessageParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::HttpNotAvailable | Self::Malformed => None, - } - } -} - -impl fmt::Display for MessageParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Malformed => { - f.write_str("Provided string did not adhere to any known guild message format") - }, - Self::Http(_) => f.write_str("Failed to request message data via HTTP"), - Self::HttpNotAvailable => f.write_str( - "Gateway feature is disabled and the required information was not in cache", - ), - } - } -} - -/// Look up a message by a string. -/// -/// The lookup strategy is as follows (in order): -/// 1. [Lookup by "{channel ID}-{message ID}"](`crate::utils::parse_message_id_pair`) (retrieved by -/// shift-clicking on "Copy ID") -/// 2. Lookup by message ID (the message must be in the context channel) -/// 3. [Lookup by message URL](`crate::utils::parse_message_url`) -#[async_trait::async_trait] -impl ArgumentConvert for Message { - type Err = MessageParseError; - - async fn convert( - ctx: impl CacheHttp, - _guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result { - let extract_from_message_id = || Some((channel_id?, s.parse().ok()?)); - - let extract_from_message_url = || { - let (_guild_id, channel_id, message_id) = super::parse_message_url(s)?; - Some((channel_id, message_id)) - }; - - let (channel_id, message_id) = super::parse_message_id_pair(s) - .or_else(extract_from_message_id) - .or_else(extract_from_message_url) - .ok_or(MessageParseError::Malformed)?; - - channel_id.message(ctx, message_id).await.map_err(MessageParseError::Http) - } -} diff --git a/src/utils/argument_convert/mod.rs b/src/utils/argument_convert/mod.rs deleted file mode 100644 index 8a43f726a69..00000000000 --- a/src/utils/argument_convert/mod.rs +++ /dev/null @@ -1,195 +0,0 @@ -mod member; -pub use member::*; - -mod message; -pub use message::*; - -mod user; -pub use user::*; - -mod channel; -pub use channel::*; - -// From HTTP you can only get PartialGuild; for Guild you need gateway and cache -#[cfg(feature = "cache")] -mod guild; -#[cfg(feature = "cache")] -pub use guild::*; - -mod role; -pub use role::*; - -mod emoji; -pub use emoji::*; - -use super::DOMAINS; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Parse a value from a string in context of a received message. -/// -/// This trait is a superset of [`std::str::FromStr`]. The difference is that this trait aims to -/// support serenity-specific Discord types like [`Member`] or [`Message`]. -/// -/// Trait implementations may do network requests as part of their parsing procedure. -/// -/// Useful for implementing argument parsing in command frameworks. -#[async_trait::async_trait] -pub trait ArgumentConvert: Sized { - /// The associated error which can be returned from parsing. - type Err; - - /// Parses a string `s` as a command parameter of this type. - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result; -} - -#[async_trait::async_trait] -impl ArgumentConvert for T { - type Err = ::Err; - - async fn convert( - _: impl CacheHttp, - _: Option, - _: Option, - s: &str, - ) -> Result { - T::from_str(s) - } -} - -// The following few parse_XXX methods are in here (parse.rs) because they need to be gated behind -// the model feature and it's just convenient to put them here for that - -/// Retrieves IDs from "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID"). -/// -/// If the string is invalid, None is returned. -/// -/// # Examples -/// ```rust -/// use serenity::model::prelude::*; -/// use serenity::utils::parse_message_id_pair; -/// -/// assert_eq!( -/// parse_message_id_pair("673965002805477386-842482646604972082"), -/// Some((ChannelId::new(673965002805477386), MessageId::new(842482646604972082))), -/// ); -/// assert_eq!( -/// parse_message_id_pair("673965002805477386-842482646604972082-472029906943868929"), -/// None, -/// ); -/// ``` -#[must_use] -pub fn parse_message_id_pair(s: &str) -> Option<(ChannelId, MessageId)> { - let mut parts = s.splitn(2, '-'); - let channel_id = parts.next()?.parse().ok()?; - let message_id = parts.next()?.parse().ok()?; - Some((channel_id, message_id)) -} - -/// Retrieves guild, channel, and message ID from a message URL. -/// -/// If the URL is malformed, None is returned. -/// -/// # Examples -/// ```rust -/// use serenity::model::prelude::*; -/// use serenity::utils::parse_message_url; -/// -/// assert_eq!( -/// parse_message_url( -/// "https://discord.com/channels/381880193251409931/381880193700069377/806164913558781963" -/// ), -/// Some(( -/// GuildId::new(381880193251409931), -/// ChannelId::new(381880193700069377), -/// MessageId::new(806164913558781963), -/// )), -/// ); -/// assert_eq!( -/// parse_message_url( -/// "https://canary.discord.com/channels/381880193251409931/381880193700069377/806164913558781963" -/// ), -/// Some(( -/// GuildId::new(381880193251409931), -/// ChannelId::new(381880193700069377), -/// MessageId::new(806164913558781963), -/// )), -/// ); -/// assert_eq!(parse_message_url("https://google.com"), None); -/// ``` -#[must_use] -pub fn parse_message_url(s: &str) -> Option<(GuildId, ChannelId, MessageId)> { - use aformat::{aformat, CapStr}; - - for domain in DOMAINS { - let prefix = aformat!("https://{}/channels/", CapStr::(domain)); - if let Some(parts) = s.strip_prefix(prefix.as_str()) { - let mut parts = parts.splitn(3, '/'); - - let guild_id = parts.next()?.parse().ok()?; - let channel_id = parts.next()?.parse().ok()?; - let message_id = parts.next()?.parse().ok()?; - return Some((guild_id, channel_id, message_id)); - } - } - None -} - -/// Retrieves guild, and channel ID from a channel URL. -/// -/// If the URL is malformed, None is returned. -/// -/// # Examples -/// ```rust -/// use serenity::model::prelude::*; -/// use serenity::utils::parse_channel_url; -/// -/// assert_eq!( -/// parse_channel_url("https://discord.com/channels/381880193251409931/381880193700069377"), -/// Some((GuildId::new(381880193251409931), ChannelId::new(381880193700069377),)), -/// ); -/// assert_eq!( -/// parse_channel_url( -/// "https://canary.discord.com/channels/381880193251409931/381880193700069377" -/// ), -/// Some((GuildId::new(381880193251409931), ChannelId::new(381880193700069377),)), -/// ); -/// assert_eq!(parse_channel_url("https://google.com"), None); -/// ``` -#[must_use] -pub fn parse_channel_url(s: &str) -> Option<(GuildId, ChannelId)> { - use aformat::{aformat, CapStr}; - - for domain in DOMAINS { - let prefix = aformat!("https://{}/channels/", CapStr::(domain)); - if let Some(parts) = s.strip_prefix(prefix.as_str()) { - let mut parts = parts.splitn(2, '/'); - - let guild_id = parts.next()?.parse().ok()?; - let channel_id = parts.next()?.parse().ok()?; - return Some((guild_id, channel_id)); - } - } - None -} - -const MAX_DOMAIN_LEN: usize = { - let mut max_len = 0; - let mut i = 0; - - while i < DOMAINS.len() { - let cur_len = DOMAINS[i].len(); - if cur_len > max_len { - max_len = cur_len; - } - - i += 1; - } - - max_len -}; diff --git a/src/utils/argument_convert/role.rs b/src/utils/argument_convert/role.rs deleted file mode 100644 index c18319d9a9a..00000000000 --- a/src/utils/argument_convert/role.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`Role::convert`]. -#[non_exhaustive] -#[derive(Debug)] -pub enum RoleParseError { - /// When the operation was invoked outside a guild. - NotInGuild, - /// When the guild's roles were not found in cache. - NotInCache, - /// HTTP error while retrieving guild roles. - Http(SerenityError), - /// The provided channel string failed to parse, or the parsed result cannot be found in the - /// cache. - NotFoundOrMalformed, -} - -impl std::error::Error for RoleParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Http(e) => Some(e), - Self::NotFoundOrMalformed | Self::NotInGuild | Self::NotInCache => None, - } - } -} - -impl fmt::Display for RoleParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotInGuild => f.write_str("Must invoke this operation in a guild"), - Self::NotInCache => f.write_str("Guild's roles were not found in cache"), - Self::Http(_) => f.write_str("Failed to retrieve roles via HTTP"), - Self::NotFoundOrMalformed => f.write_str("Role not found or unknown format"), - } - } -} - -/// Look up a [`Role`] by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID -/// 2. [Lookup by mention](`crate::utils::parse_role_mention`). -/// 3. Lookup by name (case-insensitive) -#[async_trait::async_trait] -impl ArgumentConvert for Role { - type Err = RoleParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - _channel_id: Option, - s: &str, - ) -> Result { - let guild_id = guild_id.ok_or(RoleParseError::NotInGuild)?; - - #[cfg(feature = "cache")] - let guild; - - #[cfg(feature = "cache")] - let roles = { - let cache = ctx.cache().ok_or(RoleParseError::NotInCache)?; - guild = cache.guild(guild_id).ok_or(RoleParseError::NotInCache)?; - &guild.roles - }; - - #[cfg(not(feature = "cache"))] - let roles = ctx.http().get_guild_roles(guild_id).await.map_err(RoleParseError::Http)?; - - if let Some(role_id) = s.parse().ok().or_else(|| crate::utils::parse_role_mention(s)) { - #[cfg(feature = "cache")] - if let Some(role) = roles.get(&role_id) { - return Ok(role.clone()); - } - #[cfg(not(feature = "cache"))] - if let Some(role) = roles.iter().find(|role| role.id == role_id) { - return Ok(role.clone()); - } - } - - #[cfg(feature = "cache")] - if let Some(role) = roles.iter().find(|role| role.name.eq_ignore_ascii_case(s)) { - return Ok(role.clone()); - } - #[cfg(not(feature = "cache"))] - if let Some(role) = roles.into_iter().find(|role| role.name.eq_ignore_ascii_case(s)) { - return Ok(role); - } - - Err(RoleParseError::NotFoundOrMalformed) - } -} diff --git a/src/utils/argument_convert/user.rs b/src/utils/argument_convert/user.rs deleted file mode 100644 index c49db78a360..00000000000 --- a/src/utils/argument_convert/user.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::fmt; - -use super::ArgumentConvert; -use crate::model::prelude::*; -use crate::prelude::*; - -/// Error that can be returned from [`User::convert`]. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum UserParseError { - /// The provided user string failed to parse, or the parsed result cannot be found in the guild - /// cache data. - NotFoundOrMalformed, -} - -impl std::error::Error for UserParseError {} - -impl fmt::Display for UserParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotFoundOrMalformed => f.write_str("User not found or unknown format"), - } - } -} - -/// Look up a user by a string case-insensitively. -/// -/// Requires the cache feature to be enabled. If a user is not in cache, they will not be found! -/// -/// The lookup strategy is as follows (in order): -/// 1. Lookup by ID. -/// 2. [Lookup by mention](`crate::utils::parse_user_mention`). -/// 3. [Lookup by name#discrim](`crate::utils::parse_user_tag`). -/// 4. Lookup by name -#[async_trait::async_trait] -impl ArgumentConvert for User { - type Err = UserParseError; - - async fn convert( - ctx: impl CacheHttp, - guild_id: Option, - channel_id: Option, - s: &str, - ) -> Result { - // Convert as a Member which uses HTTP endpoints instead of cache - if let Ok(member) = Member::convert(&ctx, guild_id, channel_id, s).await { - return Ok(member.user); - } - - // If string is a raw user ID or a mention - if let Some(user_id) = s.parse().ok().or_else(|| crate::utils::parse_user_mention(s)) { - // Now, we can still try UserId::to_user because it works for all users from all guilds - // the bot is joined - if let Ok(user) = user_id.to_user(&ctx).await { - return Ok(user); - } - } - - Err(UserParseError::NotFoundOrMalformed) - } -}