diff --git a/Cargo.lock b/Cargo.lock index abe673cd8..9c79894c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2872,6 +2872,7 @@ version = "0.19.3" dependencies = [ "actix-web", "anyhow", + "cfg-if", "deser-hjson", "diesel", "doku", diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index e61d92b78..2a68c3c5d 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -13,42 +13,95 @@ name = "lemmy_utils" path = "src/lib.rs" doctest = false +[[bin]] +name = "lemmy_util_bin" +path = "src/main.rs" +required-features = ["default"] + [lints] workspace = true [features] -full = ["ts-rs"] +default = [ + "error", + "request", + "response", + "settings", + "misc", + "rate-limit", + "apub", + "cache-header", + "email" +] +full = ["default", "ts-rs"] +ts-rs = ["dep:ts-rs"] +error-type = ["dep:serde", "dep:strum"] +error = [ + "error-type", + "dep:serde_json", + "dep:anyhow", + "dep:tracing-error", + "dep:diesel", + "dep:http", + "dep:actix-web" +] +request = ["dep:reqwest-middleware", "dep:tracing", "dep:strum"] +response = ["error", "dep:actix-web"] +settings = [ + "error", + "dep:deser-hjson", + "dep:regex", + "dep:urlencoding", + "dep:doku", + "dep:url", + "dep:once_cell", + "dep:smart-default" +] +rate-limit = [ + "error", + "dep:enum-map", + "dep:tracing", + "dep:actix-web", + "dep:futures", + "dep:tokio", + "dep:once_cell" +] +apub = ["dep:openssl"] +cache-header = ["dep:actix-web"] +email = ["error", "settings", "dep:html2text", "dep:lettre", "dep:uuid", "dep:rosetta-i18n"] +misc = ["settings", "dep:itertools", "dep:markdown-it", "dep:tokio", "dep:futures", "dep:tracing"] [dependencies] -regex = { workspace = true } -tracing = { workspace = true } -tracing-error = { workspace = true } -itertools = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -once_cell = { workspace = true } -url = { workspace = true } -actix-web = { workspace = true } -anyhow = { workspace = true } -reqwest-middleware = { workspace = true } -strum = { workspace = true } +regex = { workspace = true, optional = true } +tracing = { workspace = true, optional = true } +tracing-error = { workspace = true, optional = true } +itertools = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +once_cell = { workspace = true, optional = true } +url = { workspace = true, optional = true } +actix-web = { workspace = true, optional = true } +anyhow = { workspace = true, optional = true } +reqwest-middleware = { workspace = true, optional = true } +strum = { workspace = true, optional = true } strum_macros = { workspace = true } -futures = { workspace = true } -diesel = { workspace = true, features = ["chrono"] } -http = { workspace = true } -doku = { workspace = true, features = ["url-2"] } -uuid = { workspace = true, features = ["serde", "v4"] } -rosetta-i18n = { workspace = true } -tokio = { workspace = true } -urlencoding = { workspace = true } -openssl = "0.10.63" -html2text = "0.6.0" -deser-hjson = "2.2.4" -smart-default = "0.7.1" -lettre = { version = "0.11.3", features = ["tokio1", "tokio1-native-tls"] } -markdown-it = "0.6.0" +futures = { workspace = true, optional = true } +diesel = { workspace = true, features = ["chrono"], optional = true } +http = { workspace = true, optional = true } +doku = { workspace = true, features = ["url-2"], optional = true } +uuid = { workspace = true, features = ["serde", "v4"], optional = true} +rosetta-i18n = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } +urlencoding = { workspace = true, optional = true } +openssl = { version = "0.10.63", optional = true } +html2text = { version = "0.6.0", optional = true } +deser-hjson = { version = "2.2.4", optional = true } +smart-default = { version = "0.7.1", optional = true } +lettre = { version = "0.11.3", features = ["tokio1", "tokio1-native-tls"], optional = true } +markdown-it = { version = "0.6.0", optional = true } ts-rs = { workspace = true, optional = true } -enum-map = { workspace = true } +enum-map = { workspace = true, optional = true } +cfg-if = "1" [dev-dependencies] reqwest = { workspace = true } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index d05b8c3a7..d42a168b9 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -1,78 +1,13 @@ +use cfg_if::cfg_if; use serde::{Deserialize, Serialize}; -use std::{ - fmt, - fmt::{Debug, Display}, -}; -use tracing_error::SpanTrace; -#[cfg(feature = "full")] +use std::fmt::Debug; +use strum_macros::{Display, EnumIter}; +#[cfg(feature = "ts-rs")] use ts_rs::TS; -pub type LemmyResult = Result; - -pub struct LemmyError { - pub error_type: LemmyErrorType, - pub inner: anyhow::Error, - pub context: SpanTrace, -} - -/// Maximum number of items in an array passed as API parameter. See [[LemmyErrorType::TooManyItems]] -pub const MAX_API_PARAM_ELEMENTS: usize = 10_000; - -impl From for LemmyError -where - T: Into, -{ - fn from(t: T) -> Self { - let cause = t.into(); - LemmyError { - error_type: LemmyErrorType::Unknown(format!("{}", &cause)), - inner: cause, - context: SpanTrace::capture(), - } - } -} - -impl Debug for LemmyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("LemmyError") - .field("message", &self.error_type) - .field("inner", &self.inner) - .field("context", &self.context) - .finish() - } -} - -impl Display for LemmyError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}: ", &self.error_type)?; - // print anyhow including trace - // https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations - // this will print the anyhow trace (only if it exists) - // and if RUST_BACKTRACE=1, also a full backtrace - writeln!(f, "{:?}", self.inner)?; - fmt::Display::fmt(&self.context, f) - } -} - -impl actix_web::error::ResponseError for LemmyError { - fn status_code(&self) -> http::StatusCode { - if self.error_type == LemmyErrorType::IncorrectLogin { - return http::StatusCode::UNAUTHORIZED; - } - match self.inner.downcast_ref::() { - Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND, - _ => http::StatusCode::BAD_REQUEST, - } - } - - fn error_response(&self) -> actix_web::HttpResponse { - actix_web::HttpResponse::build(self.status_code()).json(&self.error_type) - } -} - #[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, EnumIter)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] +#[cfg_attr(feature = "ts-rs", derive(TS))] +#[cfg_attr(feature = "ts-rs", ts(export))] #[serde(tag = "error", content = "message", rename_all = "snake_case")] // TODO: order these based on the crate they belong to (utils, federation, db, api) pub enum LemmyErrorType { @@ -231,6 +166,74 @@ pub enum LemmyErrorType { Unknown(String), } +cfg_if! { + if #[cfg(feature = "error")] { + +use tracing_error::SpanTrace; +use std::fmt; + pub type LemmyResult = Result; + +pub struct LemmyError { + pub error_type: LemmyErrorType, + pub inner: anyhow::Error, + pub context: SpanTrace, +} + +/// Maximum number of items in an array passed as API parameter. See [[LemmyErrorType::TooManyItems]] +pub const MAX_API_PARAM_ELEMENTS: usize = 10_000; + +impl From for LemmyError +where + T: Into, +{ + fn from(t: T) -> Self { + let cause = t.into(); + LemmyError { + error_type: LemmyErrorType::Unknown(format!("{}", &cause)), + inner: cause, + context: SpanTrace::capture(), + } + } +} + +impl Debug for LemmyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("LemmyError") + .field("message", &self.error_type) + .field("inner", &self.inner) + .field("context", &self.context) + .finish() + } +} + +impl fmt::Display for LemmyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}: ", &self.error_type)?; + // print anyhow including trace + // https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations + // this will print the anyhow trace (only if it exists) + // and if RUST_BACKTRACE=1, also a full backtrace + writeln!(f, "{:?}", self.inner)?; + fmt::Display::fmt(&self.context, f) + } +} + +impl actix_web::error::ResponseError for LemmyError { + fn status_code(&self) -> http::StatusCode { + if self.error_type == LemmyErrorType::IncorrectLogin { + return http::StatusCode::UNAUTHORIZED; + } + match self.inner.downcast_ref::() { + Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND, + _ => http::StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> actix_web::HttpResponse { + actix_web::HttpResponse::build(self.status_code()).json(&self.error_type) + } +} + impl From for LemmyError { fn from(error_type: LemmyErrorType) -> Self { let inner = anyhow::anyhow!("{}", error_type); @@ -273,6 +276,9 @@ impl LemmyErrorExt2 for Result { } } + } +} + #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 6f261febd..66e50787b 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,23 +1,24 @@ -#[macro_use] -extern crate strum_macros; -#[macro_use] -extern crate smart_default; - +#[cfg(feature = "apub")] pub mod apub; +#[cfg(feature = "cache-header")] pub mod cache_header; +#[cfg(feature = "email")] pub mod email; +#[cfg(feature = "error-type")] pub mod error; +#[cfg(feature = "rate-limit")] pub mod rate_limit; +#[cfg(feature = "request")] pub mod request; +#[cfg(feature = "response")] pub mod response; +#[cfg(feature = "settings")] pub mod settings; +#[cfg(feature = "misc")] pub mod utils; pub mod version; -use error::LemmyError; -use futures::Future; use std::time::Duration; -use tracing::Instrument; pub type ConnectionId = usize; @@ -35,10 +36,14 @@ macro_rules! location_info { }; } +#[cfg(feature = "misc")] /// tokio::spawn, but accepts a future that may fail and also /// * logs errors /// * attaches the spawned task to the tracing span of the caller for better logging -pub fn spawn_try_task(task: impl Future> + Send + 'static) { +pub fn spawn_try_task( + task: impl futures::Future> + Send + 'static, +) { + use tracing::Instrument; tokio::spawn( async { if let Err(e) = task.await { diff --git a/crates/utils/src/rate_limit/mod.rs b/crates/utils/src/rate_limit/mod.rs index b2efbe2c9..2b5e951bb 100644 --- a/crates/utils/src/rate_limit/mod.rs +++ b/crates/utils/src/rate_limit/mod.rs @@ -2,8 +2,7 @@ use crate::error::{LemmyError, LemmyErrorType}; use actix_web::dev::{ConnectionInfo, Service, ServiceRequest, ServiceResponse, Transform}; use enum_map::{enum_map, EnumMap}; use futures::future::{ok, Ready}; -pub use rate_limiter::{ActionType, BucketConfig}; -use rate_limiter::{InstantSecs, RateLimitState}; +use rate_limiter::{ActionType, BucketConfig, InstantSecs, RateLimitState}; use std::{ future::Future, net::{IpAddr, Ipv4Addr, SocketAddr}, diff --git a/crates/utils/src/rate_limit/rate_limiter.rs b/crates/utils/src/rate_limit/rate_limiter.rs index 93ef1beec..7c68003d8 100644 --- a/crates/utils/src/rate_limit/rate_limiter.rs +++ b/crates/utils/src/rate_limit/rate_limiter.rs @@ -6,6 +6,7 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, time::Instant, }; +use strum_macros::AsRefStr; use tracing::debug; static START_TIME: Lazy = Lazy::new(Instant::now); diff --git a/crates/utils/src/settings/mod.rs b/crates/utils/src/settings/mod.rs index 4642a67cf..f630d0217 100644 --- a/crates/utils/src/settings/mod.rs +++ b/crates/utils/src/settings/mod.rs @@ -1,8 +1,4 @@ -use crate::{ - error::LemmyError, - location_info, - settings::structs::{PictrsConfig, Settings}, -}; +use crate::{error::LemmyError, location_info}; use anyhow::{anyhow, Context}; use deser_hjson::from_str; use once_cell::sync::Lazy; @@ -12,8 +8,7 @@ use urlencoding::encode; pub mod structs; -use crate::settings::structs::PictrsImageMode; -use structs::DatabaseConnection; +use structs::{DatabaseConnection, PictrsConfig, PictrsImageMode, Settings}; static DEFAULT_CONFIG_FILE: &str = "config/config.hjson"; diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index 46e9b747c..4a8d8afb6 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -1,5 +1,6 @@ use doku::Document; use serde::{Deserialize, Serialize}; +use smart_default::SmartDefault; use std::{ env, net::{IpAddr, Ipv4Addr},