diff --git a/Cargo.lock b/Cargo.lock index 3dab373b..7e99f8a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,7 +229,7 @@ dependencies = [ "async-channel 1.6.1", "async-executor", "async-io", - "async-lock", + "async-lock 2.8.0", "blocking", "futures-lite", "once_cell", @@ -265,6 +265,17 @@ dependencies = [ "event-listener 2.5.2", ] +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite 0.2.13", +] + [[package]] name = "async-process" version = "1.4.0" @@ -303,7 +314,7 @@ dependencies = [ "async-channel 1.6.1", "async-global-executor", "async-io", - "async-lock", + "async-lock 2.8.0", "async-process", "crossbeam-utils", "futures-channel", @@ -435,7 +446,7 @@ dependencies = [ "nanoid", "rand 0.8.5", "regex", - "reqwest", + "reqwest 0.11.10", "revolt_okapi", "revolt_rocket_okapi", "rocket", @@ -899,6 +910,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes 1.5.0", + "futures-util", + "headers", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite 0.2.13", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum_typed_multipart" version = "0.12.1" @@ -1407,6 +1441,24 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.5" @@ -1997,7 +2049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc9ed4ed7618b2f7581edb6ce28794a10fcc335e196fc2bcf56afee99ad63d10" dependencies = [ "async-trait", - "reqwest", + "reqwest 0.11.10", "serde", "serde_json", "yup-oauth2", @@ -2441,6 +2493,30 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.3", + "bytes 1.5.0", + "headers-core", + "http 1.1.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.1.0", +] + [[package]] name = "heck" version = "0.4.0" @@ -2690,6 +2766,22 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes 1.5.0", + "http-body-util", + "hyper 1.3.1", + "hyper-util", + "native-tls", + "tokio 1.35.1", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.5" @@ -2981,11 +3073,11 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin 0.9.8", ] [[package]] @@ -3243,9 +3335,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -3303,6 +3395,30 @@ dependencies = [ "redis 0.22.3", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock 3.4.0", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version 0.4.0", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid 1.4.1", +] + [[package]] name = "mongodb" version = "2.2.2" @@ -4081,6 +4197,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "querystring" version = "1.1.0" @@ -4251,6 +4382,15 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "raw-cpuid" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "rdrand" version = "0.4.0" @@ -4424,7 +4564,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.5", "hyper 0.14.30", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "lazy_static", @@ -4445,6 +4585,48 @@ dependencies = [ "winreg 0.10.1", ] +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.1", + "bytes 1.5.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite 0.2.13", + "rustls-pemfile 2.1.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio 1.35.1", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.52.0", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -4462,6 +4644,7 @@ dependencies = [ "axum", "axum_typed_multipart", "revolt-config", + "revolt-result", "serde", "serde_json", "tempfile", @@ -4522,7 +4705,7 @@ dependencies = [ name = "revolt-database" version = "0.7.16" dependencies = [ - "async-lock", + "async-lock 2.8.0", "async-recursion", "async-std", "async-trait", @@ -4590,7 +4773,7 @@ dependencies = [ "rand 0.8.5", "redis-kiss", "regex", - "reqwest", + "reqwest 0.11.10", "revolt-config", "revolt-database", "revolt-models", @@ -4621,6 +4804,28 @@ dependencies = [ "aws-sdk-s3", ] +[[package]] +name = "revolt-january" +version = "0.7.14" +dependencies = [ + "axum", + "axum-extra", + "lazy_static", + "mime", + "moka", + "reqwest 0.12.4", + "revolt-config", + "revolt-models", + "revolt-result", + "serde", + "serde_json", + "tokio 1.35.1", + "tracing", + "tracing-subscriber", + "utoipa", + "utoipa-scalar", +] + [[package]] name = "revolt-models" version = "0.7.16" @@ -4635,6 +4840,7 @@ dependencies = [ "rocket", "schemars", "serde", + "utoipa", "validator 0.16.0", ] @@ -4680,12 +4886,14 @@ dependencies = [ name = "revolt-result" version = "0.7.16" dependencies = [ + "axum", "revolt_okapi", "revolt_rocket_okapi", "rocket", "schemars", "serde", "serde_json", + "utoipa", ] [[package]] @@ -5325,7 +5533,7 @@ checksum = "01b0ad16faa5d12372f914ed40d00bda21a6d1bdcc99264c5e5e1c9495cf3654" dependencies = [ "httpdate", "native-tls", - "reqwest", + "reqwest 0.11.10", "sentry-backtrace", "sentry-contexts", "sentry-core", @@ -5818,6 +6026,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "take_mut" version = "0.2.2" @@ -6163,6 +6398,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "trust-dns-proto" version = "0.21.2" @@ -7029,6 +7270,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/Dockerfile b/Dockerfile index 23346910..0127c0a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,6 +28,7 @@ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/ +COPY crates/services/january/Cargo.toml ./crates/services/january/ RUN sh /tmp/build-image-layer.sh deps # Build all apps diff --git a/Dockerfile.useCurrentArch b/Dockerfile.useCurrentArch index 994d3935..90aa754c 100644 --- a/Dockerfile.useCurrentArch +++ b/Dockerfile.useCurrentArch @@ -24,6 +24,7 @@ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/ +COPY crates/services/january/Cargo.toml ./crates/services/january/ RUN sh /tmp/build-image-layer.sh deps # Build all apps diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 773dce15..77f9f54a 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -11,9 +11,10 @@ description = "Revolt Backend: API Models" [features] serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"] schemas = ["dep:schemars", "revolt-permissions/schemas"] +utoipa = ["dep:utoipa"] validator = ["dep:validator"] rocket = ["dep:rocket"] -partials = ["dep:revolt_optional_struct", "serde", "schemas"] +partials = ["dep:revolt_optional_struct", "serde", "schemas", "utoipa"] default = ["serde", "partials", "rocket"] @@ -37,6 +38,7 @@ iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] } # Spec Generation schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] } +utoipa = { version = "4.2.3", optional = true } # Validation validator = { version = "0.16.0", optional = true, features = ["derive"] } diff --git a/crates/core/models/src/lib.rs b/crates/core/models/src/lib.rs index 7aa5d4d1..7f3d3d23 100644 --- a/crates/core/models/src/lib.rs +++ b/crates/core/models/src/lib.rs @@ -6,6 +6,10 @@ extern crate serde; #[macro_use] extern crate schemars; +#[cfg(feature = "utoipa")] +#[macro_use] +extern crate utoipa; + #[cfg(feature = "partials")] #[macro_use] extern crate revolt_optional_struct; @@ -18,6 +22,7 @@ macro_rules! auto_derived { $( #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "schemas", derive(JsonSchema))] + #[cfg_attr(feature = "utoipa", derive(ToSchema))] #[derive(Debug, Clone, Eq, PartialEq)] $item )+ diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 11d4ab24..742834fa 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -11,7 +11,9 @@ description = "Revolt Backend: Result and Error types" [features] serde = ["dep:serde"] schemas = ["dep:schemars"] +utoipa = ["dep:utoipa"] rocket = ["dep:rocket", "dep:serde_json"] +axum = ["dep:axum", "dep:serde_json"] okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"] default = ["serde"] @@ -23,8 +25,12 @@ serde = { version = "1", features = ["derive"], optional = true } # Spec Generation schemars = { version = "0.8.8", optional = true } +utoipa = { version = "4.2.3", optional = true } # Rocket rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } revolt_rocket_okapi = { version = "0.9.1", optional = true } revolt_okapi = { version = "0.9.1", optional = true } + +# Axum +axum = { version = "0.7.5", optional = true } diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs new file mode 100644 index 00000000..e42348d6 --- /dev/null +++ b/crates/core/result/src/axum.rs @@ -0,0 +1,81 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; + +use crate::{Error, ErrorType}; + +/// HTTP response builder for Error enum +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let status = match self.error_type { + ErrorType::LabelMe => StatusCode::INTERNAL_SERVER_ERROR, + + ErrorType::AlreadyOnboarded => StatusCode::FORBIDDEN, + + ErrorType::UnknownUser => StatusCode::NOT_FOUND, + ErrorType::InvalidUsername => StatusCode::BAD_REQUEST, + ErrorType::UsernameTaken => StatusCode::CONFLICT, + ErrorType::DiscriminatorChangeRatelimited => StatusCode::TOO_MANY_REQUESTS, + ErrorType::AlreadyFriends => StatusCode::CONFLICT, + ErrorType::AlreadySentRequest => StatusCode::CONFLICT, + ErrorType::Blocked => StatusCode::CONFLICT, + ErrorType::BlockedByOther => StatusCode::FORBIDDEN, + ErrorType::NotFriends => StatusCode::FORBIDDEN, + ErrorType::TooManyPendingFriendRequests { .. } => StatusCode::BAD_REQUEST, + + ErrorType::UnknownChannel => StatusCode::NOT_FOUND, + ErrorType::UnknownMessage => StatusCode::NOT_FOUND, + ErrorType::UnknownAttachment => StatusCode::BAD_REQUEST, + ErrorType::CannotEditMessage => StatusCode::FORBIDDEN, + ErrorType::CannotJoinCall => StatusCode::BAD_REQUEST, + ErrorType::TooManyAttachments { .. } => StatusCode::BAD_REQUEST, + ErrorType::TooManyReplies { .. } => StatusCode::BAD_REQUEST, + ErrorType::EmptyMessage => StatusCode::UNPROCESSABLE_ENTITY, + ErrorType::PayloadTooLarge => StatusCode::UNPROCESSABLE_ENTITY, + ErrorType::CannotRemoveYourself => StatusCode::BAD_REQUEST, + ErrorType::GroupTooLarge { .. } => StatusCode::FORBIDDEN, + ErrorType::AlreadyInGroup => StatusCode::CONFLICT, + ErrorType::NotInGroup => StatusCode::NOT_FOUND, + ErrorType::AlreadyPinned => StatusCode::BAD_REQUEST, + ErrorType::NotPinned => StatusCode::BAD_REQUEST, + + ErrorType::UnknownServer => StatusCode::NOT_FOUND, + ErrorType::InvalidRole => StatusCode::NOT_FOUND, + ErrorType::Banned => StatusCode::FORBIDDEN, + ErrorType::AlreadyInServer => StatusCode::CONFLICT, + + ErrorType::TooManyServers { .. } => StatusCode::BAD_REQUEST, + ErrorType::TooManyEmbeds { .. } => StatusCode::BAD_REQUEST, + ErrorType::TooManyEmoji { .. } => StatusCode::BAD_REQUEST, + ErrorType::TooManyChannels { .. } => StatusCode::BAD_REQUEST, + ErrorType::TooManyRoles { .. } => StatusCode::BAD_REQUEST, + + ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST, + ErrorType::IsBot => StatusCode::BAD_REQUEST, + ErrorType::BotIsPrivate => StatusCode::FORBIDDEN, + + ErrorType::CannotReportYourself => StatusCode::BAD_REQUEST, + + ErrorType::MissingPermission { .. } => StatusCode::FORBIDDEN, + ErrorType::MissingUserPermission { .. } => StatusCode::FORBIDDEN, + ErrorType::NotElevated => StatusCode::FORBIDDEN, + ErrorType::NotPrivileged => StatusCode::FORBIDDEN, + ErrorType::CannotGiveMissingPermissions => StatusCode::FORBIDDEN, + ErrorType::NotOwner => StatusCode::FORBIDDEN, + + ErrorType::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR, + ErrorType::InternalError => StatusCode::INTERNAL_SERVER_ERROR, + ErrorType::InvalidOperation => StatusCode::BAD_REQUEST, + ErrorType::InvalidCredentials => StatusCode::UNAUTHORIZED, + ErrorType::InvalidProperty => StatusCode::BAD_REQUEST, + ErrorType::InvalidSession => StatusCode::UNAUTHORIZED, + ErrorType::DuplicateNonce => StatusCode::CONFLICT, + ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST, + ErrorType::NotFound => StatusCode::NOT_FOUND, + ErrorType::NoEffect => StatusCode::OK, + ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST, + + ErrorType::ProxyError => StatusCode::BAD_REQUEST, + }; + + (status, Json(&self)).into_response() + } +} diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 328e0752..9e0579de 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -8,9 +8,16 @@ extern crate serde; #[macro_use] extern crate schemars; +#[cfg(feature = "utoipa")] +#[macro_use] +extern crate utoipa; + #[cfg(feature = "rocket")] pub mod rocket; +#[cfg(feature = "axum")] +pub mod axum; + #[cfg(feature = "okapi")] pub mod okapi; @@ -20,6 +27,7 @@ pub type Result = std::result::Result; /// Error information #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "schemas", derive(JsonSchema))] +#[cfg_attr(feature = "utoipa", derive(ToSchema))] #[derive(Debug, Clone)] pub struct Error { /// Type of error and additional information @@ -42,6 +50,7 @@ impl std::error::Error for Error {} #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(tag = "type"))] #[cfg_attr(feature = "schemas", derive(JsonSchema))] +#[cfg_attr(feature = "utoipa", derive(ToSchema))] #[derive(Debug, Clone)] pub enum ErrorType { /// This error was not labeled :( @@ -145,6 +154,9 @@ pub enum ErrorType { error: String, }, + // ? Micro-service errors + ProxyError, + // ? Legacy errors VosoUnavailable, } diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 3e9e71c1..58b54689 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -78,6 +78,8 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::NotFound => Status::NotFound, ErrorType::NoEffect => Status::Ok, ErrorType::FailedValidation { .. } => Status::BadRequest, + + ErrorType::ProxyError => Status::BadRequest, }; // Serialize the error data structure into JSON. diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index dda8c741..8eb1c614 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -4,19 +4,29 @@ version = "0.7.14" edition = "2021" [dependencies] +# Serialisation serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.68" +# Async runtime tokio = { version = "1.0", features = ["full"] } +# Logging tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +# Core crates revolt-config = { version = "0.7.16", path = "../../core/config" } +revolt-result = { version = "0.7.16", path = "../../core/result", features = [ + "utoipa", + "axum", +] } +# Axum / web server tempfile = "3.12.0" axum_typed_multipart = "0.12.1" axum = { version = "0.7.5", features = ["multipart"] } +# OpenAPI & documentation generation utoipa-scalar = { version = "0.1.0", features = ["axum"] } utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] } diff --git a/crates/services/autumn/src/main.rs b/crates/services/autumn/src/main.rs index 3c991bbd..118cb062 100644 --- a/crates/services/autumn/src/main.rs +++ b/crates/services/autumn/src/main.rs @@ -28,6 +28,8 @@ async fn main() -> Result<(), std::io::Error> { ), components( schemas( + revolt_result::Error, + revolt_result::ErrorType, api::RootResponse, api::Tag, api::UploadPayload, diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml new file mode 100644 index 00000000..242e4696 --- /dev/null +++ b/crates/services/january/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "revolt-january" +version = "0.7.14" +edition = "2021" + +[dependencies] +# Utility +mime = "0.3.17" +lazy_static = "1.5.0" +moka = { version = "0.12.8", features = ["future"] } + +# Serialisation +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.68" + +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Web requests +reqwest = { version = "0.12", features = ["json"] } + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# Core crates +revolt-config = { version = "0.7.16", path = "../../core/config" } +revolt-models = { version = "0.7.16", path = "../../core/models" } +revolt-result = { version = "0.7.16", path = "../../core/result", features = [ + "utoipa", + "axum", +] } + +# Axum / web server +axum = { version = "0.7.5" } +axum-extra = { version = "0.9", features = ["typed-header"] } + +# OpenAPI & documentation generation +utoipa-scalar = { version = "0.1.0", features = ["axum"] } +utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] } diff --git a/crates/services/january/src/api.rs b/crates/services/january/src/api.rs new file mode 100644 index 00000000..cba288f5 --- /dev/null +++ b/crates/services/january/src/api.rs @@ -0,0 +1,85 @@ +use axum::{body::Bytes, extract::Query, routing::get, Json, Router}; +use revolt_models::v0::Embed; +use revolt_result::Result; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; + +use crate::requests::Request; + +pub async fn router() -> Router { + Router::new() + .route("/", get(root)) + .route("/proxy", get(proxy)) + .route("/embed", get(embed)) +} + +/// Successful root response +#[derive(Serialize, Debug, ToSchema)] +pub struct RootResponse { + january: &'static str, + version: &'static str, +} + +/// Capture crate version from Cargo +static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Root response from service +#[utoipa::path( + get, + path = "/", + responses( + (status = 200, description = "Echo response", body = RootResponse) + ) +)] +async fn root() -> Json { + Json(RootResponse { + january: "Hello, I am a media proxy server!", + version: CRATE_VERSION, + }) +} + +#[derive(Deserialize)] +struct UrlQuery { + url: String, +} + +/// Proxy a given URL and load media +#[utoipa::path( + get, + path = "/proxy", + responses( + (status = 200, description = "Requested media file", body = Vec) + ), + params( + ("url" = String, Query, description = "URL to fetch") + ), +)] +async fn proxy(Query(UrlQuery { url }): Query) -> Result { + Request::proxy_file(&url).await +} + +/// Generate embed for a given URL +#[utoipa::path( + get, + path = "/embed", + responses( + (status = 200, description = "Generated embed information", body = Embed) + ), + params( + ("url" = String, Query, description = "URL to fetch") + ), + security( + ("api_key" = []) + ) +)] +async fn embed( + Query(UrlQuery { url }): Query, + TypedHeader(Authorization(_bearer)): TypedHeader>, +) -> Result> { + Request::generate_embed(&url).await.map(Json) +} diff --git a/crates/services/january/src/main.rs b/crates/services/january/src/main.rs new file mode 100644 index 00000000..1d72fabc --- /dev/null +++ b/crates/services/january/src/main.rs @@ -0,0 +1,71 @@ +use std::net::{Ipv4Addr, SocketAddr}; + +use axum::Router; + +use tokio::net::TcpListener; +use utoipa::{ + openapi::security::{Http, HttpAuthScheme, SecurityScheme}, + Modify, OpenApi, +}; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; + +mod api; +pub mod requests; + +#[tokio::main] +async fn main() -> Result<(), std::io::Error> { + // Configure logging and environment + revolt_config::configure!(api); + + // Configure API schema + #[derive(OpenApi)] + #[openapi( + modifiers(&SecurityAddon), + paths( + api::root, + api::proxy, + api::embed + ), + components( + schemas( + api::RootResponse, + revolt_result::Error, + revolt_result::ErrorType, + revolt_models::v0::ImageSize, + revolt_models::v0::Image, + revolt_models::v0::Video, + revolt_models::v0::TwitchType, + revolt_models::v0::LightspeedType, + revolt_models::v0::BandcampType, + revolt_models::v0::Special, + revolt_models::v0::WebsiteMetadata, + revolt_models::v0::Text, + revolt_models::v0::Embed + ) + ) + )] + struct ApiDoc; + + struct SecurityAddon; + + impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_scheme( + "api_key", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ) + } + } + } + + // Configure Axum and router + let app = Router::new() + .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) + .nest("/", api::router().await); + + // Configure TCP listener and bind + let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 3000)); + let listener = TcpListener::bind(&address).await?; + axum::serve(listener, app.into_make_service()).await +} diff --git a/crates/services/january/src/requests.rs b/crates/services/january/src/requests.rs new file mode 100644 index 00000000..a25c3918 --- /dev/null +++ b/crates/services/january/src/requests.rs @@ -0,0 +1,91 @@ +use std::time::Duration; + +use axum::body::Bytes; +use lazy_static::lazy_static; +use mime::Mime; +use reqwest::{header::CONTENT_TYPE, redirect, Client, Response}; +use revolt_models::v0::Embed; +use revolt_result::{create_error, Result}; + +lazy_static! { + static ref CLIENT: Client = reqwest::Client::builder() + .user_agent("Mozilla/5.0 (compatible; January/2.0; +https://github.com/revoltchat/backend)") + .timeout(Duration::from_secs(10)) // TODO config + .connect_timeout(Duration::from_secs(5)) // TODO config + .redirect(redirect::Policy::custom(|attempt| { + if attempt.previous().len() > 5 { // TODO config + attempt.error("too many redirects") + } else if attempt.url().host_str() == Some("jan.revolt.chat") { // TODO config + attempt.stop() + } else { + attempt.follow() + } + })) + .build() + .expect("reqwest Client"); + + /// Cache for proxy results + static ref PROXY_CACHE: moka::future::Cache> = moka::future::Cache::builder() + .max_capacity(10_000) // TODO config + .time_to_live(Duration::from_secs(60)) // TODO config + .build(); + + /// Cache for embed results + static ref EMBED_CACHE: moka::future::Cache> = moka::future::Cache::builder() + .max_capacity(1_000) // TODO config + .time_to_live(Duration::from_secs(60)) // TODO config + .build(); +} + +/// Information about a successful request +pub struct Request { + response: Response, + mime: Mime, +} + +impl Request { + /// Proxy a given URL + pub async fn proxy_file(url: &str) -> Result { + if let Some(hit) = PROXY_CACHE.get(url).await { + hit + } else { + todo!() + } + } + + /// Generate embed for a given URL + pub async fn generate_embed(url: &str) -> Result { + if let Some(hit) = EMBED_CACHE.get(url).await { + hit + } else { + todo!() + } + } + + /// Send a new request to a service + pub async fn new(url: &str) -> Result { + let response = CLIENT + .get(url) + .send() + .await + .map_err(|_| create_error!(ProxyError))?; + + if !response.status().is_success() { + tracing::error!("{:?}", response); + return Err(create_error!(ProxyError)); + } + + let content_type = response + .headers() + .get(CONTENT_TYPE) + .ok_or(create_error!(ProxyError))? + .to_str() + .map_err(|_| create_error!(ProxyError))?; + + let mime: mime::Mime = content_type + .parse() + .map_err(|_| create_error!(ProxyError))?; + + Ok(Request { response, mime }) + } +} diff --git a/scripts/build-image-layer.sh b/scripts/build-image-layer.sh index f6d0b22f..5043b09b 100644 --- a/scripts/build-image-layer.sh +++ b/scripts/build-image-layer.sh @@ -32,11 +32,13 @@ deps() { crates/core/permissions/src \ crates/core/presence/src \ crates/core/result/src \ - crates/services/autumn/src + crates/services/autumn/src \ + crates/services/january/src echo 'fn main() { panic!("stub"); }' | tee crates/bonfire/src/main.rs | tee crates/delta/src/main.rs | - tee crates/services/autumn/src/main.rs + tee crates/services/autumn/src/main.rs | + tee crates/services/january/src/main.rs echo '' | tee crates/bindings/node/src/lib.rs | tee crates/core/config/src/lib.rs |