diff --git a/.idea/code.iml b/.idea/code.iml index 70105a514..5fda4410f 100644 --- a/.idea/code.iml +++ b/.idea/code.iml @@ -10,6 +10,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index 8dffdfd5a..741985a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1204,7 +1204,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" dependencies = [ "byteorder", "fnv", - "uuid 1.10.0", + "uuid 1.12.0", ] [[package]] @@ -1291,7 +1291,7 @@ dependencies = [ "time", "tokio 1.42.0", "url", - "uuid 1.10.0", + "uuid 1.12.0", ] [[package]] @@ -1995,7 +1995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ "serde", - "uuid 1.10.0", + "uuid 1.12.0", ] [[package]] @@ -2511,7 +2511,7 @@ checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" dependencies = [ "bit_field", "flume", - "half", + "half 2.4.1", "lebe", "miniz_oxide 0.7.4", "rayon-core", @@ -3242,6 +3242,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "half" version = "2.4.1" @@ -4245,6 +4251,7 @@ dependencies = [ "deadpool-redis", "derive-new", "dotenvy", + "either", "env_logger", "flate2", "futures 0.3.30", @@ -4270,6 +4277,7 @@ dependencies = [ "redis", "regex", "reqwest 0.11.27", + "rust-common", "rust-s3", "rust_decimal", "rust_iso3166", @@ -4277,6 +4285,7 @@ dependencies = [ "sentry", "sentry-actix", "serde", + "serde_cbor", "serde_json", "serde_with", "sha1 0.6.1", @@ -4290,7 +4299,7 @@ dependencies = [ "totp-rs", "url", "urlencoding", - "uuid 1.10.0", + "uuid 1.12.0", "validator", "webp", "woothee", @@ -4657,7 +4666,7 @@ dependencies = [ "serde_json", "thiserror 1.0.64", "time", - "uuid 1.10.0", + "uuid 1.12.0", "wasm-bindgen-futures", "web-sys", "yaup", @@ -6724,7 +6733,7 @@ dependencies = [ "rkyv_derive", "seahash", "tinyvec", - "uuid 1.10.0", + "uuid 1.12.0", ] [[package]] @@ -6758,6 +6767,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-common" +version = "0.1.0" +dependencies = [ + "actix-ws", + "chrono", + "either", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_cbor", + "serde_json", + "thiserror 2.0.7", + "uuid 1.12.0", +] + [[package]] name = "rust-ini" version = "0.18.0" @@ -7108,7 +7133,7 @@ dependencies = [ "serde", "serde_json", "url", - "uuid 1.10.0", + "uuid 1.12.0", ] [[package]] @@ -7361,7 +7386,7 @@ dependencies = [ "thiserror 1.0.64", "time", "url", - "uuid 1.10.0", + "uuid 1.12.0", ] [[package]] @@ -7396,6 +7421,25 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half 1.8.3", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.210" @@ -8536,7 +8580,7 @@ dependencies = [ "thiserror 2.0.7", "time", "url", - "uuid 1.10.0", + "uuid 1.12.0", "walkdir", ] @@ -8629,7 +8673,7 @@ dependencies = [ "thiserror 2.0.7", "toml 0.8.19", "url", - "uuid 1.10.0", + "uuid 1.12.0", ] [[package]] @@ -8810,7 +8854,7 @@ dependencies = [ "toml 0.8.19", "url", "urlpattern", - "uuid 1.10.0", + "uuid 1.12.0", "walkdir", ] @@ -8884,6 +8928,7 @@ dependencies = [ "dirs 5.0.1", "discord-rich-presence", "dunce", + "either", "flate2", "futures 0.3.30", "indicatif", @@ -8895,6 +8940,7 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest 0.12.7", + "rust-common", "serde", "serde_ini", "serde_json", @@ -8913,7 +8959,7 @@ dependencies = [ "tracing-subscriber", "url", "urlencoding", - "uuid 1.10.0", + "uuid 1.12.0", "whoami", "winreg 0.52.0", "zip 0.6.6", @@ -8955,7 +9001,7 @@ dependencies = [ "tracing", "tracing-error", "url", - "uuid 1.10.0", + "uuid 1.12.0", "window-shadows", ] @@ -8974,7 +9020,7 @@ dependencies = [ "tracing-error", "tracing-subscriber", "url", - "uuid 1.10.0", + "uuid 1.12.0", "webbrowser", ] @@ -9819,9 +9865,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ "getrandom 0.2.15", "rand 0.8.5", diff --git a/Cargo.toml b/Cargo.toml index 06487ac14..15731ab15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,12 @@ resolver = '2' members = [ './packages/app-lib', + './packages/rust-common', './apps/app-playground', './apps/app', './apps/labrinth', - './apps/daedalus_client', - './packages/daedalus', + './apps/daedalus_client', + './packages/daedalus', ] # Optimize for speed and reduce size on release builds @@ -21,4 +22,4 @@ strip = true # Remove debug symbols opt-level = 3 [patch.crates-io] -wry = { git = "https://github.com/modrinth/wry", rev ="cdbf938" } \ No newline at end of file +wry = { git = "https://github.com/modrinth/wry", rev ="cdbf938" } diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 376045a0a..9e4025190 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -37,6 +37,7 @@ hyper = { version = "0.14", features = ["full"] } hyper-tls = "0.5.0" serde_json = "1.0" +serde_cbor = "0.11" serde = { version = "1.0", features = ["derive"] } serde_with = "3.0.0" chrono = { version = "0.4.26", features = ["serde"] } @@ -74,6 +75,7 @@ dotenvy = "0.15.7" log = "0.4.20" env_logger = "0.10.1" thiserror = "1.0.56" +either = "1.13" sqlx = { version = "0.8.2", features = [ "runtime-tokio-rustls", @@ -124,6 +126,8 @@ async-stripe = { version = "0.39.1", features = ["runtime-tokio-hyper-rustls"] } rusty-money = "0.4.1" json-patch = "*" +rust-common = { path = "../../packages/rust-common" } + [dev-dependencies] actix-http = "3.4.0" diff --git a/apps/labrinth/src/auth/mod.rs b/apps/labrinth/src/auth/mod.rs index 30eca4d15..81f9824cc 100644 --- a/apps/labrinth/src/auth/mod.rs +++ b/apps/labrinth/src/auth/mod.rs @@ -34,7 +34,7 @@ pub enum AuthenticationError { #[error("Error uploading user profile picture")] FileHosting(#[from] FileHostingError), #[error("Error while decoding PAT: {0}")] - Decoding(#[from] crate::models::ids::DecodingError), + Decoding(#[from] rust_common::ids::DecodingError), #[error("{0}")] Mail(#[from] email::MailError), #[error("Invalid Authentication Credentials")] diff --git a/apps/labrinth/src/auth/oauth/errors.rs b/apps/labrinth/src/auth/oauth/errors.rs index dab6ff850..2d27384d4 100644 --- a/apps/labrinth/src/auth/oauth/errors.rs +++ b/apps/labrinth/src/auth/oauth/errors.rs @@ -1,9 +1,9 @@ use super::ValidatedRedirectUri; use crate::auth::AuthenticationError; use crate::models::error::ApiError; -use crate::models::ids::DecodingError; use actix_web::http::{header::LOCATION, StatusCode}; use actix_web::HttpResponse; +use rust_common::ids::DecodingError; #[derive(thiserror::Error, Debug)] #[error("{}", .error_type)] diff --git a/apps/labrinth/src/database/models/ids.rs b/apps/labrinth/src/database/models/ids.rs index aa1b99895..1f23e6ebe 100644 --- a/apps/labrinth/src/database/models/ids.rs +++ b/apps/labrinth/src/database/models/ids.rs @@ -1,9 +1,9 @@ use super::DatabaseError; -use crate::models::ids::base62_impl::to_base62; -use crate::models::ids::{random_base62_rng, random_base62_rng_range}; use censor::Censor; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; +use rust_common::ids::base62_impl::to_base62; +use rust_common::ids::{random_base62_rng, random_base62_rng_range}; use serde::{Deserialize, Serialize}; use sqlx::sqlx_macros::Type; diff --git a/apps/labrinth/src/database/models/organization_item.rs b/apps/labrinth/src/database/models/organization_item.rs index b01052776..ee9fbca13 100644 --- a/apps/labrinth/src/database/models/organization_item.rs +++ b/apps/labrinth/src/database/models/organization_item.rs @@ -1,8 +1,7 @@ -use crate::{ - database::redis::RedisPool, models::ids::base62_impl::parse_base62, -}; +use crate::database::redis::RedisPool; use dashmap::DashMap; use futures::TryStreamExt; +use rust_common::ids::base62_impl::parse_base62; use std::fmt::{Debug, Display}; use std::hash::Hash; diff --git a/apps/labrinth/src/database/models/pat_item.rs b/apps/labrinth/src/database/models/pat_item.rs index 205a70e4b..59c1ca842 100644 --- a/apps/labrinth/src/database/models/pat_item.rs +++ b/apps/labrinth/src/database/models/pat_item.rs @@ -1,11 +1,11 @@ use super::ids::*; use crate::database::models::DatabaseError; use crate::database::redis::RedisPool; -use crate::models::ids::base62_impl::parse_base62; use crate::models::pats::Scopes; use chrono::{DateTime, Utc}; use dashmap::DashMap; use futures::TryStreamExt; +use rust_common::ids::base62_impl::parse_base62; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; use std::hash::Hash; diff --git a/apps/labrinth/src/database/models/project_item.rs b/apps/labrinth/src/database/models/project_item.rs index 1bd07d224..e7033c1f7 100644 --- a/apps/labrinth/src/database/models/project_item.rs +++ b/apps/labrinth/src/database/models/project_item.rs @@ -6,12 +6,12 @@ use super::{ids::*, User}; use crate::database::models; use crate::database::models::DatabaseError; use crate::database::redis::RedisPool; -use crate::models::ids::base62_impl::parse_base62; use crate::models::projects::{MonetizationStatus, ProjectStatus}; use chrono::{DateTime, Utc}; use dashmap::{DashMap, DashSet}; use futures::TryStreamExt; use itertools::Itertools; +use rust_common::ids::base62_impl::parse_base62; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; use std::hash::Hash; @@ -300,7 +300,7 @@ impl Project { slug, color, monetization_status, organization_id ) VALUES ( - $1, $2, $3, $4, $5, $6, + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, LOWER($14), $15, $16, $17 diff --git a/apps/labrinth/src/database/models/session_item.rs b/apps/labrinth/src/database/models/session_item.rs index adb1659ea..00316f4cf 100644 --- a/apps/labrinth/src/database/models/session_item.rs +++ b/apps/labrinth/src/database/models/session_item.rs @@ -1,9 +1,9 @@ use super::ids::*; use crate::database::models::DatabaseError; use crate::database::redis::RedisPool; -use crate::models::ids::base62_impl::parse_base62; use chrono::{DateTime, Utc}; use dashmap::DashMap; +use rust_common::ids::base62_impl::parse_base62; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; use std::hash::Hash; diff --git a/apps/labrinth/src/database/models/user_item.rs b/apps/labrinth/src/database/models/user_item.rs index b42fc4651..19f47ed5f 100644 --- a/apps/labrinth/src/database/models/user_item.rs +++ b/apps/labrinth/src/database/models/user_item.rs @@ -3,10 +3,10 @@ use super::{CollectionId, ReportId, ThreadId}; use crate::database::models; use crate::database::models::{DatabaseError, OrganizationId}; use crate::database::redis::RedisPool; -use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::users::Badges; use chrono::{DateTime, Utc}; use dashmap::DashMap; +use rust_common::ids::base62_impl::{parse_base62, to_base62}; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display}; use std::hash::Hash; diff --git a/apps/labrinth/src/database/redis.rs b/apps/labrinth/src/database/redis.rs index cc59ffef7..7507a43f5 100644 --- a/apps/labrinth/src/database/redis.rs +++ b/apps/labrinth/src/database/redis.rs @@ -1,9 +1,9 @@ use super::models::DatabaseError; -use crate::models::ids::base62_impl::{parse_base62, to_base62}; use chrono::{TimeZone, Utc}; use dashmap::DashMap; use deadpool_redis::{Config, Runtime}; use redis::{cmd, Cmd, ExistenceCheck, SetExpiry, SetOptions}; +use rust_common::ids::base62_impl::{parse_base62, to_base62}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/apps/labrinth/src/lib.rs b/apps/labrinth/src/lib.rs index db1416a71..2cd97d8e8 100644 --- a/apps/labrinth/src/lib.rs +++ b/apps/labrinth/src/lib.rs @@ -297,7 +297,7 @@ pub fn app_setup( } let ip_salt = Pepper { - pepper: models::ids::Base62Id(models::ids::random_base62(11)) + pepper: rust_common::ids::Base62Id(rust_common::ids::random_base62(11)) .to_string(), }; diff --git a/apps/labrinth/src/models/v3/ids.rs b/apps/labrinth/src/models/v3/ids.rs index d6530af2f..2cb48274f 100644 --- a/apps/labrinth/src/models/v3/ids.rs +++ b/apps/labrinth/src/models/v3/ids.rs @@ -16,114 +16,10 @@ pub use super::users::UserId; pub use crate::models::billing::{ ChargeId, ProductId, ProductPriceId, UserSubscriptionId, }; -use thiserror::Error; +use rust_common::ids::base62_id_impl; +pub use rust_common::ids::Base62Id; -/// Generates a random 64 bit integer that is exactly `n` characters -/// long when encoded as base62. -/// -/// Uses `rand`'s thread rng on every call. -/// -/// # Panics -/// -/// This method panics if `n` is 0 or greater than 11, since a `u64` -/// can only represent up to 11 character base62 strings -#[inline] -pub fn random_base62(n: usize) -> u64 { - random_base62_rng(&mut rand::thread_rng(), n) -} - -/// Generates a random 64 bit integer that is exactly `n` characters -/// long when encoded as base62, using the given rng. -/// -/// # Panics -/// -/// This method panics if `n` is 0 or greater than 11, since a `u64` -/// can only represent up to 11 character base62 strings -pub fn random_base62_rng(rng: &mut R, n: usize) -> u64 { - random_base62_rng_range(rng, n, n) -} - -pub fn random_base62_rng_range( - rng: &mut R, - n_min: usize, - n_max: usize, -) -> u64 { - use rand::Rng; - assert!(n_min > 0 && n_max <= 11 && n_min <= n_max); - // gen_range is [low, high): max value is `MULTIPLES[n] - 1`, - // which is n characters long when encoded - rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max]) -} - -const MULTIPLES: [u64; 12] = [ - 1, - 62, - 62 * 62, - 62 * 62 * 62, - 62 * 62 * 62 * 62, - 62 * 62 * 62 * 62 * 62, - 62 * 62 * 62 * 62 * 62 * 62, - 62 * 62 * 62 * 62 * 62 * 62 * 62, - 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62, - 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62, - 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62, - u64::MAX, -]; - -/// An ID encoded as base62 for use in the API. -/// -/// All ids should be random and encode to 8-10 character base62 strings, -/// to avoid enumeration and other attacks. -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct Base62Id(pub u64); - -/// An error decoding a number from base62. -#[derive(Error, Debug)] -pub enum DecodingError { - /// Encountered a non-base62 character in a base62 string - #[error("Invalid character {0:?} in base62 encoding")] - InvalidBase62(char), - /// Encountered integer overflow when decoding a base62 id. - #[error("Base62 decoding overflowed")] - Overflow, -} - -macro_rules! from_base62id { - ($($struct:ty, $con:expr;)+) => { - $( - impl From for $struct { - fn from(id: Base62Id) -> $struct { - $con(id.0) - } - } - impl From<$struct> for Base62Id { - fn from(id: $struct) -> Base62Id { - Base62Id(id.0) - } - } - )+ - }; -} - -macro_rules! impl_base62_display { - ($struct:ty) => { - impl std::fmt::Display for $struct { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&base62_impl::to_base62(self.0)) - } - } - }; -} -impl_base62_display!(Base62Id); - -macro_rules! base62_id_impl { - ($struct:ty, $cons:expr) => { - from_base62id!($struct, $cons;); - impl_base62_display!($struct); - } -} base62_id_impl!(ProjectId, ProjectId); -base62_id_impl!(UserId, UserId); base62_id_impl!(VersionId, VersionId); base62_id_impl!(CollectionId, CollectionId); base62_id_impl!(TeamId, TeamId); @@ -143,91 +39,3 @@ base62_id_impl!(ProductId, ProductId); base62_id_impl!(ProductPriceId, ProductPriceId); base62_id_impl!(UserSubscriptionId, UserSubscriptionId); base62_id_impl!(ChargeId, ChargeId); - -pub mod base62_impl { - use serde::de::{self, Deserializer, Visitor}; - use serde::ser::Serializer; - use serde::{Deserialize, Serialize}; - - use super::{Base62Id, DecodingError}; - - impl<'de> Deserialize<'de> for Base62Id { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct Base62Visitor; - - impl Visitor<'_> for Base62Visitor { - type Value = Base62Id; - - fn expecting( - &self, - formatter: &mut std::fmt::Formatter, - ) -> std::fmt::Result { - formatter.write_str("a base62 string id") - } - - fn visit_str(self, string: &str) -> Result - where - E: de::Error, - { - parse_base62(string).map(Base62Id).map_err(E::custom) - } - } - - deserializer.deserialize_str(Base62Visitor) - } - } - - impl Serialize for Base62Id { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&to_base62(self.0)) - } - } - - const BASE62_CHARS: [u8; 62] = - *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - - pub fn to_base62(mut num: u64) -> String { - let length = (num as f64).log(62.0).ceil() as usize; - let mut output = String::with_capacity(length); - - while num > 0 { - // Could be done more efficiently, but requires byte - // manipulation of strings & Vec -> String conversion - output.insert(0, BASE62_CHARS[(num % 62) as usize] as char); - num /= 62; - } - output - } - - pub fn parse_base62(string: &str) -> Result { - let mut num: u64 = 0; - for c in string.chars() { - let next_digit; - if c.is_ascii_digit() { - next_digit = (c as u8 - b'0') as u64; - } else if c.is_ascii_uppercase() { - next_digit = 10 + (c as u8 - b'A') as u64; - } else if c.is_ascii_lowercase() { - next_digit = 36 + (c as u8 - b'a') as u64; - } else { - return Err(DecodingError::InvalidBase62(c)); - } - - // We don't want this panicking or wrapping on integer overflow - if let Some(n) = - num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) - { - num = n; - } else { - return Err(DecodingError::Overflow); - } - } - Ok(num) - } -} diff --git a/apps/labrinth/src/models/v3/users.rs b/apps/labrinth/src/models/v3/users.rs index 9bffd442f..57409e85a 100644 --- a/apps/labrinth/src/models/v3/users.rs +++ b/apps/labrinth/src/models/v3/users.rs @@ -1,14 +1,9 @@ -use super::ids::Base62Id; use crate::{auth::AuthProvider, bitflags_serde_impl}; use chrono::{DateTime, Utc}; +pub use rust_common::users::{UserId, UserStatus}; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; -#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] -#[serde(from = "Base62Id")] -#[serde(into = "Base62Id")] -pub struct UserId(pub u64); - pub const DELETED_USER: UserId = UserId(127155982985829); bitflags::bitflags! { @@ -211,10 +206,3 @@ impl UserFriend { } } } - -#[derive(Serialize, Deserialize, Clone)] -pub struct UserStatus { - pub user_id: UserId, - pub profile_name: Option, - pub last_update: DateTime, -} diff --git a/apps/labrinth/src/routes/internal/admin.rs b/apps/labrinth/src/routes/internal/admin.rs index b352e4a14..3b88d62cf 100644 --- a/apps/labrinth/src/routes/internal/admin.rs +++ b/apps/labrinth/src/routes/internal/admin.rs @@ -15,12 +15,12 @@ use crate::search::SearchConfig; use crate::util::date::get_current_tenths_of_ms; use crate::util::guards::admin_key_guard; use actix_web::{get, patch, post, web, HttpRequest, HttpResponse}; +use log::info; use serde::Deserialize; use sqlx::PgPool; use std::collections::HashMap; use std::net::Ipv4Addr; use std::sync::Arc; -use log::info; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service( @@ -74,7 +74,7 @@ pub async fn count_download( let project_id: crate::database::models::ids::ProjectId = download_body.project_id.into(); - let id_option = crate::models::ids::base62_impl::parse_base62( + let id_option = rust_common::ids::base62_impl::parse_base62( &download_body.version_name, ) .ok() diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index 38a130c59..0bcd03e07 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -10,7 +10,6 @@ use crate::models::billing::{ Product, ProductMetadata, ProductPrice, SubscriptionMetadata, SubscriptionStatus, UserSubscription, }; -use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::pats::Scopes; use crate::models::users::Badges; use crate::queue::session::AuthQueue; @@ -18,6 +17,7 @@ use crate::routes::ApiError; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::Utc; use log::{info, warn}; +use rust_common::ids::base62_impl::{parse_base62, to_base62}; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use serde::Serialize; diff --git a/apps/labrinth/src/routes/internal/flows.rs b/apps/labrinth/src/routes/internal/flows.rs index d0f830609..8e9e8228e 100644 --- a/apps/labrinth/src/routes/internal/flows.rs +++ b/apps/labrinth/src/routes/internal/flows.rs @@ -4,8 +4,6 @@ use crate::auth::{get_user_from_headers, AuthProvider, AuthenticationError}; use crate::database::models::flow_item::Flow; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; -use crate::models::ids::base62_impl::{parse_base62, to_base62}; -use crate::models::ids::random_base62_rng; use crate::models::pats::Scopes; use crate::models::users::{Badges, Role}; use crate::queue::session::AuthQueue; @@ -25,6 +23,8 @@ use chrono::{Duration, Utc}; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; use reqwest::header::AUTHORIZATION; +use rust_common::ids::base62_impl::{parse_base62, to_base62}; +use rust_common::ids::random_base62_rng; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; use std::collections::HashMap; diff --git a/apps/labrinth/src/routes/internal/moderation.rs b/apps/labrinth/src/routes/internal/moderation.rs index 9f59e738e..bfabbcd74 100644 --- a/apps/labrinth/src/routes/internal/moderation.rs +++ b/apps/labrinth/src/routes/internal/moderation.rs @@ -1,12 +1,12 @@ use super::ApiError; use crate::database; use crate::database::redis::RedisPool; -use crate::models::ids::random_base62; use crate::models::projects::ProjectStatus; use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata}; use crate::queue::session::AuthQueue; use crate::{auth::check_is_moderator_from_headers, models::pats::Scopes}; use actix_web::{web, HttpRequest, HttpResponse}; +use rust_common::ids::random_base62; use serde::Deserialize; use sqlx::PgPool; use std::collections::HashMap; diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs index 4b595b1cc..dff4af3f5 100644 --- a/apps/labrinth/src/routes/internal/statuses.rs +++ b/apps/labrinth/src/routes/internal/statuses.rs @@ -2,9 +2,8 @@ use crate::auth::validate::get_user_record_from_bearer_token; use crate::auth::AuthenticationError; use crate::database::models::friend_item::FriendItem; use crate::database::redis::RedisPool; -use crate::models::ids::UserId; use crate::models::pats::Scopes; -use crate::models::users::{User, UserStatus}; +use crate::models::users::User; use crate::queue::session::AuthQueue; use crate::queue::socket::ActiveSockets; use crate::routes::ApiError; @@ -12,30 +11,20 @@ use actix_web::web::{Data, Payload}; use actix_web::{get, web, HttpRequest, HttpResponse}; use actix_ws::Message; use chrono::Utc; +use either::Either; use futures_util::{StreamExt, TryStreamExt}; -use serde::{Deserialize, Serialize}; +use rust_common::ids::UserId; +use rust_common::networking::message::{ + ClientToServerMessage, ServerToClientMessage, +}; +use rust_common::users::UserStatus; +use serde::Deserialize; use sqlx::PgPool; pub fn config(cfg: &mut web::ServiceConfig) { cfg.service(ws_init); } -#[derive(Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ClientToServerMessage { - StatusUpdate { profile_name: Option }, -} - -#[derive(Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ServerToClientMessage { - StatusUpdate { status: UserStatus }, - UserOffline { id: UserId }, - FriendStatuses { statuses: Vec }, - FriendRequest { from: UserId }, - FriendRequestRejected { from: UserId }, -} - #[derive(Deserialize)] struct LauncherHeartbeatInit { code: String, @@ -133,57 +122,59 @@ pub async fn ws_init( actix_web::rt::spawn(async move { // receive messages from websocket while let Some(msg) = stream.next().await { - match msg { - Ok(Message::Text(text)) => { - if let Ok(message) = - serde_json::from_str::(&text) - { - match message { - ClientToServerMessage::StatusUpdate { - profile_name, - } => { - if let Some(mut pair) = - db.auth_sockets.get_mut(&user.id) + if msg.is_err() { + continue; + } + match ClientToServerMessage::deserialize(msg.unwrap()) { + Ok(Either::Left(message)) => { + match message { + ClientToServerMessage::StatusUpdate { + profile_name, + } => { + if let Some(mut pair) = + db.auth_sockets.get_mut(&user.id) + { + let (status, _) = pair.value_mut(); + + if status + .profile_name + .as_ref() + .map(|x| x.len() > 64) + .unwrap_or(false) { - let (status, _) = pair.value_mut(); - - if status - .profile_name - .as_ref() - .map(|x| x.len() > 64) - .unwrap_or(false) - { - continue; - } - - status.profile_name = profile_name; - status.last_update = Utc::now(); - - let user_status = status.clone(); - // We drop the pair to avoid holding the lock for too long - drop(pair); - - let _ = broadcast_friends( - user.id, - ServerToClientMessage::StatusUpdate { - status: user_status, - }, - &pool, - &db, - None, - ) - .await; + return; } + + status.profile_name = profile_name; + status.last_update = Utc::now(); + + let user_status = status.clone(); + // We drop the pair to avoid holding the lock for too long + drop(pair); + + let _ = broadcast_friends( + user.id, + ServerToClientMessage::StatusUpdate { + status: user_status, + }, + &pool, + &db, + None, + ) + .await; } } + ClientToServerMessage::SocketOpen + | ClientToServerMessage::SocketClose { .. } + | ClientToServerMessage::SocketSend { .. } => todo!(), } } - Ok(Message::Close(_)) => { + Ok(Either::Right(Message::Close(_))) => { let _ = close_socket(user.id, &pool, &db).await; } - Ok(Message::Ping(msg)) => { + Ok(Either::Right(Message::Ping(msg))) => { if let Some(socket) = db.auth_sockets.get(&user.id) { let (_, socket) = socket.value(); let _ = socket.clone().pong(&msg).await; @@ -207,6 +198,7 @@ pub async fn broadcast_friends( sockets: &ActiveSockets, friends: Option>, ) -> Result<(), crate::database::models::DatabaseError> { + // FIXME Probably shouldn't be using database errors for this let friends = if let Some(friends) = friends { friends } else { @@ -224,8 +216,7 @@ pub async fn broadcast_friends( if let Some(socket) = sockets.auth_sockets.get(&friend_id.into()) { let (_, socket) = socket.value(); - let _ = - socket.clone().text(serde_json::to_string(&message)?).await; + let _ = message.send(&mut socket.clone()).await; // FIXME Probably shouldn't swallow this error } } } diff --git a/apps/labrinth/src/routes/maven.rs b/apps/labrinth/src/routes/maven.rs index 193b5b0f9..0c2eecda0 100644 --- a/apps/labrinth/src/routes/maven.rs +++ b/apps/labrinth/src/routes/maven.rs @@ -164,7 +164,7 @@ async fn find_version( pool: &PgPool, redis: &RedisPool, ) -> Result, ApiError> { - let id_option = crate::models::ids::base62_impl::parse_base62(vcoords) + let id_option = rust_common::ids::base62_impl::parse_base62(vcoords) .ok() .map(|x| x as i64); diff --git a/apps/labrinth/src/routes/mod.rs b/apps/labrinth/src/routes/mod.rs index 79d2cc4c6..79696d2db 100644 --- a/apps/labrinth/src/routes/mod.rs +++ b/apps/labrinth/src/routes/mod.rs @@ -117,7 +117,7 @@ pub enum ApiError { #[error("Captcha Error. Try resubmitting the form.")] Turnstile, #[error("Error while decoding Base62: {0}")] - Decoding(#[from] crate::models::ids::DecodingError), + Decoding(#[from] rust_common::ids::DecodingError), #[error("Image Parsing Error: {0}")] ImageParse(#[from] image::ImageError), #[error("Password Hashing Error: {0}")] diff --git a/apps/labrinth/src/routes/v3/analytics_get.rs b/apps/labrinth/src/routes/v3/analytics_get.rs index a31e753b4..62365153e 100644 --- a/apps/labrinth/src/routes/v3/analytics_get.rs +++ b/apps/labrinth/src/routes/v3/analytics_get.rs @@ -6,13 +6,14 @@ use crate::{ auth::get_user_from_headers, database::models::user_item, models::{ - ids::{base62_impl::to_base62, ProjectId, VersionId}, + ids::{ProjectId, VersionId}, pats::Scopes, }, queue::session::AuthQueue, }; use actix_web::{web, HttpRequest, HttpResponse}; use chrono::{DateTime, Duration, Utc}; +use rust_common::ids::base62_impl::to_base62; use serde::{Deserialize, Serialize}; use sqlx::postgres::types::PgInterval; use sqlx::PgPool; diff --git a/apps/labrinth/src/routes/v3/collections.rs b/apps/labrinth/src/routes/v3/collections.rs index 6a9f19e39..5990da14b 100644 --- a/apps/labrinth/src/routes/v3/collections.rs +++ b/apps/labrinth/src/routes/v3/collections.rs @@ -6,7 +6,6 @@ use crate::database::models::{ use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models::collections::{Collection, CollectionStatus}; -use crate::models::ids::base62_impl::parse_base62; use crate::models::ids::{CollectionId, ProjectId}; use crate::models::pats::Scopes; use crate::queue::session::AuthQueue; @@ -20,6 +19,7 @@ use actix_web::web::Data; use actix_web::{web, HttpRequest, HttpResponse}; use chrono::Utc; use itertools::Itertools; +use rust_common::ids::base62_impl::parse_base62; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::sync::Arc; diff --git a/apps/labrinth/src/routes/v3/friends.rs b/apps/labrinth/src/routes/v3/friends.rs index 552e75831..f195c514b 100644 --- a/apps/labrinth/src/routes/v3/friends.rs +++ b/apps/labrinth/src/routes/v3/friends.rs @@ -5,10 +5,11 @@ use crate::models::pats::Scopes; use crate::models::users::UserFriend; use crate::queue::session::AuthQueue; use crate::queue::socket::ActiveSockets; -use crate::routes::internal::statuses::{close_socket, ServerToClientMessage}; +use crate::routes::internal::statuses::close_socket; use crate::routes::ApiError; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; use chrono::Utc; +use rust_common::networking::message::ServerToClientMessage; use sqlx::PgPool; pub fn config(cfg: &mut web::ServiceConfig) { diff --git a/apps/labrinth/src/routes/v3/oauth_clients.rs b/apps/labrinth/src/routes/v3/oauth_clients.rs index a65dcc75d..8d4b2b094 100644 --- a/apps/labrinth/src/routes/v3/oauth_clients.rs +++ b/apps/labrinth/src/routes/v3/oauth_clients.rs @@ -1,18 +1,5 @@ use std::{collections::HashSet, fmt::Display, sync::Arc}; -use actix_web::{ - delete, get, patch, post, - web::{self, scope}, - HttpRequest, HttpResponse, -}; -use chrono::Utc; -use itertools::Itertools; -use rand::{distributions::Alphanumeric, Rng, SeedableRng}; -use rand_chacha::ChaCha20Rng; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use validator::Validate; - use super::ApiError; use crate::{ auth::{checks::ValidateAuthorized, get_user_from_headers}, @@ -35,13 +22,22 @@ use crate::{ util::validate::validation_errors_to_string, }; use crate::{ - file_hosting::FileHost, - models::{ - ids::base62_impl::parse_base62, - oauth_clients::DeleteOAuthClientQueryParam, - }, + file_hosting::FileHost, models::oauth_clients::DeleteOAuthClientQueryParam, util::routes::read_from_payload, }; +use actix_web::{ + delete, get, patch, post, + web::{self, scope}, + HttpRequest, HttpResponse, +}; +use chrono::Utc; +use itertools::Itertools; +use rand::{distributions::Alphanumeric, Rng, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use rust_common::ids::base62_impl::parse_base62; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use validator::Validate; use crate::database::models::oauth_client_item::OAuthClient as DBOAuthClient; use crate::models::ids::OAuthClientId as ApiOAuthClientId; diff --git a/apps/labrinth/src/routes/v3/organizations.rs b/apps/labrinth/src/routes/v3/organizations.rs index 0307341b3..416613430 100644 --- a/apps/labrinth/src/routes/v3/organizations.rs +++ b/apps/labrinth/src/routes/v3/organizations.rs @@ -9,7 +9,6 @@ use crate::database::models::{ }; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; -use crate::models::ids::base62_impl::parse_base62; use crate::models::ids::UserId; use crate::models::organizations::OrganizationId; use crate::models::pats::Scopes; @@ -22,6 +21,7 @@ use crate::util::validate::validation_errors_to_string; use crate::{database, models}; use actix_web::{web, HttpRequest, HttpResponse}; use futures::TryStreamExt; +use rust_common::ids::base62_impl::parse_base62; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sqlx::PgPool; @@ -786,7 +786,7 @@ pub async fn organization_projects_add( let organization_owner_user_id = sqlx::query!( " - SELECT u.id + SELECT u.id FROM team_members INNER JOIN users u ON u.id = team_members.user_id WHERE team_id = $1 AND is_owner = TRUE @@ -969,7 +969,7 @@ pub async fn organization_projects_remove( sqlx::query!( " UPDATE team_members - SET + SET is_owner = TRUE, accepted = TRUE, permissions = $2, diff --git a/apps/labrinth/src/routes/v3/project_creation.rs b/apps/labrinth/src/routes/v3/project_creation.rs index 31d751205..23e61f9db 100644 --- a/apps/labrinth/src/routes/v3/project_creation.rs +++ b/apps/labrinth/src/routes/v3/project_creation.rs @@ -8,7 +8,6 @@ use crate::database::models::{self, image_item, User}; use crate::database::redis::RedisPool; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; -use crate::models::ids::base62_impl::to_base62; use crate::models::ids::{ImageId, OrganizationId}; use crate::models::images::{Image, ImageContext}; use crate::models::pats::Scopes; @@ -32,6 +31,7 @@ use chrono::Utc; use futures::stream::StreamExt; use image::ImageError; use itertools::Itertools; +use rust_common::ids::base62_impl::to_base62; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 0a47aea2c..a7df8b2bf 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -11,7 +11,6 @@ use crate::database::redis::RedisPool; use crate::database::{self, models as db_models}; use crate::file_hosting::FileHost; use crate::models; -use crate::models::ids::base62_impl::parse_base62; use crate::models::images::ImageContext; use crate::models::notifications::NotificationBody; use crate::models::pats::Scopes; @@ -33,6 +32,7 @@ use actix_web::{web, HttpRequest, HttpResponse}; use chrono::Utc; use futures::TryStreamExt; use itertools::Itertools; +use rust_common::ids::base62_impl::parse_base62; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; diff --git a/apps/labrinth/src/routes/v3/reports.rs b/apps/labrinth/src/routes/v3/reports.rs index 1af674627..b597b7b07 100644 --- a/apps/labrinth/src/routes/v3/reports.rs +++ b/apps/labrinth/src/routes/v3/reports.rs @@ -6,9 +6,7 @@ use crate::database::models::thread_item::{ }; use crate::database::redis::RedisPool; use crate::models::ids::ImageId; -use crate::models::ids::{ - base62_impl::parse_base62, ProjectId, UserId, VersionId, -}; +use crate::models::ids::{ProjectId, UserId, VersionId}; use crate::models::images::{Image, ImageContext}; use crate::models::pats::Scopes; use crate::models::reports::{ItemType, Report}; @@ -19,6 +17,7 @@ use crate::util::img; use actix_web::{web, HttpRequest, HttpResponse}; use chrono::Utc; use futures::StreamExt; +use rust_common::ids::base62_impl::parse_base62; use serde::Deserialize; use sqlx::PgPool; use validator::Validate; diff --git a/apps/labrinth/src/routes/v3/versions.rs b/apps/labrinth/src/routes/v3/versions.rs index ac27a075c..81e4be019 100644 --- a/apps/labrinth/src/routes/v3/versions.rs +++ b/apps/labrinth/src/routes/v3/versions.rs @@ -13,7 +13,6 @@ use crate::database::models::version_item::{DependencyBuilder, LoaderVersion}; use crate::database::models::{image_item, Organization}; use crate::database::redis::RedisPool; use crate::models; -use crate::models::ids::base62_impl::parse_base62; use crate::models::ids::VersionId; use crate::models::images::ImageContext; use crate::models::pats::Scopes; @@ -29,6 +28,7 @@ use crate::util::img; use crate::util::validate::validation_errors_to_string; use actix_web::{web, HttpRequest, HttpResponse}; use itertools::Itertools; +use rust_common::ids::base62_impl::parse_base62; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use validator::Validate; @@ -444,7 +444,7 @@ pub async fn version_edit_helper( .collect::>(); sqlx::query!( " - DELETE FROM version_fields + DELETE FROM version_fields WHERE version_id = $1 AND field_id = ANY($2) ", diff --git a/apps/labrinth/src/search/indexing/mod.rs b/apps/labrinth/src/search/indexing/mod.rs index 0c5648530..0b6324366 100644 --- a/apps/labrinth/src/search/indexing/mod.rs +++ b/apps/labrinth/src/search/indexing/mod.rs @@ -2,13 +2,13 @@ pub mod local_import; use crate::database::redis::RedisPool; -use crate::models::ids::base62_impl::to_base62; use crate::search::{SearchConfig, UploadSearchProject}; use local_import::index_local; use log::info; use meilisearch_sdk::client::{Client, SwapIndexes}; use meilisearch_sdk::indexes::Index; use meilisearch_sdk::settings::{PaginationSetting, Settings}; +use rust_common::ids::base62_impl::to_base62; use sqlx::postgres::PgPool; use thiserror::Error; #[derive(Error, Debug)] diff --git a/apps/labrinth/src/util/webhook.rs b/apps/labrinth/src/util/webhook.rs index 70ab62dc9..e4f9ff2d1 100644 --- a/apps/labrinth/src/util/webhook.rs +++ b/apps/labrinth/src/util/webhook.rs @@ -1,9 +1,9 @@ use crate::database::models::legacy_loader_fields::MinecraftGameVersion; use crate::database::redis::RedisPool; -use crate::models::ids::base62_impl::to_base62; use crate::models::projects::ProjectId; use crate::routes::ApiError; use chrono::{DateTime, Utc}; +use rust_common::ids::base62_impl::to_base62; use serde::Serialize; use sqlx::PgPool; diff --git a/apps/labrinth/tests/analytics.rs b/apps/labrinth/tests/analytics.rs index 96e2a440a..3176f4cf6 100644 --- a/apps/labrinth/tests/analytics.rs +++ b/apps/labrinth/tests/analytics.rs @@ -7,9 +7,9 @@ use common::{ environment::{with_test_environment, TestEnvironment}, }; use itertools::Itertools; -use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::teams::ProjectPermissions; use labrinth::queue::payouts; +use rust_common::ids::base62_impl::parse_base62; use rust_decimal::{prelude::ToPrimitive, Decimal}; mod common; diff --git a/apps/labrinth/tests/project.rs b/apps/labrinth/tests/project.rs index 11c63abbc..6b6a072c2 100644 --- a/apps/labrinth/tests/project.rs +++ b/apps/labrinth/tests/project.rs @@ -12,10 +12,10 @@ use futures::StreamExt; use labrinth::database::models::project_item::{ PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE, }; -use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::projects::ProjectId; use labrinth::models::teams::ProjectPermissions; use labrinth::util::actix::{MultipartSegment, MultipartSegmentData}; +use rust_common::ids::base62_impl::parse_base62; use serde_json::json; use crate::common::api_common::models::CommonProject; diff --git a/apps/labrinth/tests/scopes.rs b/apps/labrinth/tests/scopes.rs index 1d19d2b4f..fff619e76 100644 --- a/apps/labrinth/tests/scopes.rs +++ b/apps/labrinth/tests/scopes.rs @@ -18,10 +18,10 @@ use common::environment::{ with_test_environment, with_test_environment_all, TestEnvironment, }; use common::{database::*, scopes::ScopeTest}; -use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::pats::Scopes; use labrinth::models::projects::ProjectId; use labrinth::models::users::UserId; +use rust_common::ids::base62_impl::parse_base62; use serde_json::json; // For each scope, we (using test_scope): diff --git a/apps/labrinth/tests/search.rs b/apps/labrinth/tests/search.rs index d0c5fb14c..29a273d91 100644 --- a/apps/labrinth/tests/search.rs +++ b/apps/labrinth/tests/search.rs @@ -8,7 +8,7 @@ use common::environment::with_test_environment; use common::environment::TestEnvironment; use common::search::setup_search_projects; use futures::stream::StreamExt; -use labrinth::models::ids::base62_impl::parse_base62; +use rust_common::ids::base62_impl::parse_base62; use serde_json::json; use crate::common::api_common::Api; diff --git a/apps/labrinth/tests/v2/project.rs b/apps/labrinth/tests/v2/project.rs index 5e9006af7..bd4cbdec5 100644 --- a/apps/labrinth/tests/v2/project.rs +++ b/apps/labrinth/tests/v2/project.rs @@ -20,12 +20,10 @@ use futures::StreamExt; use itertools::Itertools; use labrinth::{ database::models::project_item::PROJECTS_SLUGS_NAMESPACE, - models::{ - ids::base62_impl::parse_base62, projects::ProjectId, - teams::ProjectPermissions, - }, + models::{projects::ProjectId, teams::ProjectPermissions}, util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}, }; +use rust_common::ids::base62_impl::parse_base62; use serde_json::json; #[actix_rt::test] diff --git a/apps/labrinth/tests/v2/scopes.rs b/apps/labrinth/tests/v2/scopes.rs index be53bc20e..6cbfa15f1 100644 --- a/apps/labrinth/tests/v2/scopes.rs +++ b/apps/labrinth/tests/v2/scopes.rs @@ -6,9 +6,9 @@ use crate::common::dummy_data::TestFile; use crate::common::environment::with_test_environment; use crate::common::environment::TestEnvironment; use crate::common::scopes::ScopeTest; -use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::pats::Scopes; use labrinth::models::projects::ProjectId; +use rust_common::ids::base62_impl::parse_base62; // Project version creation scopes #[actix_rt::test] diff --git a/apps/labrinth/tests/v2/search.rs b/apps/labrinth/tests/v2/search.rs index 622bbcab1..f3ebb994c 100644 --- a/apps/labrinth/tests/v2/search.rs +++ b/apps/labrinth/tests/v2/search.rs @@ -11,7 +11,7 @@ use crate::common::environment::with_test_environment; use crate::common::environment::TestEnvironment; use actix_http::StatusCode; use futures::stream::StreamExt; -use labrinth::models::ids::base62_impl::parse_base62; +use rust_common::ids::base62_impl::parse_base62; use serde_json::json; use std::collections::HashMap; use std::sync::Arc; diff --git a/apps/labrinth/tests/version.rs b/apps/labrinth/tests/version.rs index b085c435d..2bc6631c2 100644 --- a/apps/labrinth/tests/version.rs +++ b/apps/labrinth/tests/version.rs @@ -14,11 +14,11 @@ use common::database::USER_USER_PAT; use common::environment::{with_test_environment, with_test_environment_all}; use futures::StreamExt; use labrinth::database::models::version_item::VERSIONS_NAMESPACE; -use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::projects::{ Dependency, DependencyType, VersionId, VersionStatus, VersionType, }; use labrinth::routes::v3::version_file::FileUpdateData; +use rust_common::ids::base62_impl::parse_base62; use serde_json::json; // importing common module. diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index dcf0b6ae7..51036c543 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -29,6 +29,7 @@ regex = "1.5" sys-info = "0.9.0" sysinfo = "0.30.8" thiserror = "1.0" +either = "1.13" tracing = "0.1.37" tracing-subscriber = { version = "0.3.18", features = ["chrono", "env-filter"] } @@ -62,6 +63,8 @@ base64 = "0.22.0" sqlx = { version = "0.8.2", features = [ "runtime-tokio", "sqlite", "macros" ] } +rust-common = { path = "../rust-common" } + [target.'cfg(windows)'.dependencies] winreg = "0.52.0" diff --git a/packages/app-lib/src/api/friends.rs b/packages/app-lib/src/api/friends.rs index e221cf56b..7dd5f8cbc 100644 --- a/packages/app-lib/src/api/friends.rs +++ b/packages/app-lib/src/api/friends.rs @@ -1,4 +1,5 @@ -use crate::state::{FriendsSocket, UserFriend, UserStatus}; +use crate::state::{FriendsSocket, UserFriend}; +use rust_common::users::UserStatus; #[tracing::instrument] pub async fn friends() -> crate::Result> { diff --git a/packages/app-lib/src/api/mod.rs b/packages/app-lib/src/api/mod.rs index 643798f0f..2fd5fe0fb 100644 --- a/packages/app-lib/src/api/mod.rs +++ b/packages/app-lib/src/api/mod.rs @@ -19,8 +19,9 @@ pub mod data { Hooks, JavaVersion, LinkedData, MemorySettings, ModLoader, ModrinthCredentials, Organization, ProcessMetadata, ProfileFile, Project, ProjectType, SearchResult, SearchResults, Settings, - TeamMember, Theme, User, UserFriend, UserStatus, Version, WindowSize, + TeamMember, Theme, User, UserFriend, Version, WindowSize, }; + pub use rust_common::users::UserStatus; } pub mod prelude { diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs index 3fc6bd075..34cc426e8 100644 --- a/packages/app-lib/src/event/mod.rs +++ b/packages/app-lib/src/event/mod.rs @@ -1,6 +1,6 @@ //! Theseus state management system -use crate::state::UserStatus; use dashmap::DashMap; +use rust_common::users::{UserId, UserStatus}; use serde::{Deserialize, Serialize}; use std::{path::PathBuf, sync::Arc}; #[cfg(feature = "tauri")] @@ -262,8 +262,8 @@ pub enum EventError { #[serde(rename_all = "snake_case")] #[serde(tag = "event")] pub enum FriendPayload { - FriendRequest { from: String }, - UserOffline { id: String }, + FriendRequest { from: UserId }, + UserOffline { id: UserId }, StatusUpdate { user_status: UserStatus }, StatusSync, } diff --git a/packages/app-lib/src/state/friends.rs b/packages/app-lib/src/state/friends.rs index 197590ea8..5cc23d48b 100644 --- a/packages/app-lib/src/state/friends.rs +++ b/packages/app-lib/src/state/friends.rs @@ -14,6 +14,10 @@ use futures::stream::SplitSink; use futures::{SinkExt, StreamExt}; use reqwest::header::HeaderValue; use reqwest::Method; +use rust_common::networking::message::{ + ClientToServerMessage, ServerToClientMessage, +}; +use rust_common::users::{UserId, UserStatus}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; @@ -23,7 +27,7 @@ type WriteSocket = pub struct FriendsSocket { write: WriteSocket, - user_statuses: Arc>, + user_statuses: Arc>, } #[derive(Deserialize, Serialize)] @@ -34,28 +38,6 @@ pub struct UserFriend { pub created: DateTime, } -#[derive(Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ClientToServerMessage { - StatusUpdate { profile_name: Option }, -} - -#[derive(Deserialize, Debug)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ServerToClientMessage { - StatusUpdate { status: UserStatus }, - UserOffline { id: String }, - FriendStatuses { statuses: Vec }, - FriendRequest { from: String }, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct UserStatus { - pub user_id: String, - pub profile_name: Option, - pub last_update: DateTime, -} - impl Default for FriendsSocket { fn default() -> Self { Self::new() @@ -126,6 +108,7 @@ impl FriendsSocket { while let Some(msg_result) = read_stream.next().await { match msg_result { Ok(msg) => { + // TODO: Make wire package work with this other library let server_message = match msg { Message::Text(text) => { serde_json::from_str::< @@ -165,7 +148,7 @@ impl FriendsSocket { { match server_message { ServerToClientMessage::StatusUpdate { status } => { - statuses.insert(status.user_id.clone(), status.clone()); + statuses.insert(status.user_id, status.clone()); let _ = emit_friend(FriendPayload::StatusUpdate { user_status: status }).await; }, ServerToClientMessage::UserOffline { id } => { @@ -175,13 +158,18 @@ impl FriendsSocket { ServerToClientMessage::FriendStatuses { statuses: new_statuses } => { statuses.clear(); new_statuses.into_iter().for_each(|status| { - statuses.insert(status.user_id.clone(), status); + statuses.insert(status.user_id, status); }); let _ = emit_friend(FriendPayload::StatusSync).await; } ServerToClientMessage::FriendRequest { from } => { let _ = emit_friend(FriendPayload::FriendRequest { from }).await; } + ServerToClientMessage::FriendRequestRejected { .. } => todo!(), + ServerToClientMessage::SocketOpened { .. } => todo!(), + ServerToClientMessage::SocketClosed { .. } => todo!(), + ServerToClientMessage::FriendSocketOpened { .. } => todo!(), + ServerToClientMessage::SocketData { .. } => todo!(), } } } diff --git a/packages/rust-common/Cargo.toml b/packages/rust-common/Cargo.toml new file mode 100644 index 000000000..d68110ec7 --- /dev/null +++ b/packages/rust-common/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rust-common" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_bytes = "0.11" +serde_json = "1.0" +serde_cbor = "0.11" + +uuid = { version = "1.12", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +actix-ws = "0.3" + +thiserror = "2.0" +rand = "0.8" +either = "1.13" diff --git a/packages/rust-common/package.json b/packages/rust-common/package.json new file mode 100644 index 000000000..77b1aa69f --- /dev/null +++ b/packages/rust-common/package.json @@ -0,0 +1,9 @@ +{ + "name": "@modrinth/networking", + "scripts": { + "build": "cargo build --release", + "lint": "cargo fmt --check && cargo clippy --all-targets -- -D warnings", + "fix": "cargo fmt && cargo clippy --fix", + "test": "cargo test" + } +} diff --git a/packages/rust-common/src/ids.rs b/packages/rust-common/src/ids.rs new file mode 100644 index 000000000..6b1a00203 --- /dev/null +++ b/packages/rust-common/src/ids.rs @@ -0,0 +1,201 @@ +pub use super::users::UserId; +use thiserror::Error; + +/// Generates a random 64 bit integer that is exactly `n` characters +/// long when encoded as base62. +/// +/// Uses `rand`'s thread rng on every call. +/// +/// # Panics +/// +/// This method panics if `n` is 0 or greater than 11, since a `u64` +/// can only represent up to 11 character base62 strings +#[inline] +pub fn random_base62(n: usize) -> u64 { + random_base62_rng(&mut rand::thread_rng(), n) +} + +/// Generates a random 64 bit integer that is exactly `n` characters +/// long when encoded as base62, using the given rng. +/// +/// # Panics +/// +/// This method panics if `n` is 0 or greater than 11, since a `u64` +/// can only represent up to 11 character base62 strings +pub fn random_base62_rng(rng: &mut R, n: usize) -> u64 { + random_base62_rng_range(rng, n, n) +} + +pub fn random_base62_rng_range( + rng: &mut R, + n_min: usize, + n_max: usize, +) -> u64 { + use rand::Rng; + assert!(n_min > 0 && n_max <= 11 && n_min <= n_max); + // gen_range is [low, high): max value is `MULTIPLES[n] - 1`, + // which is n characters long when encoded + rng.gen_range(MULTIPLES[n_min - 1]..MULTIPLES[n_max]) +} + +const MULTIPLES: [u64; 12] = [ + 1, + 62, + 62 * 62, + 62 * 62 * 62, + 62 * 62 * 62 * 62, + 62 * 62 * 62 * 62 * 62, + 62 * 62 * 62 * 62 * 62 * 62, + 62 * 62 * 62 * 62 * 62 * 62 * 62, + 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62, + 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62, + 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62, + u64::MAX, +]; + +/// An ID encoded as base62 for use in the API. +/// +/// All ids should be random and encode to 8-10 character base62 strings, +/// to avoid enumeration and other attacks. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct Base62Id(pub u64); + +/// An error decoding a number from base62. +#[derive(Error, Debug)] +pub enum DecodingError { + /// Encountered a non-base62 character in a base62 string + #[error("Invalid character {0:?} in base62 encoding")] + InvalidBase62(char), + /// Encountered integer overflow when decoding a base62 id. + #[error("Base62 decoding overflowed")] + Overflow, +} + +#[macro_export] +macro_rules! from_base62id { + ($($struct:ty, $con:expr;)+) => { + $( + impl From for $struct { + fn from(id: Base62Id) -> $struct { + $con(id.0) + } + } + impl From<$struct> for Base62Id { + fn from(id: $struct) -> Base62Id { + Base62Id(id.0) + } + } + )+ + }; +} + +#[macro_export] +macro_rules! impl_base62_display { + ($struct:ty) => { + impl std::fmt::Display for $struct { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&$crate::ids::base62_impl::to_base62(self.0)) + } + } + }; +} +impl_base62_display!(Base62Id); + +#[macro_export] +macro_rules! base62_id_impl { + ($struct:ty, $cons:expr) => { + $crate::ids::from_base62id!($struct, $cons;); + $crate::ids::impl_base62_display!($struct); + } +} +base62_id_impl!(UserId, UserId); + +pub use {base62_id_impl, from_base62id, impl_base62_display}; + +pub mod base62_impl { + use serde::de::{self, Deserializer, Visitor}; + use serde::ser::Serializer; + use serde::{Deserialize, Serialize}; + + use super::{Base62Id, DecodingError}; + + impl<'de> Deserialize<'de> for Base62Id { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct Base62Visitor; + + impl Visitor<'_> for Base62Visitor { + type Value = Base62Id; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + formatter.write_str("a base62 string id") + } + + fn visit_str(self, string: &str) -> Result + where + E: de::Error, + { + parse_base62(string).map(Base62Id).map_err(E::custom) + } + } + + deserializer.deserialize_str(Base62Visitor) + } + } + + impl Serialize for Base62Id { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&to_base62(self.0)) + } + } + + const BASE62_CHARS: [u8; 62] = + *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + pub fn to_base62(mut num: u64) -> String { + let length = (num as f64).log(62.0).ceil() as usize; + let mut output = String::with_capacity(length); + + while num > 0 { + // Could be done more efficiently, but requires byte + // manipulation of strings & Vec -> String conversion + output.insert(0, BASE62_CHARS[(num % 62) as usize] as char); + num /= 62; + } + output + } + + pub fn parse_base62(string: &str) -> Result { + let mut num: u64 = 0; + for c in string.chars() { + let next_digit; + if c.is_ascii_digit() { + next_digit = (c as u8 - b'0') as u64; + } else if c.is_ascii_uppercase() { + next_digit = 10 + (c as u8 - b'A') as u64; + } else if c.is_ascii_lowercase() { + next_digit = 36 + (c as u8 - b'a') as u64; + } else { + return Err(DecodingError::InvalidBase62(c)); + } + + // We don't want this panicking or wrapping on integer overflow + if let Some(n) = + num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) + { + num = n; + } else { + return Err(DecodingError::Overflow); + } + } + Ok(num) + } +} diff --git a/packages/rust-common/src/lib.rs b/packages/rust-common/src/lib.rs new file mode 100644 index 000000000..4a4251a3e --- /dev/null +++ b/packages/rust-common/src/lib.rs @@ -0,0 +1,3 @@ +pub mod ids; +pub mod networking; +pub mod users; diff --git a/packages/rust-common/src/networking/message.rs b/packages/rust-common/src/networking/message.rs new file mode 100644 index 000000000..983d269a8 --- /dev/null +++ b/packages/rust-common/src/networking/message.rs @@ -0,0 +1,58 @@ +use crate::ids::UserId; +use crate::users::UserStatus; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ClientToServerMessage { + StatusUpdate { + profile_name: Option, + }, + + SocketOpen, + SocketClose { + socket: Uuid, + }, + SocketSend { + socket: Uuid, + #[serde(with = "serde_bytes")] + data: Vec, + }, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ServerToClientMessage { + StatusUpdate { + status: UserStatus, + }, + UserOffline { + id: UserId, + }, + FriendStatuses { + statuses: Vec, + }, + FriendRequest { + from: UserId, + }, + FriendRequestRejected { + from: UserId, + }, + + SocketOpened { + socket: Uuid, + }, + SocketClosed { + socket: Uuid, + }, + FriendSocketOpened { + user: UserId, + socket: Uuid, + }, + SocketData { + socket: Uuid, + #[serde(with = "serde_bytes")] + data: Vec, + }, +} diff --git a/packages/rust-common/src/networking/mod.rs b/packages/rust-common/src/networking/mod.rs new file mode 100644 index 000000000..b4d18c775 --- /dev/null +++ b/packages/rust-common/src/networking/mod.rs @@ -0,0 +1,2 @@ +pub mod message; +pub mod wire; diff --git a/packages/rust-common/src/networking/wire.rs b/packages/rust-common/src/networking/wire.rs new file mode 100644 index 000000000..d194e6646 --- /dev/null +++ b/packages/rust-common/src/networking/wire.rs @@ -0,0 +1,72 @@ +use super::message::{ClientToServerMessage, ServerToClientMessage}; +use actix_ws::{Closed, Message, Session}; +use either::Either; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DeserializationError { + #[error("Failed to deserialize message: {0}")] + DeserializationFailed(#[from] serde_json::Error), + + #[error("Failed to deserialize binary message: {0}")] + BinaryDeserializationFailed(#[from] serde_cbor::Error), +} + +#[derive(Debug, Error)] +pub enum SendError { + #[error("Failed to serialize message: {0}")] + SerializationFailed(#[from] serde_json::Error), + + #[error("Failed to serialize binary message: {0}")] + BinarySerializationFailed(#[from] serde_cbor::Error), + + #[error("Websocket closed")] + Closed, +} + +impl From for SendError { + fn from(_: Closed) -> Self { + SendError::Closed + } +} + +macro_rules! message_wire { + ($message_enum:ty, $binary_pattern:pat) => { + impl $message_enum { + pub fn deserialize( + msg: Message, + ) -> Result, DeserializationError> { + Ok(match msg { + Message::Text(text) => { + Either::Left(serde_json::from_str(&text)?) + } + Message::Binary(bytes) => { + Either::Left(serde_cbor::from_slice(&bytes)?) + } + other => Either::Right(other), + }) + } + + pub async fn send( + &self, + session: &mut Session, + ) -> Result<(), SendError> { + Ok(match self { + $binary_pattern => { + session.binary(serde_cbor::to_vec(self)?).await? + } + _ => session.text(serde_json::to_string(self)?).await?, + }) + } + } + }; +} + +message_wire!( + ClientToServerMessage, + ClientToServerMessage::SocketSend { .. } +); +message_wire!( + ServerToClientMessage, + ServerToClientMessage::SocketData { .. } +); diff --git a/packages/rust-common/src/users.rs b/packages/rust-common/src/users.rs new file mode 100644 index 000000000..5ca7d55f5 --- /dev/null +++ b/packages/rust-common/src/users.rs @@ -0,0 +1,15 @@ +use super::ids::Base62Id; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug, Hash)] +#[serde(from = "Base62Id")] +#[serde(into = "Base62Id")] +pub struct UserId(pub u64); + +#[derive(Serialize, Deserialize, Clone)] +pub struct UserStatus { + pub user_id: UserId, + pub profile_name: Option, + pub last_update: DateTime, +}