From d8d170faf8764c2a9642d590a0d278c7aa3a6c1d Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Wed, 27 Nov 2024 11:46:10 +0100 Subject: [PATCH 01/13] Create external api --- Cargo.lock | 168 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/bookmark.rs | 4 +- src/file.rs | 10 +-- src/follow.rs | 2 +- src/lib.rs | 20 +++--- src/mute.rs | 2 +- src/post.rs | 32 ++++----- src/tag.rs | 186 +++++++++++++++++++++++++++++++++++++++++++++--- src/traits.rs | 4 ++ src/user.rs | 18 ++--- 11 files changed, 394 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c3e753..3a8cf00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -77,6 +92,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "base32" version = "0.5.1" @@ -520,6 +550,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hash32" version = "0.2.1" @@ -549,6 +585,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "httpdate" version = "1.0.3" @@ -818,6 +860,27 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + [[package]] name = "nanorand" version = "0.7.0" @@ -836,6 +899,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -848,6 +920,29 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "password-hash" version = "0.5.0" @@ -968,6 +1063,7 @@ dependencies = [ "pubky-common", "serde", "serde_json", + "tokio", "url", "utoipa", ] @@ -1047,6 +1143,21 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1163,6 +1274,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -1196,6 +1316,16 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.9.8" @@ -1279,6 +1409,35 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing" version = "0.1.40" @@ -1476,6 +1635,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index dce5752..3a9f313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ url = "2.5.4" base32 = "0.5.1" blake3 = "1.5.4" chrono = "0.4.38" + +[dev-dependencies] +tokio = { version = "1.41.1", features = ["full"] } diff --git a/src/bookmark.rs b/src/bookmark.rs index 1b92cdd..d02db3c 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -13,8 +13,8 @@ use serde::{Deserialize, Serialize}; /// Where bookmark_id is Crockford-base32(Blake3("{uri_bookmarked}"")[:half]) #[derive(Serialize, Deserialize, Default)] pub struct PubkyAppBookmark { - pub uri: String, - pub created_at: i64, + uri: String, + created_at: i64, } #[async_trait] diff --git a/src/file.rs b/src/file.rs index 1d69c62..900daac 100644 --- a/src/file.rs +++ b/src/file.rs @@ -6,11 +6,11 @@ use serde::{Deserialize, Serialize}; /// Profile schema #[derive(Deserialize, Serialize, Debug)] pub struct PubkyAppFile { - pub name: String, - pub created_at: i64, - pub src: String, - pub content_type: String, - pub size: i64, + name: String, + created_at: i64, + src: String, + content_type: String, + size: i64, } impl TimestampId for PubkyAppFile {} diff --git a/src/follow.rs b/src/follow.rs index d14ff83..5ccc22f 100644 --- a/src/follow.rs +++ b/src/follow.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Serialize, Deserialize, Default)] pub struct PubkyAppFollow { - pub created_at: i64, + created_at: i64, } #[async_trait] diff --git a/src/lib.rs b/src/lib.rs index a768d44..4b2cd5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,17 @@ -pub mod bookmark; -pub mod file; -pub mod follow; -pub mod mute; -pub mod post; -pub mod tag; +mod bookmark; +mod file; +mod follow; +mod mute; +mod post; +mod tag; pub mod traits; -pub mod types; -pub mod user; +mod types; +mod user; pub use bookmark::PubkyAppBookmark; pub use file::PubkyAppFile; pub use follow::PubkyAppFollow; pub use mute::PubkyAppMute; -pub use post::{PostEmbed, PostKind, PubkyAppPost}; +pub use post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; pub use tag::PubkyAppTag; -pub use user::{PubkyAppUser, UserLink}; +pub use user::{PubkyAppUser, PubkyAppUserLink}; diff --git a/src/mute.rs b/src/mute.rs index 20b6f8e..b0d546d 100644 --- a/src/mute.rs +++ b/src/mute.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Serialize, Deserialize, Default)] pub struct PubkyAppMute { - pub created_at: i64, + created_at: i64, } #[async_trait] diff --git a/src/post.rs b/src/post.rs index 2ebc770..f5614e0 100644 --- a/src/post.rs +++ b/src/post.rs @@ -14,7 +14,7 @@ const MAX_LONG_CONTENT_LENGTH: usize = 50000; /// Used primarily to best display the content in UI #[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone)] #[serde(rename_all = "lowercase")] -pub enum PostKind { +pub enum PubkyAppPostKind { #[default] Short, Long, @@ -24,7 +24,7 @@ pub enum PostKind { File, } -impl fmt::Display for PostKind { +impl fmt::Display for PubkyAppPostKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let string_repr = serde_json::to_value(self) .ok() @@ -36,9 +36,9 @@ impl fmt::Display for PostKind { /// Used primarily to best display the content in UI #[derive(Serialize, Deserialize, Default, Clone)] -pub struct PostEmbed { - pub kind: PostKind, - pub uri: String, // If a repost a `Short` and uri of the reposted post. +pub struct PubkyAppPostEmbed { + kind: PubkyAppPostKind, + uri: String, // If a repost a `Short` and uri of the reposted post. } /// Represents raw post in homeserver with content and kind @@ -50,11 +50,11 @@ pub struct PostEmbed { /// `/pub/pubky.app/posts/00321FCW75ZFY` #[derive(Serialize, Deserialize, Default, Clone)] pub struct PubkyAppPost { - pub content: String, - pub kind: PostKind, - pub parent: Option, // If a reply, the URI of the parent post. - pub embed: Option, - pub attachments: Option>, + content: String, + kind: PubkyAppPostKind, + parent: Option, // If a reply, the URI of the parent post. + embed: Option, + attachments: Option>, } impl TimestampId for PubkyAppPost {} @@ -72,10 +72,10 @@ impl Validatable for PubkyAppPost { content = "empty".to_string() } - // Define content length limits based on PostKind + // Define content length limits based on PubkyAppPostKind let max_content_length = match self.kind { - PostKind::Short => MAX_SHORT_CONTENT_LENGTH, - PostKind::Long => MAX_LONG_CONTENT_LENGTH, + PubkyAppPostKind::Short => MAX_SHORT_CONTENT_LENGTH, + PubkyAppPostKind::Long => MAX_LONG_CONTENT_LENGTH, _ => MAX_SHORT_CONTENT_LENGTH, // Default limit for other kinds }; @@ -94,7 +94,7 @@ impl Validatable for PubkyAppPost { // Sanitize embed if present let embed = if let Some(embed) = &self.embed { match Url::parse(&embed.uri) { - Ok(url) => Some(PostEmbed { + Ok(url) => Some(PubkyAppPostEmbed { kind: embed.kind.clone(), uri: url.to_string(), // Use normalized version }), @@ -119,12 +119,12 @@ impl Validatable for PubkyAppPost { // Validate content length match self.kind { - PostKind::Short => { + PubkyAppPostKind::Short => { if self.content.chars().count() > MAX_SHORT_CONTENT_LENGTH { return Err("Post content exceeds maximum length for Short kind".into()); } } - PostKind::Long => { + PubkyAppPostKind::Long => { if self.content.chars().count() > MAX_LONG_CONTENT_LENGTH { return Err("Post content exceeds maximum length for Short kind".into()); } diff --git a/src/tag.rs b/src/tag.rs index d52b83a..fedbd72 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,6 +1,7 @@ -use crate::traits::{HashId, Validatable}; +use crate::traits::{HasPath, HashId, Validatable}; use crate::types::DynError; use async_trait::async_trait; +use chrono::Utc; use serde::{Deserialize, Serialize}; use url::Url; @@ -15,13 +16,35 @@ const MAX_TAG_LABEL_LENGTH: usize = 20; /// `/pub/pubky.app/tags/FPB0AM9S93Q3M1GFY1KV09GMQM` /// /// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppTag { pub uri: String, pub label: String, pub created_at: i64, } +impl PubkyAppTag { + pub async fn new(uri: String, label: String) -> Self { + let created_at = Utc::now().timestamp_millis(); + let tag = Self { + uri, + label, + created_at, + }; + + match tag.sanitize().await { + Ok(tag) => tag, + Err(_) => Self::default(), + } + } +} + +impl HasPath for PubkyAppTag { + fn get_path(&self) -> String { + format!("pubky:///pub/pubky.app/tags/{}", self.create_id()) + } +} + #[async_trait] impl HashId for PubkyAppTag { /// Tag ID is created based on the hash of the URI tagged and the label used @@ -66,14 +89,155 @@ impl Validatable for PubkyAppTag { } } -#[test] -fn test_create_id() { - let tag = PubkyAppTag { - uri: "user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - label: "cool".to_string(), - }; +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use bytes::Bytes; + use tokio; + + #[tokio::test] + async fn test_create_id() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + created_at: 1627849723000, + label: "cool".to_string(), + }; + + let tag_id = tag.create_id(); + println!("Generated Tag ID: {}", tag_id); + + // Assert that the tag ID is of expected length + // The length depends on your implementation of create_id + assert!(!tag_id.is_empty()); + } + + #[tokio::test] + async fn test_new() { + let uri = "https://example.com/post/1".to_string(); + let label = "interesting".to_string(); + let tag = PubkyAppTag::new(uri.clone(), label.clone()).await; + + assert_eq!(tag.uri, uri); + assert_eq!(tag.label, label); + // Check that created_at is recent + let now = Utc::now().timestamp_millis(); + assert!(tag.created_at <= now && tag.created_at >= now - 1000); // within 1 second + } + + #[tokio::test] + async fn test_get_path() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + created_at: 1627849723000, + label: "cool".to_string(), + }; + + let expected_id = tag.create_id(); + let expected_path = format!("pubky:///pub/pubky.app/tags/{}", expected_id); + let path = tag.get_path(); + + assert_eq!(path, expected_path); + } + + #[tokio::test] + async fn test_sanitize() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: " CoOl ".to_string(), + created_at: 1627849723000, + }; + + let sanitized_tag = tag.sanitize().await.unwrap(); + assert_eq!(sanitized_tag.label, "cool"); + } + + #[tokio::test] + async fn test_validate_valid() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: "cool".to_string(), + created_at: 1627849723000, + }; + + let id = tag.create_id(); + let result = tag.validate(&id).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_validate_invalid_label_length() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: "a".repeat(MAX_TAG_LABEL_LENGTH + 1), + created_at: 1627849723000, + }; + + let id = tag.create_id(); + let result = tag.validate(&id).await; + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Tag label exceeds maximum length" + ); + } + + #[tokio::test] + async fn test_validate_invalid_id() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + label: "cool".to_string(), + created_at: 1627849723000, + }; + + let invalid_id = "INVALIDID"; + let result = tag.validate(&invalid_id).await; + assert!(result.is_err()); + // You can check the specific error message if necessary + } + + #[tokio::test] + async fn test_try_from_valid() { + let tag_json = r#" + { + "uri": "pubky://user_pubky_id/pub/pubky.app/v1/profile.json", + "label": "Cool Tag", + "created_at": 1627849723000 + } + "#; + + let id = PubkyAppTag::new( + "pubky://user_pubky_id/pub/pubky.app/v1/profile.json".to_string(), + "Cool Tag".to_string(), + ) + .await + .create_id(); + + let blob = Bytes::from(tag_json); + let tag = ::try_from(&blob, &id) + .await + .unwrap(); + assert_eq!( + tag.uri, + "pubky://user_pubky_id/pub/pubky.app/v1/profile.json" + ); + assert_eq!(tag.label, "cool tag"); // After sanitization + } + + #[tokio::test] + async fn test_try_from_invalid_uri() { + let tag_json = r#" + { + "uri": "invalid_uri", + "label": "Cool Tag", + "created_at": 1627849723000 + } + "#; - let tag_id = tag.create_id(); - println!("Generated Tag ID: {}", tag_id); + let id = "SomeID"; // The ID doesn't matter here + let blob = Bytes::from(tag_json); + let result = ::try_from(&blob, &id).await; + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "Invalid URI in tag"); + } } diff --git a/src/traits.rs b/src/traits.rs index 78f0a41..0498358 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -118,3 +118,7 @@ pub trait Validatable: Sized + DeserializeOwned { Ok(self) } } + +pub trait HasPath { + fn get_path(&self) -> String; +} diff --git a/src/user.rs b/src/user.rs index 379800c..5c4982e 100644 --- a/src/user.rs +++ b/src/user.rs @@ -19,18 +19,18 @@ const MAX_STATUS_LENGTH: usize = 50; /// URI: /pub/pubky.app/profile.json #[derive(Deserialize, Serialize, Debug)] pub struct PubkyAppUser { - pub name: String, - pub bio: Option, - pub image: Option, - pub links: Option>, - pub status: Option, + name: String, + bio: Option, + image: Option, + links: Option>, + status: Option, } /// Represents a user's single link with a title and URL. #[derive(Serialize, Deserialize, ToSchema, Default, Clone, Debug)] -pub struct UserLink { - pub title: String, - pub url: String, +pub struct PubkyAppUserLink { + title: String, + url: String, } #[async_trait] @@ -105,7 +105,7 @@ impl Validatable for PubkyAppUser { .collect::(); // Only keep valid URLs - Some(UserLink { title, url }) + Some(PubkyAppUserLink { title, url }) } Err(_) => { None // Discard invalid links From e71a9b0ea2739e8c11e5a028eed334f87d212ea5 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Wed, 27 Nov 2024 17:02:13 +0100 Subject: [PATCH 02/13] Feat: no async, less deps, fix tag --- Cargo.lock | 168 ------------------------------------------------ Cargo.toml | 3 - src/bookmark.rs | 8 +-- src/file.rs | 9 +-- src/follow.rs | 5 +- src/lib.rs | 1 - src/mute.rs | 5 +- src/post.rs | 28 ++++---- src/tag.rs | 113 ++++++++++++++++---------------- src/traits.rs | 33 +++++----- src/types.rs | 1 - src/user.rs | 98 +++++++++++++++------------- 12 files changed, 148 insertions(+), 324 deletions(-) delete mode 100644 src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 3a8cf00..6c3e753 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,21 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - [[package]] name = "aead" version = "0.5.2" @@ -92,21 +77,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - [[package]] name = "base32" version = "0.5.1" @@ -550,12 +520,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "hash32" version = "0.2.1" @@ -585,12 +549,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "httpdate" version = "1.0.3" @@ -860,27 +818,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "miniz_oxide" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi", - "libc", - "wasi", - "windows-sys", -] - [[package]] name = "nanorand" version = "0.7.0" @@ -899,15 +836,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.20.2" @@ -920,29 +848,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "password-hash" version = "0.5.0" @@ -1063,7 +968,6 @@ dependencies = [ "pubky-common", "serde", "serde_json", - "tokio", "url", "utoipa", ] @@ -1143,21 +1047,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" -dependencies = [ - "bitflags", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustc_version" version = "0.4.1" @@ -1274,15 +1163,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.2.0" @@ -1316,16 +1196,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "spin" version = "0.9.8" @@ -1409,35 +1279,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tokio" -version = "1.41.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys", -] - -[[package]] -name = "tokio-macros" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing" version = "0.1.40" @@ -1635,15 +1476,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 3a9f313..dce5752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,3 @@ url = "2.5.4" base32 = "0.5.1" blake3 = "1.5.4" chrono = "0.4.38" - -[dev-dependencies] -tokio = { version = "1.41.1", features = ["full"] } diff --git a/src/bookmark.rs b/src/bookmark.rs index d02db3c..8e24e96 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -1,6 +1,4 @@ use crate::traits::{HashId, Validatable}; -use crate::types::DynError; -use async_trait::async_trait; use serde::{Deserialize, Serialize}; /// Represents raw homeserver bookmark with id @@ -17,7 +15,6 @@ pub struct PubkyAppBookmark { created_at: i64, } -#[async_trait] impl HashId for PubkyAppBookmark { /// Bookmark ID is created based on the hash of the URI bookmarked fn get_id_data(&self) -> String { @@ -25,10 +22,9 @@ impl HashId for PubkyAppBookmark { } } -#[async_trait] impl Validatable for PubkyAppBookmark { - async fn validate(&self, id: &str) -> Result<(), DynError> { - self.validate_id(id).await?; + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; // TODO: more bookmarks validation? Ok(()) } diff --git a/src/file.rs b/src/file.rs index 900daac..7cf5953 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,10 +1,8 @@ use crate::traits::{TimestampId, Validatable}; -use crate::types::DynError; -use async_trait::async_trait; use serde::{Deserialize, Serialize}; /// Profile schema -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Default)] pub struct PubkyAppFile { name: String, created_at: i64, @@ -15,11 +13,10 @@ pub struct PubkyAppFile { impl TimestampId for PubkyAppFile {} -#[async_trait] impl Validatable for PubkyAppFile { // TODO: content_type validation. - async fn validate(&self, id: &str) -> Result<(), DynError> { - self.validate_id(id).await?; + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; // TODO: content_type validation. // TODO: size and other validation. Ok(()) diff --git a/src/follow.rs b/src/follow.rs index 5ccc22f..768b7e7 100644 --- a/src/follow.rs +++ b/src/follow.rs @@ -1,6 +1,4 @@ use crate::traits::Validatable; -use crate::types::DynError; -use async_trait::async_trait; use serde::{Deserialize, Serialize}; /// Represents raw homeserver follow object with timestamp @@ -15,9 +13,8 @@ pub struct PubkyAppFollow { created_at: i64, } -#[async_trait] impl Validatable for PubkyAppFollow { - async fn validate(&self, _id: &str) -> Result<(), DynError> { + fn validate(&self, _id: &str) -> Result<(), String> { // TODO: additional follow validation? E.g, validate `created_at` ? Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 4b2cd5e..f59fb3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ mod mute; mod post; mod tag; pub mod traits; -mod types; mod user; pub use bookmark::PubkyAppBookmark; diff --git a/src/mute.rs b/src/mute.rs index b0d546d..a2f995a 100644 --- a/src/mute.rs +++ b/src/mute.rs @@ -1,6 +1,4 @@ use crate::traits::Validatable; -use crate::types::DynError; -use async_trait::async_trait; use serde::{Deserialize, Serialize}; /// Represents raw homeserver Mute object with timestamp @@ -15,9 +13,8 @@ pub struct PubkyAppMute { created_at: i64, } -#[async_trait] impl Validatable for PubkyAppMute { - async fn validate(&self, _id: &str) -> Result<(), DynError> { + fn validate(&self, _id: &str) -> Result<(), String> { // TODO: additional Mute validation? E.g, validate `created_at` ? Ok(()) } diff --git a/src/post.rs b/src/post.rs index f5614e0..f027dca 100644 --- a/src/post.rs +++ b/src/post.rs @@ -1,6 +1,4 @@ use crate::traits::{TimestampId, Validatable}; -use crate::types::DynError; -use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::fmt; use url::Url; @@ -37,8 +35,8 @@ impl fmt::Display for PubkyAppPostKind { /// Used primarily to best display the content in UI #[derive(Serialize, Deserialize, Default, Clone)] pub struct PubkyAppPostEmbed { - kind: PubkyAppPostKind, - uri: String, // If a repost a `Short` and uri of the reposted post. + kind: PubkyAppPostKind, // If a repost: `short`, and uri of the reposted post. + uri: String, } /// Represents raw post in homeserver with content and kind @@ -59,9 +57,8 @@ pub struct PubkyAppPost { impl TimestampId for PubkyAppPost {} -#[async_trait] impl Validatable for PubkyAppPost { - async fn sanitize(self) -> Result { + fn sanitize(self) -> Self { // Sanitize content let mut content = self.content.trim().to_string(); @@ -104,29 +101,34 @@ impl Validatable for PubkyAppPost { None }; - Ok(PubkyAppPost { + PubkyAppPost { content, kind: self.kind, parent, embed, attachments: self.attachments, - }) + } } - //TODO: implement full validation rules. Min/Max lengths, post kinds, etc. - async fn validate(&self, id: &str) -> Result<(), DynError> { - self.validate_id(id).await?; + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; // Validate content length match self.kind { PubkyAppPostKind::Short => { if self.content.chars().count() > MAX_SHORT_CONTENT_LENGTH { - return Err("Post content exceeds maximum length for Short kind".into()); + return Err( + "Validation Error: Post content exceeds maximum length for Short kind" + .into(), + ); } } PubkyAppPostKind::Long => { if self.content.chars().count() > MAX_LONG_CONTENT_LENGTH { - return Err("Post content exceeds maximum length for Short kind".into()); + return Err( + "Validation Error: Post content exceeds maximum length for Short kind" + .into(), + ); } } _ => (), diff --git a/src/tag.rs b/src/tag.rs index fedbd72..a7dc9d9 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,6 +1,4 @@ use crate::traits::{HasPath, HashId, Validatable}; -use crate::types::DynError; -use async_trait::async_trait; use chrono::Utc; use serde::{Deserialize, Serialize}; use url::Url; @@ -24,18 +22,14 @@ pub struct PubkyAppTag { } impl PubkyAppTag { - pub async fn new(uri: String, label: String) -> Self { + pub fn new(uri: String, label: String) -> Self { let created_at = Utc::now().timestamp_millis(); - let tag = Self { + Self { uri, label, created_at, - }; - - match tag.sanitize().await { - Ok(tag) => tag, - Err(_) => Self::default(), } + .sanitize() } } @@ -45,7 +39,6 @@ impl HasPath for PubkyAppTag { } } -#[async_trait] impl HashId for PubkyAppTag { /// Tag ID is created based on the hash of the URI tagged and the label used fn get_id_data(&self) -> String { @@ -53,9 +46,8 @@ impl HashId for PubkyAppTag { } } -#[async_trait] impl Validatable for PubkyAppTag { - async fn sanitize(self) -> Result { + fn sanitize(self) -> Self { // Convert label to lowercase and trim let label = self.label.trim().to_lowercase(); @@ -64,28 +56,40 @@ impl Validatable for PubkyAppTag { // Sanitize URI let uri = match Url::parse(&self.uri) { - Ok(url) => url.to_string(), - Err(_) => return Err("Invalid URI in tag".into()), + Ok(url) => { + // If the URL is valid, reformat it to a sanitized string representation + url.to_string() + } + Err(_) => { + // If the URL is invalid, return as-is for error reporting later + self.uri.trim().to_string() + } }; - Ok(PubkyAppTag { + PubkyAppTag { uri, label, created_at: self.created_at, - }) + } } - async fn validate(&self, id: &str) -> Result<(), DynError> { - self.validate_id(id).await?; + fn validate(&self, id: &str) -> Result<(), String> { + // Validate the tag ID + self.validate_id(id)?; - // Validate label length based on characters + // Validate label length if self.label.chars().count() > MAX_TAG_LABEL_LENGTH { - return Err("Tag label exceeds maximum length".into()); + return Err("Validation Error: Tag label exceeds maximum length".to_string()); } - // TODO: more validation? - - Ok(()) + // Validate URI format + match Url::parse(&self.uri) { + Ok(_) => Ok(()), + Err(_) => Err(format!( + "Validation Error: Invalid URI format: {}", + self.uri + )), + } } } @@ -94,10 +98,9 @@ mod tests { use super::*; use crate::traits::Validatable; use bytes::Bytes; - use tokio; - #[tokio::test] - async fn test_create_id() { + #[test] + fn test_create_id() { let tag = PubkyAppTag { uri: "https://example.com/post/1".to_string(), created_at: 1627849723000, @@ -112,11 +115,11 @@ mod tests { assert!(!tag_id.is_empty()); } - #[tokio::test] - async fn test_new() { + #[test] + fn test_new() { let uri = "https://example.com/post/1".to_string(); let label = "interesting".to_string(); - let tag = PubkyAppTag::new(uri.clone(), label.clone()).await; + let tag = PubkyAppTag::new(uri.clone(), label.clone()); assert_eq!(tag.uri, uri); assert_eq!(tag.label, label); @@ -125,8 +128,8 @@ mod tests { assert!(tag.created_at <= now && tag.created_at >= now - 1000); // within 1 second } - #[tokio::test] - async fn test_get_path() { + #[test] + fn test_get_path() { let tag = PubkyAppTag { uri: "https://example.com/post/1".to_string(), created_at: 1627849723000, @@ -140,20 +143,20 @@ mod tests { assert_eq!(path, expected_path); } - #[tokio::test] - async fn test_sanitize() { + #[test] + fn test_sanitize() { let tag = PubkyAppTag { uri: "https://example.com/post/1".to_string(), label: " CoOl ".to_string(), created_at: 1627849723000, }; - let sanitized_tag = tag.sanitize().await.unwrap(); + let sanitized_tag = tag.sanitize(); assert_eq!(sanitized_tag.label, "cool"); } - #[tokio::test] - async fn test_validate_valid() { + #[test] + fn test_validate_valid() { let tag = PubkyAppTag { uri: "https://example.com/post/1".to_string(), label: "cool".to_string(), @@ -161,12 +164,12 @@ mod tests { }; let id = tag.create_id(); - let result = tag.validate(&id).await; + let result = tag.validate(&id); assert!(result.is_ok()); } - #[tokio::test] - async fn test_validate_invalid_label_length() { + #[test] + fn test_validate_invalid_label_length() { let tag = PubkyAppTag { uri: "https://example.com/post/1".to_string(), label: "a".repeat(MAX_TAG_LABEL_LENGTH + 1), @@ -174,16 +177,16 @@ mod tests { }; let id = tag.create_id(); - let result = tag.validate(&id).await; + let result = tag.validate(&id); assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - "Tag label exceeds maximum length" + "Validation Error: Tag label exceeds maximum length" ); } - #[tokio::test] - async fn test_validate_invalid_id() { + #[test] + fn test_validate_invalid_id() { let tag = PubkyAppTag { uri: "https://example.com/post/1".to_string(), label: "cool".to_string(), @@ -191,13 +194,13 @@ mod tests { }; let invalid_id = "INVALIDID"; - let result = tag.validate(&invalid_id).await; + let result = tag.validate(&invalid_id); assert!(result.is_err()); // You can check the specific error message if necessary } - #[tokio::test] - async fn test_try_from_valid() { + #[test] + fn test_try_from_valid() { let tag_json = r#" { "uri": "pubky://user_pubky_id/pub/pubky.app/v1/profile.json", @@ -210,13 +213,10 @@ mod tests { "pubky://user_pubky_id/pub/pubky.app/v1/profile.json".to_string(), "Cool Tag".to_string(), ) - .await .create_id(); let blob = Bytes::from(tag_json); - let tag = ::try_from(&blob, &id) - .await - .unwrap(); + let tag = ::try_from(&blob, &id).unwrap(); assert_eq!( tag.uri, "pubky://user_pubky_id/pub/pubky.app/v1/profile.json" @@ -224,8 +224,8 @@ mod tests { assert_eq!(tag.label, "cool tag"); // After sanitization } - #[tokio::test] - async fn test_try_from_invalid_uri() { + #[test] + fn test_try_from_invalid_uri() { let tag_json = r#" { "uri": "invalid_uri", @@ -234,10 +234,13 @@ mod tests { } "#; - let id = "SomeID"; // The ID doesn't matter here + let id = "B55PGPFV1E5E0HQ2PB76EQGXPR"; let blob = Bytes::from(tag_json); - let result = ::try_from(&blob, &id).await; + let result = ::try_from(&blob, &id); assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "Invalid URI in tag"); + assert_eq!( + result.unwrap_err().to_string(), + "Validation Error: Invalid URI format: invalid_uri" + ); } } diff --git a/src/traits.rs b/src/traits.rs index 0498358..91b35c4 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,5 +1,3 @@ -use crate::types::DynError; -use async_trait::async_trait; use base32::{decode, encode, Alphabet}; use blake3::Hasher; use bytes::Bytes; @@ -7,7 +5,6 @@ use chrono::{DateTime, Duration, NaiveDate, Utc}; use pubky_common::timestamp::Timestamp; use serde::de::DeserializeOwned; -#[async_trait] pub trait TimestampId { /// Creates a unique identifier based on the current timestamp. fn create_id(&self) -> String { @@ -17,10 +14,10 @@ pub trait TimestampId { /// Validates that the provided ID is a valid Crockford Base32-encoded timestamp, /// 13 characters long, and represents a reasonable timestamp. - async fn validate_id(&self, id: &str) -> Result<(), DynError> { + fn validate_id(&self, id: &str) -> Result<(), String> { // Ensure ID is 13 characters long if id.len() != 13 { - return Err("Invalid ID length: must be 13 characters".into()); + return Err("Validation Error: Invalid ID length: must be 13 characters".into()); } // Decode the Crockford Base32-encoded ID @@ -28,7 +25,7 @@ pub trait TimestampId { decode(Alphabet::Crockford, id).ok_or("Failed to decode Crockford Base32 ID")?; if decoded_bytes.len() != 8 { - return Err("Invalid ID length after decoding".into()); + return Err("Validation Error: Invalid ID length after decoding".into()); } // Convert the decoded bytes to a timestamp in microseconds @@ -48,12 +45,14 @@ pub trait TimestampId { // Validate that the ID's timestamp is after October 1st, 2024 if id_datetime < oct_first_2024 { - return Err("Invalid ID: timestamp must be after October 1st, 2024".into()); + return Err( + "Validation Error: Invalid ID, timestamp must be after October 1st, 2024".into(), + ); } // Validate that the ID's timestamp is not more than 2 hours in the future if id_datetime > max_future { - return Err("Invalid ID: timestamp is too far in the future".into()); + return Err("Validation Error: Invalid ID, timestamp is too far in the future".into()); } Ok(()) @@ -61,7 +60,6 @@ pub trait TimestampId { } /// Trait for generating an ID based on the struct's data. -#[async_trait] pub trait HashId { fn get_id_data(&self) -> String; @@ -94,7 +92,7 @@ pub trait HashId { } /// Validates that the provided ID matches the generated ID. - async fn validate_id(&self, id: &str) -> Result<(), DynError> { + fn validate_id(&self, id: &str) -> Result<(), String> { let generated_id = self.create_id(); if generated_id != id { return Err(format!("Invalid ID: expected {}, found {}", generated_id, id).into()); @@ -103,19 +101,18 @@ pub trait HashId { } } -#[async_trait] pub trait Validatable: Sized + DeserializeOwned { - async fn try_from(blob: &Bytes, id: &str) -> Result { - let mut instance: Self = serde_json::from_slice(blob)?; - instance = instance.sanitize().await?; - instance.validate(id).await?; + fn try_from(blob: &Bytes, id: &str) -> Result { + let mut instance: Self = serde_json::from_slice(blob).map_err(|e| e.to_string())?; + instance = instance.sanitize(); + instance.validate(id)?; Ok(instance) } - async fn validate(&self, id: &str) -> Result<(), DynError>; + fn validate(&self, id: &str) -> Result<(), String>; - async fn sanitize(self) -> Result { - Ok(self) + fn sanitize(self) -> Self { + self } } diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 706e3cf..0000000 --- a/src/types.rs +++ /dev/null @@ -1 +0,0 @@ -pub type DynError = Box; diff --git a/src/user.rs b/src/user.rs index 5c4982e..317e291 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,6 +1,4 @@ use crate::traits::Validatable; -use crate::types::DynError; -use async_trait::async_trait; use serde::{Deserialize, Serialize}; use url::Url; use utoipa::ToSchema; @@ -17,7 +15,7 @@ const MAX_STATUS_LENGTH: usize = 50; /// Profile schema /// URI: /pub/pubky.app/profile.json -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Default)] pub struct PubkyAppUser { name: String, bio: Option, @@ -33,9 +31,8 @@ pub struct PubkyAppUserLink { url: String, } -#[async_trait] impl Validatable for PubkyAppUser { - async fn sanitize(self) -> Result { + fn sanitize(self) -> Self { // Sanitize name let sanitized_name = self.name.trim(); // Crop name to a maximum length of MAX_USERNAME_LENGTH characters @@ -85,87 +82,98 @@ impl Validatable for PubkyAppUser { links_vec .into_iter() .take(MAX_LINKS) - .filter_map(|link| { - let title = link.title.trim(); - let sanitized_url = link.url.trim(); - - // Parse and validate the URL - match Url::parse(sanitized_url) { - Ok(_) => { - // Ensure the title is within the allowed limit - let title = title - .chars() - .take(MAX_LINK_TITLE_LENGTH) - .collect::(); - - // Ensure the URL is within the allowed limit - let url = sanitized_url - .chars() - .take(MAX_LINK_URL_LENGTH) - .collect::(); - - // Only keep valid URLs - Some(PubkyAppUserLink { title, url }) - } - Err(_) => { - None // Discard invalid links - } - } - }) + .map(|link| link.sanitize()) + .filter(|link| !link.url.is_empty()) .collect() }); - Ok(PubkyAppUser { + PubkyAppUser { name, bio, image, links, status, - }) + } } - async fn validate(&self, _id: &str) -> Result<(), DynError> { + fn validate(&self, _id: &str) -> Result<(), String> { // Validate name length let name_length = self.name.chars().count(); if !(MIN_USERNAME_LENGTH..=MAX_USERNAME_LENGTH).contains(&name_length) { - return Err("Invalid name length".into()); + return Err("Validation Error: Invalid name length".into()); } // Validate bio length if let Some(bio) = &self.bio { if bio.chars().count() > MAX_BIO_LENGTH { - return Err("Bio exceeds maximum length".into()); + return Err("Validation Error: Bio exceeds maximum length".into()); } } // Validate image length if let Some(image) = &self.image { if image.chars().count() > MAX_IMAGE_LENGTH { - return Err("Image URI exceeds maximum length".into()); + return Err("Validation Error: Image URI exceeds maximum length".into()); } } // Validate links if let Some(links) = &self.links { if links.len() > MAX_LINKS { - return Err("Too many links".into()); + return Err("Too many links".to_string()); } + for link in links { - if link.title.chars().count() > MAX_LINK_TITLE_LENGTH - || link.url.chars().count() > MAX_LINK_URL_LENGTH - { - return Err("Link title or URL too long".into()); - } + link.validate(_id)?; } } // Validate status length if let Some(status) = &self.status { if status.chars().count() > MAX_STATUS_LENGTH { - return Err("Status exceeds maximum length".into()); + return Err("Validation Error: Status exceeds maximum length".into()); } } Ok(()) } } + +impl Validatable for PubkyAppUserLink { + fn sanitize(self) -> Self { + let title = self + .title + .trim() + .chars() + .take(MAX_LINK_TITLE_LENGTH) + .collect::(); + + let url = match Url::parse(self.url.trim()) { + Ok(parsed_url) => { + let sanitized_url = parsed_url.to_string(); + sanitized_url + .chars() + .take(MAX_LINK_URL_LENGTH) + .collect::() + } + Err(_) => "".to_string(), // Default to empty string for invalid URLs + }; + + PubkyAppUserLink { title, url } + } + + fn validate(&self, _id: &str) -> Result<(), String> { + if self.title.chars().count() > MAX_LINK_TITLE_LENGTH { + return Err("Validation Error: Link title exceeds maximum length".to_string()); + } + + if self.url.chars().count() > MAX_LINK_URL_LENGTH { + return Err("Validation Error: Link URL exceeds maximum length".to_string()); + } + + match Url::parse(&self.url) { + Ok(_) => Ok(()), + Err(_) => Err("Validation Error: Invalid URL format".to_string()), + } + } +} From 65083d06ccd99b49a748aa296b2cd134eb6803da Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Wed, 27 Nov 2024 17:27:58 +0100 Subject: [PATCH 03/13] Remove heavy deps --- Cargo.lock | 1140 +------------------------------------------------ Cargo.toml | 3 - src/tag.rs | 17 +- src/traits.rs | 40 +- 4 files changed, 42 insertions(+), 1158 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c3e753..fedb48d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,43 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - [[package]] name = "arrayref" version = "0.3.9" @@ -51,59 +14,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "async-trait" -version = "0.1.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - [[package]] name = "base32" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - [[package]] name = "blake3" version = "1.5.4" @@ -117,27 +33,6 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.8.0" @@ -159,159 +54,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "cobs" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "constant_time_eq" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "crypto_secretbox" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" -dependencies = [ - "aead", - "cipher", - "generic-array", - "poly1305", - "salsa20", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -323,81 +71,12 @@ dependencies = [ "syn", ] -[[package]] -name = "document-features" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" -dependencies = [ - "litrs", -] - -[[package]] -name = "dyn-clone" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "serde", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "nanorand", - "spin", -] - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -407,177 +86,12 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", -] - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "1.5.0" @@ -728,15 +242,6 @@ dependencies = [ "serde", ] -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - [[package]] name = "itoa" version = "1.0.14" @@ -744,209 +249,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] -name = "js-sys" -version = "0.3.72" +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" -dependencies = [ - "wasm-bindgen", -] +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] -name = "libc" -version = "0.2.165" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb4d3d38eab6c5239a362fa8bae48c03baf980a6e7079f063942d563ef3533e" - -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - -[[package]] -name = "litrs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" - -[[package]] -name = "mainline" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" -dependencies = [ - "bytes", - "crc", - "ed25519-dalek", - "flume", - "lru", - "rand", - "serde", - "serde_bencode", - "serde_bytes", - "sha1_smol", - "thiserror", - "tracing", -] - -[[package]] -name = "memchr" -version = "2.7.4" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pin-project-lite" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkarr" -version = "2.2.1-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b59d10828418841f34089b861b33d966b63ffd34fe770f4bc46df2d8aba118f5" -dependencies = [ - "base32", - "bytes", - "document-features", - "dyn-clone", - "ed25519-dalek", - "flume", - "futures", - "js-sys", - "lru", - "mainline", - "rand", - "self_cell", - "serde", - "simple-dns", - "thiserror", - "tracing", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "postcard" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "serde", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro2" version = "1.0.92" @@ -960,54 +279,15 @@ dependencies = [ name = "pubky-app-specs" version = "0.2.0" dependencies = [ - "async-trait", "base32", "blake3", "bytes", - "chrono", - "pubky-common", "serde", "serde_json", "url", "utoipa", ] -[[package]] -name = "pubky-common" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2176f9837b635d3fd4d85d36d5fc06baba85df53ca06f584af9686b85eb55177" -dependencies = [ - "argon2", - "base32", - "blake3", - "crypto_secretbox", - "ed25519-dalek", - "js-sys", - "once_cell", - "pkarr", - "postcard", - "pubky-timestamp", - "rand", - "serde", - "thiserror", -] - -[[package]] -name = "pubky-timestamp" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084b6e5bfcc186781b71257d636b660f20e94bb588c3ba52393fd9faf7a7bfda" -dependencies = [ - "base32", - "document-features", - "getrandom", - "httpdate", - "js-sys", - "once_cell", - "serde", -] - [[package]] name = "quote" version = "1.0.37" @@ -1017,78 +297,12 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "self_cell" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" - -[[package]] -name = "semver" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" - [[package]] name = "serde" version = "1.0.215" @@ -1098,25 +312,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde_bencode" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" -dependencies = [ - "serde", - "serde_bytes", -] - -[[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_derive" version = "1.0.215" @@ -1140,93 +335,24 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "rand_core", -] - -[[package]] -name = "simple-dns" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" -dependencies = [ - "bitflags", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "syn" version = "2.0.89" @@ -1249,26 +375,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tinystr" version = "0.7.6" @@ -1279,59 +385,12 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - [[package]] name = "url" version = "2.5.4" @@ -1378,168 +437,6 @@ dependencies = [ "syn", ] -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "web-sys" -version = "0.3.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "write16" version = "1.0.0" @@ -1576,27 +473,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.5" @@ -1618,12 +494,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index dce5752..c0c478e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,13 +9,10 @@ license = "MIT" documentation = "https://github.com/pubky/pubky-app-specs" [dependencies] -async-trait = "0.1" bytes = "^1.7.0" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" utoipa = "5.2.0" -pubky-common = "0.1.0" url = "2.5.4" base32 = "0.5.1" blake3 = "1.5.4" -chrono = "0.4.38" diff --git a/src/tag.rs b/src/tag.rs index a7dc9d9..42b5055 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,5 +1,6 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + use crate::traits::{HasPath, HashId, Validatable}; -use chrono::Utc; use serde::{Deserialize, Serialize}; use url::Url; @@ -23,7 +24,11 @@ pub struct PubkyAppTag { impl PubkyAppTag { pub fn new(uri: String, label: String) -> Self { - let created_at = Utc::now().timestamp_millis(); + let created_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros() as i64; + Self { uri, label, @@ -124,8 +129,12 @@ mod tests { assert_eq!(tag.uri, uri); assert_eq!(tag.label, label); // Check that created_at is recent - let now = Utc::now().timestamp_millis(); - assert!(tag.created_at <= now && tag.created_at >= now - 1000); // within 1 second + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros() as i64; + + assert!(tag.created_at <= now && tag.created_at >= now - 1_000_000); // within 1 second } #[test] diff --git a/src/traits.rs b/src/traits.rs index 91b35c4..07ced6e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,15 +1,23 @@ use base32::{decode, encode, Alphabet}; use blake3::Hasher; use bytes::Bytes; -use chrono::{DateTime, Duration, NaiveDate, Utc}; -use pubky_common::timestamp::Timestamp; use serde::de::DeserializeOwned; +use std::time::{SystemTime, UNIX_EPOCH}; pub trait TimestampId { /// Creates a unique identifier based on the current timestamp. fn create_id(&self) -> String { - let timestamp = Timestamp::now(); - timestamp.to_string() + // Get current time in microseconds since UNIX epoch + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros(); + + // Convert to big-endian bytes + let bytes = now.to_be_bytes(); + + // Encode the bytes using Base32 with the Crockford alphabet + encode(Alphabet::Crockford, &bytes) } /// Validates that the provided ID is a valid Crockford Base32-encoded timestamp, @@ -29,29 +37,29 @@ pub trait TimestampId { } // Convert the decoded bytes to a timestamp in microseconds - let timestamp_micros = i64::from_be_bytes(decoded_bytes.try_into().unwrap_or_default()); - let timestamp: i64 = timestamp_micros / 1_000_000; + let timestamp_micros = u64::from_be_bytes(decoded_bytes.try_into().unwrap()); - // Convert the timestamp to a DateTime - let id_datetime = DateTime::from_timestamp(timestamp, 0) - .unwrap_or_default() - .date_naive(); + // Get current time in microseconds + let now_micros = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros(); - // Define October 1st, 2024, at 00:00:00 UTC - let oct_first_2024 = NaiveDate::from_ymd_opt(2024, 10, 1).expect("Invalid date"); + // Define October 1st, 2024, in microseconds since UNIX epoch + let oct_first_2024_micros = 1727740800000000u64; // Timestamp for 2024-10-01 00:00:00 UTC - // Allowable future duration (2 hours) - let max_future = Utc::now().date_naive() + Duration::hours(2); + // Allowable future duration (2 hours) in microseconds + let max_future_micros = now_micros + 2 * 60 * 60 * 1_000_000; // Validate that the ID's timestamp is after October 1st, 2024 - if id_datetime < oct_first_2024 { + if timestamp_micros < oct_first_2024_micros { return Err( "Validation Error: Invalid ID, timestamp must be after October 1st, 2024".into(), ); } // Validate that the ID's timestamp is not more than 2 hours in the future - if id_datetime > max_future { + if timestamp_micros as u128 > max_future_micros { return Err("Validation Error: Invalid ID, timestamp is too far in the future".into()); } From e1087a1a9c7e8d9365529167a26ffb2fe8e582fe Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Wed, 27 Nov 2024 20:24:20 +0100 Subject: [PATCH 04/13] Improve pubky app post --- src/lib.rs | 2 + src/post.rs | 195 +++++++++++++++++++++++++++++++++++++++++++++++-- src/tag.rs | 38 +++++----- src/traits.rs | 6 +- src/version.rs | 3 + 5 files changed, 217 insertions(+), 27 deletions(-) create mode 100644 src/version.rs diff --git a/src/lib.rs b/src/lib.rs index f59fb3e..634aee3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ mod post; mod tag; pub mod traits; mod user; +mod version; pub use bookmark::PubkyAppBookmark; pub use file::PubkyAppFile; @@ -14,3 +15,4 @@ pub use mute::PubkyAppMute; pub use post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; pub use tag::PubkyAppTag; pub use user::{PubkyAppUser, PubkyAppUserLink}; +pub use version::{APP_PATH, PROTOCOL, VERSION}; diff --git a/src/post.rs b/src/post.rs index f027dca..c0937a6 100644 --- a/src/post.rs +++ b/src/post.rs @@ -1,4 +1,7 @@ -use crate::traits::{TimestampId, Validatable}; +use crate::{ + traits::{HasPath, TimestampId, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; use std::fmt; use url::Url; @@ -10,7 +13,7 @@ const MAX_LONG_CONTENT_LENGTH: usize = 50000; /// Represents the type of pubky-app posted data /// Used primarily to best display the content in UI -#[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum PubkyAppPostKind { #[default] @@ -32,11 +35,11 @@ impl fmt::Display for PubkyAppPostKind { } } -/// Used primarily to best display the content in UI +/// Represents embedded content within a post #[derive(Serialize, Deserialize, Default, Clone)] pub struct PubkyAppPostEmbed { - kind: PubkyAppPostKind, // If a repost: `short`, and uri of the reposted post. - uri: String, + kind: PubkyAppPostKind, // Kind of the embedded content + uri: String, // URI of the embedded content } /// Represents raw post in homeserver with content and kind @@ -55,8 +58,34 @@ pub struct PubkyAppPost { attachments: Option>, } +impl PubkyAppPost { + /// Creates a new `PubkyAppPost` instance and sanitizes it. + pub fn new( + content: String, + kind: PubkyAppPostKind, + parent: Option, + embed: Option, + attachments: Option>, + ) -> Self { + let post = PubkyAppPost { + content, + kind, + parent, + embed, + attachments, + }; + post.sanitize() + } +} + impl TimestampId for PubkyAppPost {} +impl HasPath for PubkyAppPost { + fn get_path(&self) -> String { + format!("{}posts/{}", APP_PATH, self.create_id()) + } +} + impl Validatable for PubkyAppPost { fn sanitize(self) -> Self { // Sanitize content @@ -134,8 +163,162 @@ impl Validatable for PubkyAppPost { _ => (), }; - // TODO: additional validation? + // TODO: additional validation. Attachement URLs...? Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use bytes::Bytes; + + #[test] + fn test_create_id() { + let post = PubkyAppPost::new( + "Hello World!".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let post_id = post.create_id(); + println!("Generated Post ID: {}", post_id); + + // Assert that the post ID is 13 characters long + assert_eq!(post_id.len(), 13); + } + + #[test] + fn test_new() { + let content = "This is a test post".to_string(); + let kind = PubkyAppPostKind::Short; + let post = PubkyAppPost::new(content.clone(), kind.clone(), None, None, None); + + assert_eq!(post.content, content); + assert_eq!(post.kind, kind); + assert!(post.parent.is_none()); + assert!(post.embed.is_none()); + assert!(post.attachments.is_none()); + } + + #[test] + fn test_get_path() { + let post = PubkyAppPost::new( + "Test post".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let post_id = post.create_id(); + let expected_path_len = format!("{}posts/{}", APP_PATH, post_id).len(); + let path = post.get_path(); + + assert_eq!(path.len(), expected_path_len); + } + + #[test] + fn test_sanitize() { + let content = " This is a test post with extra whitespace ".to_string(); + let post = PubkyAppPost::new( + content.clone(), + PubkyAppPostKind::Short, + Some("invalid uri".to_string()), + Some(PubkyAppPostEmbed { + kind: PubkyAppPostKind::Link, + uri: "invalid uri".to_string(), + }), + None, + ); + + let sanitized_post = post.sanitize(); + assert_eq!(sanitized_post.content, content.trim()); + assert!(sanitized_post.parent.is_none()); + assert!(sanitized_post.embed.is_none()); + } + + #[test] + fn test_validate_valid() { + let post = PubkyAppPost::new( + "Valid content".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let id = post.create_id(); + let result = post.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let post = PubkyAppPost::new( + "Valid content".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let invalid_id = "INVALIDID12345"; + let result = post.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let post_json = r#" + { + "content": "Hello World!", + "kind": "short", + "parent": null, + "embed": null, + "attachments": null + } + "#; + + let id = PubkyAppPost::new( + "Hello World!".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ) + .create_id(); + + let blob = Bytes::from(post_json); + let post = ::try_from(&blob, &id).unwrap(); + + assert_eq!(post.content, "Hello World!"); + } + + #[test] + fn test_try_from_invalid_content() { + let content = "[DELETED]".to_string(); + let post_json = format!( + r#"{{ + "content": "{}", + "kind": "short", + "parent": null, + "embed": null, + "attachments": null + }}"#, + content + ); + + let id = PubkyAppPost::new(content.clone(), PubkyAppPostKind::Short, None, None, None) + .create_id(); + + let blob = Bytes::from(post_json); + let post = ::try_from(&blob, &id).unwrap(); + + assert_eq!(post.content, "empty"); // After sanitization + } +} diff --git a/src/tag.rs b/src/tag.rs index 42b5055..e66f910 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,6 +1,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use crate::traits::{HasPath, HashId, Validatable}; +use crate::{ + traits::{HasPath, HashId, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; use url::Url; @@ -17,9 +20,9 @@ const MAX_TAG_LABEL_LENGTH: usize = 20; /// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) #[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppTag { - pub uri: String, - pub label: String, - pub created_at: i64, + uri: String, + label: String, + created_at: i64, } impl PubkyAppTag { @@ -40,7 +43,7 @@ impl PubkyAppTag { impl HasPath for PubkyAppTag { fn get_path(&self) -> String { - format!("pubky:///pub/pubky.app/tags/{}", self.create_id()) + format!("{}tags/{}", APP_PATH, self.create_id()) } } @@ -101,7 +104,7 @@ impl Validatable for PubkyAppTag { #[cfg(test)] mod tests { use super::*; - use crate::traits::Validatable; + use crate::{traits::Validatable, APP_PATH}; use bytes::Bytes; #[test] @@ -133,6 +136,8 @@ mod tests { .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_micros() as i64; + println!("TIMESTAMP {}", tag.created_at); + println!("TIMESTAMP {}", now); assert!(tag.created_at <= now && tag.created_at >= now - 1_000_000); // within 1 second } @@ -140,13 +145,13 @@ mod tests { #[test] fn test_get_path() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0032FNCGXE3R0".to_string(), created_at: 1627849723000, label: "cool".to_string(), }; let expected_id = tag.create_id(); - let expected_path = format!("pubky:///pub/pubky.app/tags/{}", expected_id); + let expected_path = format!("{}tags/{}", APP_PATH, expected_id); let path = tag.get_path(); assert_eq!(path, expected_path); @@ -155,7 +160,7 @@ mod tests { #[test] fn test_sanitize() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: " CoOl ".to_string(), created_at: 1627849723000, }; @@ -167,7 +172,7 @@ mod tests { #[test] fn test_validate_valid() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: "cool".to_string(), created_at: 1627849723000, }; @@ -180,7 +185,7 @@ mod tests { #[test] fn test_validate_invalid_label_length() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: "a".repeat(MAX_TAG_LABEL_LENGTH + 1), created_at: 1627849723000, }; @@ -197,7 +202,7 @@ mod tests { #[test] fn test_validate_invalid_id() { let tag = PubkyAppTag { - uri: "https://example.com/post/1".to_string(), + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), label: "cool".to_string(), created_at: 1627849723000, }; @@ -212,24 +217,21 @@ mod tests { fn test_try_from_valid() { let tag_json = r#" { - "uri": "pubky://user_pubky_id/pub/pubky.app/v1/profile.json", + "uri": "pubky://user_pubky_id/pub/pubky.app/profile.json", "label": "Cool Tag", "created_at": 1627849723000 } "#; let id = PubkyAppTag::new( - "pubky://user_pubky_id/pub/pubky.app/v1/profile.json".to_string(), + "pubky://user_pubky_id/pub/pubky.app/profile.json".to_string(), "Cool Tag".to_string(), ) .create_id(); let blob = Bytes::from(tag_json); let tag = ::try_from(&blob, &id).unwrap(); - assert_eq!( - tag.uri, - "pubky://user_pubky_id/pub/pubky.app/v1/profile.json" - ); + assert_eq!(tag.uri, "pubky://user_pubky_id/pub/pubky.app/profile.json"); assert_eq!(tag.label, "cool tag"); // After sanitization } diff --git a/src/traits.rs b/src/traits.rs index 07ced6e..04afe29 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -11,7 +11,7 @@ pub trait TimestampId { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") - .as_micros(); + .as_micros() as u64; // Convert to big-endian bytes let bytes = now.to_be_bytes(); @@ -43,7 +43,7 @@ pub trait TimestampId { let now_micros = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") - .as_micros(); + .as_micros() as u64; // Define October 1st, 2024, in microseconds since UNIX epoch let oct_first_2024_micros = 1727740800000000u64; // Timestamp for 2024-10-01 00:00:00 UTC @@ -59,7 +59,7 @@ pub trait TimestampId { } // Validate that the ID's timestamp is not more than 2 hours in the future - if timestamp_micros as u128 > max_future_micros { + if timestamp_micros > max_future_micros { return Err("Validation Error: Invalid ID, timestamp is too far in the future".into()); } diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..bac70cd --- /dev/null +++ b/src/version.rs @@ -0,0 +1,3 @@ +pub static VERSION: &str = "0.2.0"; +pub static APP_PATH: &str = "/pub/pubky.app/"; +pub static PROTOCOL: &str = "pubky://"; From bf4742bb1f694d65079c99e8b07c094c0c734f43 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 28 Nov 2024 12:55:51 +0100 Subject: [PATCH 05/13] Upgrade bookmarks --- src/bookmark.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++----- src/post.rs | 6 ++-- src/tag.rs | 6 ++-- src/traits.rs | 2 +- 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/bookmark.rs b/src/bookmark.rs index 8e24e96..b1274e8 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -1,5 +1,9 @@ -use crate::traits::{HashId, Validatable}; +use crate::{ + traits::{HasPath, HashId, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; /// Represents raw homeserver bookmark with id /// URI: /pub/pubky.app/bookmarks/:bookmark_id @@ -15,6 +19,17 @@ pub struct PubkyAppBookmark { created_at: i64, } +impl PubkyAppBookmark { + /// Creates a new `PubkyAppBookmark` instance. + pub fn new(uri: String) -> Self { + let created_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros() as i64; + Self { uri, created_at }.sanitize() + } +} + impl HashId for PubkyAppBookmark { /// Bookmark ID is created based on the hash of the URI bookmarked fn get_id_data(&self) -> String { @@ -22,6 +37,12 @@ impl HashId for PubkyAppBookmark { } } +impl HasPath for PubkyAppBookmark { + fn create_path(&self) -> String { + format!("{}bookmarks/{}", APP_PATH, self.create_id()) + } +} + impl Validatable for PubkyAppBookmark { fn validate(&self, id: &str) -> Result<(), String> { self.validate_id(id)?; @@ -30,13 +51,68 @@ impl Validatable for PubkyAppBookmark { } } -#[test] -fn test_create_bookmark_id() { - let bookmark = PubkyAppBookmark { - uri: "user_id/pub/pubky.app/posts/post_id".to_string(), - created_at: 1627849723, - }; +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use bytes::Bytes; + + #[test] + fn test_create_bookmark_id() { + let bookmark = PubkyAppBookmark { + uri: "user_id/pub/pubky.app/posts/post_id".to_string(), + created_at: 1627849723, + }; - let bookmark_id = bookmark.create_id(); - println!("Generated Bookmark ID: {}", bookmark_id); + let bookmark_id = bookmark.create_id(); + assert_eq!(bookmark_id, "AF7KQ6NEV5XV1EG5DVJ2E74JJ4"); + } + + #[test] + fn test_create_path() { + let bookmark = PubkyAppBookmark { + uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), + created_at: 1627849723, + }; + let expected_id = bookmark.create_id(); + let expected_path = format!("{}bookmarks/{}", APP_PATH, expected_id); + let path = bookmark.create_path(); + assert_eq!(path, expected_path); + } + + #[test] + fn test_validate_valid() { + let bookmark = + PubkyAppBookmark::new("pubky://user_id/pub/pubky.app/posts/post_id".to_string()); + let id = bookmark.create_id(); + let result = bookmark.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let bookmark = PubkyAppBookmark::new("user_id/pub/pubky.app/posts/post_id".to_string()); + let invalid_id = "INVALIDID"; + let result = bookmark.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let bookmark_json = r#" + { + "uri": "user_id/pub/pubky.app/posts/post_id", + "created_at": 1627849723 + } + "#; + + let uri = "user_id/pub/pubky.app/posts/post_id".to_string(); + let bookmark = PubkyAppBookmark::new(uri.clone()); + let id = bookmark.create_id(); + + let blob = Bytes::from(bookmark_json); + let bookmark_parsed = ::try_from(&blob, &id).unwrap(); + + assert_eq!(bookmark_parsed.uri, uri); + } } diff --git a/src/post.rs b/src/post.rs index c0937a6..1ffdcd5 100644 --- a/src/post.rs +++ b/src/post.rs @@ -81,7 +81,7 @@ impl PubkyAppPost { impl TimestampId for PubkyAppPost {} impl HasPath for PubkyAppPost { - fn get_path(&self) -> String { + fn create_path(&self) -> String { format!("{}posts/{}", APP_PATH, self.create_id()) } } @@ -206,7 +206,7 @@ mod tests { } #[test] - fn test_get_path() { + fn test_create_path() { let post = PubkyAppPost::new( "Test post".to_string(), PubkyAppPostKind::Short, @@ -217,7 +217,7 @@ mod tests { let post_id = post.create_id(); let expected_path_len = format!("{}posts/{}", APP_PATH, post_id).len(); - let path = post.get_path(); + let path = post.create_path(); assert_eq!(path.len(), expected_path_len); } diff --git a/src/tag.rs b/src/tag.rs index e66f910..af5c65a 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -42,7 +42,7 @@ impl PubkyAppTag { } impl HasPath for PubkyAppTag { - fn get_path(&self) -> String { + fn create_path(&self) -> String { format!("{}tags/{}", APP_PATH, self.create_id()) } } @@ -143,7 +143,7 @@ mod tests { } #[test] - fn test_get_path() { + fn test_create_path() { let tag = PubkyAppTag { uri: "pubky://operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0032FNCGXE3R0".to_string(), created_at: 1627849723000, @@ -152,7 +152,7 @@ mod tests { let expected_id = tag.create_id(); let expected_path = format!("{}tags/{}", APP_PATH, expected_id); - let path = tag.get_path(); + let path = tag.create_path(); assert_eq!(path, expected_path); } diff --git a/src/traits.rs b/src/traits.rs index 04afe29..ff16003 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -125,5 +125,5 @@ pub trait Validatable: Sized + DeserializeOwned { } pub trait HasPath { - fn get_path(&self) -> String; + fn create_path(&self) -> String; } From a7267a345c9eeb063e5cea9378c613150cdcfa8f Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 28 Nov 2024 14:06:18 +0100 Subject: [PATCH 06/13] Upgrade bookmarks --- src/bookmark.rs | 7 +-- src/common.rs | 13 +++++ src/file.rs | 129 ++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 4 +- src/post.rs | 6 ++- src/tag.rs | 14 ++---- src/traits.rs | 16 ++---- src/version.rs | 3 -- 8 files changed, 156 insertions(+), 36 deletions(-) create mode 100644 src/common.rs delete mode 100644 src/version.rs diff --git a/src/bookmark.rs b/src/bookmark.rs index b1274e8..65ec5b4 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -1,9 +1,9 @@ use crate::{ + common::timestamp, traits::{HasPath, HashId, Validatable}, APP_PATH, }; use serde::{Deserialize, Serialize}; -use std::time::{SystemTime, UNIX_EPOCH}; /// Represents raw homeserver bookmark with id /// URI: /pub/pubky.app/bookmarks/:bookmark_id @@ -22,10 +22,7 @@ pub struct PubkyAppBookmark { impl PubkyAppBookmark { /// Creates a new `PubkyAppBookmark` instance. pub fn new(uri: String) -> Self { - let created_at = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros() as i64; + let created_at = timestamp(); Self { uri, created_at }.sanitize() } } diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..21ccadb --- /dev/null +++ b/src/common.rs @@ -0,0 +1,13 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +pub static VERSION: &str = "0.2.0"; +pub static APP_PATH: &str = "/pub/pubky.app/"; +pub static PROTOCOL: &str = "pubky://"; + +/// Returns the current timestamp in microseconds since the UNIX epoch. +pub fn timestamp() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros() as i64 +} diff --git a/src/file.rs b/src/file.rs index 7cf5953..e6389ac 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,8 +1,13 @@ -use crate::traits::{TimestampId, Validatable}; +use crate::{ + common::timestamp, + traits::{HasPath, TimestampId, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; -/// Profile schema -#[derive(Deserialize, Serialize, Debug, Default)] +/// Represents a file uploaded by the user. +/// URI: /pub/pubky.app/files/:file_id +#[derive(Deserialize, Serialize, Debug, Default, Clone)] pub struct PubkyAppFile { name: String, created_at: i64, @@ -11,8 +16,28 @@ pub struct PubkyAppFile { size: i64, } +impl PubkyAppFile { + /// Creates a new `PubkyAppFile` instance. + pub fn new(name: String, src: String, content_type: String, size: i64) -> Self { + let created_at = timestamp(); + Self { + name, + created_at, + src, + content_type, + size, + } + } +} + impl TimestampId for PubkyAppFile {} +impl HasPath for PubkyAppFile { + fn create_path(&self) -> String { + format!("{}files/{}", APP_PATH, self.create_id()) + } +} + impl Validatable for PubkyAppFile { // TODO: content_type validation. fn validate(&self, id: &str) -> Result<(), String> { @@ -22,3 +47,101 @@ impl Validatable for PubkyAppFile { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use bytes::Bytes; + + #[test] + fn test_new() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "pubky://user_id/pub/pubky.app/blobs/id".to_string(), + "image/png".to_string(), + 1024, + ); + assert_eq!(file.name, "example.png"); + assert_eq!(file.src, "pubky://user_id/pub/pubky.app/blobs/id"); + assert_eq!(file.content_type, "image/png"); + assert_eq!(file.size, 1024); + // Check that created_at is recent + let now = timestamp(); + assert!(file.created_at <= now && file.created_at >= now - 1_000_000); // within 1 second + } + + #[test] + fn test_create_path() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "pubky://user_id/pub/pubky.app/blobs/id".to_string(), + "image/png".to_string(), + 1024, + ); + let file_id = file.create_id(); + let path = file.create_path(); + + // Check if the path starts with the expected prefix + let prefix = format!("{}files/", APP_PATH); + assert!(path.starts_with(&prefix)); + + let expected_path_len = prefix.len() + file_id.len(); + assert_eq!(path.len(), expected_path_len); + } + + #[test] + fn test_validate_valid() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "/uploads/example.png".to_string(), + "image/png".to_string(), + 1024, + ); + let id = file.create_id(); + let result = file.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "/uploads/example.png".to_string(), + "image/png".to_string(), + 1024, + ); + let invalid_id = "INVALIDID"; + let result = file.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let file_json = r#" + { + "name": "example.png", + "created_at": 1627849723, + "src": "/uploads/example.png", + "content_type": "image/png", + "size": 1024 + } + "#; + + let file = PubkyAppFile::new( + "example.png".to_string(), + "/uploads/example.png".to_string(), + "image/png".to_string(), + 1024, + ); + let id = file.create_id(); + + let blob = Bytes::from(file_json); + let file_parsed = ::try_from(&blob, &id).unwrap(); + + assert_eq!(file_parsed.name, "example.png"); + assert_eq!(file_parsed.src, "/uploads/example.png"); + assert_eq!(file_parsed.content_type, "image/png"); + assert_eq!(file_parsed.size, 1024); + } +} diff --git a/src/lib.rs b/src/lib.rs index 634aee3..3182e68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ mod bookmark; +mod common; mod file; mod follow; mod mute; @@ -6,13 +7,12 @@ mod post; mod tag; pub mod traits; mod user; -mod version; pub use bookmark::PubkyAppBookmark; +pub use common::{APP_PATH, PROTOCOL, VERSION}; pub use file::PubkyAppFile; pub use follow::PubkyAppFollow; pub use mute::PubkyAppMute; pub use post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; pub use tag::PubkyAppTag; pub use user::{PubkyAppUser, PubkyAppUserLink}; -pub use version::{APP_PATH, PROTOCOL, VERSION}; diff --git a/src/post.rs b/src/post.rs index 1ffdcd5..37bc26c 100644 --- a/src/post.rs +++ b/src/post.rs @@ -216,9 +216,13 @@ mod tests { ); let post_id = post.create_id(); - let expected_path_len = format!("{}posts/{}", APP_PATH, post_id).len(); let path = post.create_path(); + // Check if the path starts with the expected prefix + let prefix = format!("{}posts/", APP_PATH); + assert!(path.starts_with(&prefix)); + + let expected_path_len = prefix.len() + post_id.len(); assert_eq!(path.len(), expected_path_len); } diff --git a/src/tag.rs b/src/tag.rs index af5c65a..4aaac7e 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -1,6 +1,5 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - use crate::{ + common::timestamp, traits::{HasPath, HashId, Validatable}, APP_PATH, }; @@ -27,11 +26,7 @@ pub struct PubkyAppTag { impl PubkyAppTag { pub fn new(uri: String, label: String) -> Self { - let created_at = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros() as i64; - + let created_at = timestamp(); Self { uri, label, @@ -132,10 +127,7 @@ mod tests { assert_eq!(tag.uri, uri); assert_eq!(tag.label, label); // Check that created_at is recent - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros() as i64; + let now = timestamp(); println!("TIMESTAMP {}", tag.created_at); println!("TIMESTAMP {}", now); diff --git a/src/traits.rs b/src/traits.rs index ff16003..fe27958 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,17 +1,14 @@ +use crate::common::timestamp; use base32::{decode, encode, Alphabet}; use blake3::Hasher; use bytes::Bytes; use serde::de::DeserializeOwned; -use std::time::{SystemTime, UNIX_EPOCH}; pub trait TimestampId { /// Creates a unique identifier based on the current timestamp. fn create_id(&self) -> String { // Get current time in microseconds since UNIX epoch - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros() as u64; + let now = timestamp(); // Convert to big-endian bytes let bytes = now.to_be_bytes(); @@ -37,16 +34,13 @@ pub trait TimestampId { } // Convert the decoded bytes to a timestamp in microseconds - let timestamp_micros = u64::from_be_bytes(decoded_bytes.try_into().unwrap()); + let timestamp_micros = i64::from_be_bytes(decoded_bytes.try_into().unwrap()); // Get current time in microseconds - let now_micros = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros() as u64; + let now_micros = timestamp(); // Define October 1st, 2024, in microseconds since UNIX epoch - let oct_first_2024_micros = 1727740800000000u64; // Timestamp for 2024-10-01 00:00:00 UTC + let oct_first_2024_micros = 1727740800000000; // Timestamp for 2024-10-01 00:00:00 UTC // Allowable future duration (2 hours) in microseconds let max_future_micros = now_micros + 2 * 60 * 60 * 1_000_000; diff --git a/src/version.rs b/src/version.rs deleted file mode 100644 index bac70cd..0000000 --- a/src/version.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub static VERSION: &str = "0.2.0"; -pub static APP_PATH: &str = "/pub/pubky.app/"; -pub static PROTOCOL: &str = "pubky://"; From 3bdc68aae0c0e0c236008adcb9648d32d01f824a Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 28 Nov 2024 17:04:21 +0100 Subject: [PATCH 07/13] Remove utoipa and bytes --- Cargo.lock | 54 ------------- Cargo.toml | 2 - src/bookmark.rs | 3 +- src/file.rs | 3 +- src/follow.rs | 73 ++++++++++++++++- src/mute.rs | 70 ++++++++++++++++- src/post.rs | 8 +- src/tag.rs | 5 +- src/traits.rs | 7 +- src/user.rs | 203 ++++++++++++++++++++++++++++++++++++++++++++++-- 10 files changed, 345 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fedb48d..045f57a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,6 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "bytes" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" - [[package]] name = "cc" version = "1.2.1" @@ -71,12 +65,6 @@ dependencies = [ "syn", ] -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -86,12 +74,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - [[package]] name = "icu_collections" version = "1.5.0" @@ -231,17 +213,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" -dependencies = [ - "equivalent", - "hashbrown", - "serde", -] - [[package]] name = "itoa" version = "1.0.14" @@ -281,11 +252,9 @@ version = "0.2.0" dependencies = [ "base32", "blake3", - "bytes", "serde", "serde_json", "url", - "utoipa", ] [[package]] @@ -414,29 +383,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utoipa" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714" -dependencies = [ - "indexmap", - "serde", - "serde_json", - "utoipa-gen", -] - -[[package]] -name = "utoipa-gen" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index c0c478e..b070918 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,8 @@ license = "MIT" documentation = "https://github.com/pubky/pubky-app-specs" [dependencies] -bytes = "^1.7.0" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" -utoipa = "5.2.0" url = "2.5.4" base32 = "0.5.1" blake3 = "1.5.4" diff --git a/src/bookmark.rs b/src/bookmark.rs index 65ec5b4..bf53b2c 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -52,7 +52,6 @@ impl Validatable for PubkyAppBookmark { mod tests { use super::*; use crate::traits::Validatable; - use bytes::Bytes; #[test] fn test_create_bookmark_id() { @@ -107,7 +106,7 @@ mod tests { let bookmark = PubkyAppBookmark::new(uri.clone()); let id = bookmark.create_id(); - let blob = Bytes::from(bookmark_json); + let blob = bookmark_json.as_bytes(); let bookmark_parsed = ::try_from(&blob, &id).unwrap(); assert_eq!(bookmark_parsed.uri, uri); diff --git a/src/file.rs b/src/file.rs index e6389ac..1d1ada0 100644 --- a/src/file.rs +++ b/src/file.rs @@ -52,7 +52,6 @@ impl Validatable for PubkyAppFile { mod tests { use super::*; use crate::traits::Validatable; - use bytes::Bytes; #[test] fn test_new() { @@ -136,7 +135,7 @@ mod tests { ); let id = file.create_id(); - let blob = Bytes::from(file_json); + let blob = file_json.as_bytes(); let file_parsed = ::try_from(&blob, &id).unwrap(); assert_eq!(file_parsed.name, "example.png"); diff --git a/src/follow.rs b/src/follow.rs index 768b7e7..0f0ef07 100644 --- a/src/follow.rs +++ b/src/follow.rs @@ -1,21 +1,86 @@ -use crate::traits::Validatable; +use crate::{ + common::timestamp, + traits::{HasPubkyIdPath, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; /// Represents raw homeserver follow object with timestamp +/// +/// On follow objects, the main data is encoded in the path +/// /// URI: /pub/pubky.app/follows/:user_id /// /// Example URI: /// -/// `/pub/pubky.app/follows/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy`` +/// `/pub/pubky.app/follows/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` /// -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppFollow { created_at: i64, } +impl PubkyAppFollow { + /// Creates a new `PubkyAppFollow` instance. + pub fn new() -> Self { + let created_at = timestamp(); + Self { created_at } + } +} + impl Validatable for PubkyAppFollow { fn validate(&self, _id: &str) -> Result<(), String> { - // TODO: additional follow validation? E.g, validate `created_at` ? + // TODO: additional follow validation? E.g., validate `created_at`? Ok(()) } } + +impl HasPubkyIdPath for PubkyAppFollow { + fn create_path(&self, pubky_id: &str) -> String { + format!("{}follows/{}", APP_PATH, pubky_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let follow = PubkyAppFollow::new(); + // Check that created_at is recent + let now = timestamp(); + assert!(follow.created_at <= now && follow.created_at >= now - 1_000_000); + // within 1 second + } + + #[test] + fn test_create_path_with_id() { + let mute = PubkyAppFollow::new(); + let path = mute.create_path("user_id123"); + assert_eq!(path, "/pub/pubky.app/follows/user_id123"); + } + + #[test] + fn test_validate() { + let follow = PubkyAppFollow::new(); + let result = follow.validate("some_user_id"); + assert!(result.is_ok()); + } + + #[test] + fn test_try_from_valid() { + let follow_json = r#" + { + "created_at": 1627849723 + } + "#; + + let blob = follow_json.as_bytes(); + let follow_parsed = + ::try_from(&blob, "some_user_id").unwrap(); + + assert_eq!(follow_parsed.created_at, 1627849723); + } +} diff --git a/src/mute.rs b/src/mute.rs index a2f995a..b0e740b 100644 --- a/src/mute.rs +++ b/src/mute.rs @@ -1,4 +1,8 @@ -use crate::traits::Validatable; +use crate::{ + common::timestamp, + traits::{HasPubkyIdPath, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; /// Represents raw homeserver Mute object with timestamp @@ -6,16 +10,74 @@ use serde::{Deserialize, Serialize}; /// /// Example URI: /// -/// `/pub/pubky.app/mutes/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy`` +/// `/pub/pubky.app/mutes/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` /// -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppMute { created_at: i64, } +impl PubkyAppMute { + /// Creates a new `PubkyAppMute` instance. + pub fn new() -> Self { + let created_at = timestamp(); + Self { created_at } + } +} + impl Validatable for PubkyAppMute { fn validate(&self, _id: &str) -> Result<(), String> { - // TODO: additional Mute validation? E.g, validate `created_at` ? + // TODO: additional Mute validation? E.g., validate `created_at` ? Ok(()) } } + +impl HasPubkyIdPath for PubkyAppMute { + fn create_path(&self, pubky_id: &str) -> String { + format!("{}mutes/{}", APP_PATH, pubky_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::timestamp; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let mute = PubkyAppMute::new(); + // Check that created_at is recent + let now = timestamp(); + assert!(mute.created_at <= now && mute.created_at >= now - 1_000_000); + // within 1 second + } + + #[test] + fn test_create_path_with_id() { + let mute = PubkyAppMute::new(); + let path = mute.create_path("user_id123"); + assert_eq!(path, "/pub/pubky.app/mutes/user_id123"); + } + + #[test] + fn test_validate() { + let mute = PubkyAppMute::new(); + let result = mute.validate("some_user_id"); + assert!(result.is_ok()); + } + + #[test] + fn test_try_from_valid() { + let mute_json = r#" + { + "created_at": 1627849723 + } + "#; + + let blob = mute_json.as_bytes(); + let mute_parsed = ::try_from(&blob, "some_user_id").unwrap(); + + assert_eq!(mute_parsed.created_at, 1627849723); + } +} diff --git a/src/post.rs b/src/post.rs index 37bc26c..89d3db1 100644 --- a/src/post.rs +++ b/src/post.rs @@ -5,7 +5,6 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::fmt; use url::Url; -use utoipa::ToSchema; // Validation const MAX_SHORT_CONTENT_LENGTH: usize = 1000; @@ -13,7 +12,7 @@ const MAX_LONG_CONTENT_LENGTH: usize = 50000; /// Represents the type of pubky-app posted data /// Used primarily to best display the content in UI -#[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] pub enum PubkyAppPostKind { #[default] @@ -173,7 +172,6 @@ impl Validatable for PubkyAppPost { mod tests { use super::*; use crate::traits::Validatable; - use bytes::Bytes; #[test] fn test_create_id() { @@ -297,7 +295,7 @@ mod tests { ) .create_id(); - let blob = Bytes::from(post_json); + let blob = post_json.as_bytes(); let post = ::try_from(&blob, &id).unwrap(); assert_eq!(post.content, "Hello World!"); @@ -320,7 +318,7 @@ mod tests { let id = PubkyAppPost::new(content.clone(), PubkyAppPostKind::Short, None, None, None) .create_id(); - let blob = Bytes::from(post_json); + let blob = post_json.as_bytes(); let post = ::try_from(&blob, &id).unwrap(); assert_eq!(post.content, "empty"); // After sanitization diff --git a/src/tag.rs b/src/tag.rs index 4aaac7e..15481e3 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -100,7 +100,6 @@ impl Validatable for PubkyAppTag { mod tests { use super::*; use crate::{traits::Validatable, APP_PATH}; - use bytes::Bytes; #[test] fn test_create_id() { @@ -221,7 +220,7 @@ mod tests { ) .create_id(); - let blob = Bytes::from(tag_json); + let blob = tag_json.as_bytes(); let tag = ::try_from(&blob, &id).unwrap(); assert_eq!(tag.uri, "pubky://user_pubky_id/pub/pubky.app/profile.json"); assert_eq!(tag.label, "cool tag"); // After sanitization @@ -238,7 +237,7 @@ mod tests { "#; let id = "B55PGPFV1E5E0HQ2PB76EQGXPR"; - let blob = Bytes::from(tag_json); + let blob = tag_json.as_bytes(); let result = ::try_from(&blob, &id); assert!(result.is_err()); assert_eq!( diff --git a/src/traits.rs b/src/traits.rs index fe27958..8c09983 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,7 +1,6 @@ use crate::common::timestamp; use base32::{decode, encode, Alphabet}; use blake3::Hasher; -use bytes::Bytes; use serde::de::DeserializeOwned; pub trait TimestampId { @@ -104,7 +103,7 @@ pub trait HashId { } pub trait Validatable: Sized + DeserializeOwned { - fn try_from(blob: &Bytes, id: &str) -> Result { + fn try_from(blob: &[u8], id: &str) -> Result { let mut instance: Self = serde_json::from_slice(blob).map_err(|e| e.to_string())?; instance = instance.sanitize(); instance.validate(id)?; @@ -121,3 +120,7 @@ pub trait Validatable: Sized + DeserializeOwned { pub trait HasPath { fn create_path(&self) -> String; } + +pub trait HasPubkyIdPath { + fn create_path(&self, pubky_id: &str) -> String; +} diff --git a/src/user.rs b/src/user.rs index 317e291..ccbb54e 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,9 +1,11 @@ -use crate::traits::Validatable; +use crate::{ + traits::{HasPath, Validatable}, + APP_PATH, +}; use serde::{Deserialize, Serialize}; use url::Url; -use utoipa::ToSchema; -// Validation +// Validation constants const MIN_USERNAME_LENGTH: usize = 3; const MAX_USERNAME_LENGTH: usize = 50; const MAX_BIO_LENGTH: usize = 160; @@ -15,7 +17,7 @@ const MAX_STATUS_LENGTH: usize = 50; /// Profile schema /// URI: /pub/pubky.app/profile.json -#[derive(Deserialize, Serialize, Debug, Default)] +#[derive(Deserialize, Serialize, Debug, Default, Clone)] pub struct PubkyAppUser { name: String, bio: Option, @@ -25,12 +27,38 @@ pub struct PubkyAppUser { } /// Represents a user's single link with a title and URL. -#[derive(Serialize, Deserialize, ToSchema, Default, Clone, Debug)] +#[derive(Serialize, Deserialize, Default, Clone, Debug)] pub struct PubkyAppUserLink { title: String, url: String, } +impl PubkyAppUser { + /// Creates a new `PubkyAppUser` instance and sanitizes it. + pub fn new( + name: String, + bio: Option, + image: Option, + links: Option>, + status: Option, + ) -> Self { + Self { + name, + bio, + image, + links, + status, + } + .sanitize() + } +} + +impl HasPath for PubkyAppUser { + fn create_path(&self) -> String { + format!("{}profile.json", APP_PATH) + } +} + impl Validatable for PubkyAppUser { fn sanitize(self) -> Self { // Sanitize name @@ -177,3 +205,168 @@ impl Validatable for PubkyAppUserLink { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use crate::APP_PATH; + + #[test] + fn test_new() { + let user = PubkyAppUser::new( + "Alice".to_string(), + Some("Maximalist".to_string()), + Some("https://example.com/image.png".to_string()), + Some(vec![ + PubkyAppUserLink { + title: "GitHub".to_string(), + url: "https://github.com/alice".to_string(), + }, + PubkyAppUserLink { + title: "Website".to_string(), + url: "https://alice.dev".to_string(), + }, + ]), + Some("Exploring the decentralized web.".to_string()), + ); + + assert_eq!(user.name, "Alice"); + assert_eq!(user.bio.as_deref(), Some("Maximalist")); + assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); + assert_eq!( + user.status.as_deref(), + Some("Exploring the decentralized web.") + ); + assert!(user.links.is_some()); + assert_eq!(user.links.as_ref().unwrap().len(), 2); + } + + #[test] + fn test_create_path() { + let user = PubkyAppUser::default(); + let path = user.create_path(); + assert_eq!(path, format!("{}profile.json", APP_PATH)); + } + + #[test] + fn test_sanitize() { + let user = PubkyAppUser::new( + " Alice ".to_string(), + Some(" Maximalist and developer. ".to_string()), + Some("https://example.com/image.png".to_string()), + Some(vec![ + PubkyAppUserLink { + title: " GitHub ".to_string(), + url: " https://github.com/alice ".to_string(), + }, + PubkyAppUserLink { + title: "Website".to_string(), + url: "invalid_url".to_string(), // Invalid URL + }, + ]), + Some(" Exploring the decentralized web. ".to_string()), + ); + + assert_eq!(user.name, "Alice"); + assert_eq!(user.bio.as_deref(), Some("Maximalist and developer.")); + assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); + assert_eq!( + user.status.as_deref(), + Some("Exploring the decentralized web.") + ); + assert!(user.links.is_some()); + let links = user.links.unwrap(); + assert_eq!(links.len(), 1); // Invalid URL link should be filtered out + assert_eq!(links[0].title, "GitHub"); + assert_eq!(links[0].url, "https://github.com/alice"); + } + + #[test] + fn test_validate_valid() { + let user = PubkyAppUser::new( + "Alice".to_string(), + Some("Maximalist".to_string()), + Some("https://example.com/image.png".to_string()), + None, + Some("Exploring the decentralized web.".to_string()), + ); + + let result = user.validate(""); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_name() { + let user = PubkyAppUser::new( + "Al".to_string(), // Too short + None, + None, + None, + None, + ); + + let result = user.validate(""); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Validation Error: Invalid name length" + ); + } + + #[test] + fn test_try_from_valid() { + let user_json = r#" + { + "name": "Alice", + "bio": "Maximalist", + "image": "https://example.com/image.png", + "links": [ + { + "title": "GitHub", + "url": "https://github.com/alice" + }, + { + "title": "Website", + "url": "https://alice.dev" + } + ], + "status": "Exploring the decentralized web." + } + "#; + + let blob = user_json.as_bytes(); + let user = ::try_from(&blob, "").unwrap(); + + assert_eq!(user.name, "Alice"); + assert_eq!(user.bio.as_deref(), Some("Maximalist")); + assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); + assert_eq!( + user.status.as_deref(), + Some("Exploring the decentralized web.") + ); + assert!(user.links.is_some()); + assert_eq!(user.links.as_ref().unwrap().len(), 2); + } + + #[test] + fn test_try_from_invalid_link() { + let user_json = r#" + { + "name": "Alice", + "links": [ + { + "title": "GitHub", + "url": "invalid_url" + } + ] + } + "#; + + let blob = user_json.as_bytes(); + let user = ::try_from(&blob, "").unwrap(); + + // Since the link URL is invalid, it should be filtered out + assert!(user.links.is_none() || user.links.as_ref().unwrap().is_empty()); + } +} From d6273d899ce0f53b639f9545c156430f396a266f Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 28 Nov 2024 17:12:48 +0100 Subject: [PATCH 08/13] Fix make all fields public --- src/bookmark.rs | 4 ++-- src/file.rs | 10 +++++----- src/follow.rs | 2 +- src/mute.rs | 2 +- src/post.rs | 14 +++++++------- src/tag.rs | 6 +++--- src/user.rs | 15 +++++++-------- 7 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/bookmark.rs b/src/bookmark.rs index bf53b2c..1df46a8 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -15,8 +15,8 @@ use serde::{Deserialize, Serialize}; /// Where bookmark_id is Crockford-base32(Blake3("{uri_bookmarked}"")[:half]) #[derive(Serialize, Deserialize, Default)] pub struct PubkyAppBookmark { - uri: String, - created_at: i64, + pub uri: String, + pub created_at: i64, } impl PubkyAppBookmark { diff --git a/src/file.rs b/src/file.rs index 1d1ada0..2f0355c 100644 --- a/src/file.rs +++ b/src/file.rs @@ -9,11 +9,11 @@ use serde::{Deserialize, Serialize}; /// URI: /pub/pubky.app/files/:file_id #[derive(Deserialize, Serialize, Debug, Default, Clone)] pub struct PubkyAppFile { - name: String, - created_at: i64, - src: String, - content_type: String, - size: i64, + pub name: String, + pub created_at: i64, + pub src: String, + pub content_type: String, + pub size: i64, } impl PubkyAppFile { diff --git a/src/follow.rs b/src/follow.rs index 0f0ef07..fcb24c0 100644 --- a/src/follow.rs +++ b/src/follow.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppFollow { - created_at: i64, + pub created_at: i64, } impl PubkyAppFollow { diff --git a/src/mute.rs b/src/mute.rs index b0e740b..3879418 100644 --- a/src/mute.rs +++ b/src/mute.rs @@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize}; /// #[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppMute { - created_at: i64, + pub created_at: i64, } impl PubkyAppMute { diff --git a/src/post.rs b/src/post.rs index 89d3db1..447ec60 100644 --- a/src/post.rs +++ b/src/post.rs @@ -37,8 +37,8 @@ impl fmt::Display for PubkyAppPostKind { /// Represents embedded content within a post #[derive(Serialize, Deserialize, Default, Clone)] pub struct PubkyAppPostEmbed { - kind: PubkyAppPostKind, // Kind of the embedded content - uri: String, // URI of the embedded content + pub kind: PubkyAppPostKind, // Kind of the embedded content + pub uri: String, // URI of the embedded content } /// Represents raw post in homeserver with content and kind @@ -50,11 +50,11 @@ pub struct PubkyAppPostEmbed { /// `/pub/pubky.app/posts/00321FCW75ZFY` #[derive(Serialize, Deserialize, Default, Clone)] pub struct PubkyAppPost { - content: String, - kind: PubkyAppPostKind, - parent: Option, // If a reply, the URI of the parent post. - embed: Option, - attachments: Option>, + pub content: String, + pub kind: PubkyAppPostKind, + pub parent: Option, // If a reply, the URI of the parent post. + pub embed: Option, + pub attachments: Option>, } impl PubkyAppPost { diff --git a/src/tag.rs b/src/tag.rs index 15481e3..773f642 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -19,9 +19,9 @@ const MAX_TAG_LABEL_LENGTH: usize = 20; /// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) #[derive(Serialize, Deserialize, Default, Debug)] pub struct PubkyAppTag { - uri: String, - label: String, - created_at: i64, + pub uri: String, + pub label: String, + pub created_at: i64, } impl PubkyAppTag { diff --git a/src/user.rs b/src/user.rs index ccbb54e..f772d9d 100644 --- a/src/user.rs +++ b/src/user.rs @@ -15,22 +15,21 @@ const MAX_LINK_TITLE_LENGTH: usize = 100; const MAX_LINK_URL_LENGTH: usize = 300; const MAX_STATUS_LENGTH: usize = 50; -/// Profile schema /// URI: /pub/pubky.app/profile.json #[derive(Deserialize, Serialize, Debug, Default, Clone)] pub struct PubkyAppUser { - name: String, - bio: Option, - image: Option, - links: Option>, - status: Option, + pub name: String, + pub bio: Option, + pub image: Option, + pub links: Option>, + pub status: Option, } /// Represents a user's single link with a title and URL. #[derive(Serialize, Deserialize, Default, Clone, Debug)] pub struct PubkyAppUserLink { - title: String, - url: String, + pub title: String, + pub url: String, } impl PubkyAppUser { From b099c5bb7f39af32fa50d6b08b453c0377606a33 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Thu, 28 Nov 2024 20:37:46 +0100 Subject: [PATCH 09/13] Bump to v0.2.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/common.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 045f57a..d991865 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,7 +248,7 @@ dependencies = [ [[package]] name = "pubky-app-specs" -version = "0.2.0" +version = "0.2.1" dependencies = [ "base32", "blake3", diff --git a/Cargo.toml b/Cargo.toml index b070918..6cd6201 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pubky-app-specs" -version = "0.2.0" +version = "0.2.1" edition = "2021" description = "Pubky.app Data Model Specifications" homepage = "https://pubky.app" diff --git a/src/common.rs b/src/common.rs index 21ccadb..81392c8 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,6 +1,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; -pub static VERSION: &str = "0.2.0"; +pub static VERSION: &str = "0.2.1"; pub static APP_PATH: &str = "/pub/pubky.app/"; pub static PROTOCOL: &str = "pubky://"; From e169a4561a1ff1966984a3b8b221e6529c8daff0 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 29 Nov 2024 08:59:50 +0100 Subject: [PATCH 10/13] Add optional openapi schemas --- Cargo.lock | 47 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++++ src/bookmark.rs | 4 ++++ src/file.rs | 4 ++++ src/follow.rs | 4 ++++ src/mute.rs | 4 ++++ src/post.rs | 6 ++++++ src/tag.rs | 4 ++++ src/user.rs | 5 +++++ 9 files changed, 82 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d991865..eec06dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ dependencies = [ "syn", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -74,6 +80,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "icu_collections" version = "1.5.0" @@ -213,6 +225,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + [[package]] name = "itoa" version = "1.0.14" @@ -255,6 +278,7 @@ dependencies = [ "serde", "serde_json", "url", + "utoipa", ] [[package]] @@ -383,6 +407,29 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utoipa" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 6cd6201..bece68f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,7 @@ serde_json = "1.0.133" url = "2.5.4" base32 = "0.5.1" blake3 = "1.5.4" +utoipa = { version = "5.2.0", optional = true } + +[features] +openapi = ["utoipa"] diff --git a/src/bookmark.rs b/src/bookmark.rs index 1df46a8..90a323f 100644 --- a/src/bookmark.rs +++ b/src/bookmark.rs @@ -5,6 +5,9 @@ use crate::{ }; use serde::{Deserialize, Serialize}; +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + /// Represents raw homeserver bookmark with id /// URI: /pub/pubky.app/bookmarks/:bookmark_id /// @@ -14,6 +17,7 @@ use serde::{Deserialize, Serialize}; /// /// Where bookmark_id is Crockford-base32(Blake3("{uri_bookmarked}"")[:half]) #[derive(Serialize, Deserialize, Default)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppBookmark { pub uri: String, pub created_at: i64, diff --git a/src/file.rs b/src/file.rs index 2f0355c..7e54577 100644 --- a/src/file.rs +++ b/src/file.rs @@ -5,9 +5,13 @@ use crate::{ }; use serde::{Deserialize, Serialize}; +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + /// Represents a file uploaded by the user. /// URI: /pub/pubky.app/files/:file_id #[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppFile { pub name: String, pub created_at: i64, diff --git a/src/follow.rs b/src/follow.rs index fcb24c0..ad67642 100644 --- a/src/follow.rs +++ b/src/follow.rs @@ -5,6 +5,9 @@ use crate::{ }; use serde::{Deserialize, Serialize}; +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + /// Represents raw homeserver follow object with timestamp /// /// On follow objects, the main data is encoded in the path @@ -16,6 +19,7 @@ use serde::{Deserialize, Serialize}; /// `/pub/pubky.app/follows/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` /// #[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppFollow { pub created_at: i64, } diff --git a/src/mute.rs b/src/mute.rs index 3879418..15d4f00 100644 --- a/src/mute.rs +++ b/src/mute.rs @@ -5,6 +5,9 @@ use crate::{ }; use serde::{Deserialize, Serialize}; +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + /// Represents raw homeserver Mute object with timestamp /// URI: /pub/pubky.app/mutes/:user_id /// @@ -13,6 +16,7 @@ use serde::{Deserialize, Serialize}; /// `/pub/pubky.app/mutes/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` /// #[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppMute { pub created_at: i64, } diff --git a/src/post.rs b/src/post.rs index 447ec60..185c1e7 100644 --- a/src/post.rs +++ b/src/post.rs @@ -10,10 +10,14 @@ use url::Url; const MAX_SHORT_CONTENT_LENGTH: usize = 1000; const MAX_LONG_CONTENT_LENGTH: usize = 50000; +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + /// Represents the type of pubky-app posted data /// Used primarily to best display the content in UI #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub enum PubkyAppPostKind { #[default] Short, @@ -36,6 +40,7 @@ impl fmt::Display for PubkyAppPostKind { /// Represents embedded content within a post #[derive(Serialize, Deserialize, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppPostEmbed { pub kind: PubkyAppPostKind, // Kind of the embedded content pub uri: String, // URI of the embedded content @@ -49,6 +54,7 @@ pub struct PubkyAppPostEmbed { /// /// `/pub/pubky.app/posts/00321FCW75ZFY` #[derive(Serialize, Deserialize, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppPost { pub content: String, pub kind: PubkyAppPostKind, diff --git a/src/tag.rs b/src/tag.rs index 773f642..46d1431 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -9,6 +9,9 @@ use url::Url; // Validation const MAX_TAG_LABEL_LENGTH: usize = 20; +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + /// Represents raw homeserver tag with id /// URI: /pub/pubky.app/tags/:tag_id /// @@ -18,6 +21,7 @@ const MAX_TAG_LABEL_LENGTH: usize = 20; /// /// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) #[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppTag { pub uri: String, pub label: String, diff --git a/src/user.rs b/src/user.rs index f772d9d..f3ad78f 100644 --- a/src/user.rs +++ b/src/user.rs @@ -15,8 +15,12 @@ const MAX_LINK_TITLE_LENGTH: usize = 100; const MAX_LINK_URL_LENGTH: usize = 300; const MAX_STATUS_LENGTH: usize = 50; +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + /// URI: /pub/pubky.app/profile.json #[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppUser { pub name: String, pub bio: Option, @@ -27,6 +31,7 @@ pub struct PubkyAppUser { /// Represents a user's single link with a title and URL. #[derive(Serialize, Deserialize, Default, Clone, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] pub struct PubkyAppUserLink { pub title: String, pub url: String, From fa1e2d402c54cdf4ec70cd2d431621d2795b3d87 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 29 Nov 2024 09:59:02 +0100 Subject: [PATCH 11/13] Add feed and last_read models --- README.md | 13 +-- src/feed.rs | 260 +++++++++++++++++++++++++++++++++++++++++++++++ src/follow.rs | 2 +- src/last_read.rs | 89 ++++++++++++++++ src/lib.rs | 4 + 5 files changed, 361 insertions(+), 7 deletions(-) create mode 100644 src/feed.rs create mode 100644 src/last_read.rs diff --git a/README.md b/README.md index 2ee80aa..c668b4c 100644 --- a/README.md +++ b/README.md @@ -284,12 +284,13 @@ This document intents to be a faithful representation of our [Rust pubky.app mod - `sort` (string, required): Determines the sorting order of the feed content. Supported values are: - `recent`: Most recent content first. - `popularity`: Content with the highest engagement. - - `content` (string, required): Defines the type of content displayed. Options include: - - `all`: Includes all content types. - - `posts`: Only posts are shown. - - `images`: Only media images. - - `videos`: Only media videos. - - `links`: Only links. + - `content` (string, optional): Defines the type of content to filter. Possible values are the same as post kinds: + - `short` + - `long` + - `image` + - `video` + - `link` + - `file` - `name` (string, required): The user-defined name for this feed configuration. - `created_at` (integer, required): Timestamp (Unix epoch in milliseconds) representing when the feed was created. diff --git a/src/feed.rs b/src/feed.rs new file mode 100644 index 0000000..77f10cb --- /dev/null +++ b/src/feed.rs @@ -0,0 +1,260 @@ +use crate::{ + common::timestamp, + traits::{HasPath, HashId, Validatable}, + PubkyAppPostKind, APP_PATH, +}; +use serde::{Deserialize, Serialize}; +use serde_json; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Enum representing the reach of the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub enum PubkyAppFeedReach { + Following, + Followers, + Friends, + All, +} + +/// Enum representing the layout of the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub enum PubkyAppFeedLayout { + Columns, + Wide, + Visual, +} + +/// Enum representing the sort order of the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub enum PubkyAppFeedSort { + Recent, + Popularity, +} + +/// Configuration object for the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppFeedConfig { + pub tags: Option>, + pub reach: PubkyAppFeedReach, + pub layout: PubkyAppFeedLayout, + pub sort: PubkyAppFeedSort, + pub content: Option, +} + +/// Represents a feed configuration. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppFeed { + pub feed: PubkyAppFeedConfig, + pub name: String, + pub created_at: i64, +} + +impl PubkyAppFeed { + /// Creates a new `PubkyAppFeed` instance and sanitizes it. + pub fn new( + tags: Option>, + reach: PubkyAppFeedReach, + layout: PubkyAppFeedLayout, + sort: PubkyAppFeedSort, + content: Option, + name: String, + ) -> Self { + let created_at = timestamp(); + let feed = PubkyAppFeedConfig { + tags, + reach, + layout, + sort, + content, + }; + Self { + feed, + name, + created_at, + } + .sanitize() + } +} + +impl HashId for PubkyAppFeed { + /// Generates an ID based on the serialized `feed` object. + fn get_id_data(&self) -> String { + serde_json::to_string(&self.feed).unwrap_or_default() + } +} + +impl HasPath for PubkyAppFeed { + fn create_path(&self) -> String { + format!("{}feeds/{}", APP_PATH, self.create_id()) + } +} + +impl Validatable for PubkyAppFeed { + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; + + // Validate name + if self.name.trim().is_empty() { + return Err("Validation Error: Feed name cannot be empty".into()); + } + + // Additional validations can be added here + Ok(()) + } + + fn sanitize(self) -> Self { + // Sanitize name + let name = self.name.trim().to_string(); + + // Sanitize tags + let feed = PubkyAppFeedConfig { + tags: self.feed.tags.map(|tags| { + tags.into_iter() + .map(|tag| tag.trim().to_lowercase()) + .collect() + }), + ..self.feed + }; + + PubkyAppFeed { + feed, + name, + created_at: self.created_at, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + Some(PubkyAppPostKind::Image), + "Rust Bitcoiners".to_string(), + ); + + let feed_config = PubkyAppFeedConfig { + tags: Some(vec!["bitcoin".to_string(), "rust".to_string()]), + reach: PubkyAppFeedReach::Following, + layout: PubkyAppFeedLayout::Columns, + sort: PubkyAppFeedSort::Recent, + content: Some(PubkyAppPostKind::Image), + }; + assert_eq!(feed.feed, feed_config); + assert_eq!(feed.name, "Rust Bitcoiners"); + // Check that created_at is recent + let now = timestamp(); + assert!(feed.created_at <= now && feed.created_at >= now - 1_000_000); + } + + #[test] + fn test_create_id() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + "Rust Bitcoiners".to_string(), + ); + + let feed_id = feed.create_id(); + println!("Feed ID: {}", feed_id); + // The ID should not be empty + assert!(!feed_id.is_empty()); + } + + #[test] + fn test_validate() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + "Rust Bitcoiners".to_string(), + ); + let feed_id = feed.create_id(); + + let result = feed.validate(&feed_id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + "Rust Bitcoiners".to_string(), + ); + let invalid_id = "INVALIDID"; + let result = feed.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_sanitize() { + let feed = PubkyAppFeed::new( + Some(vec![" BiTcoin ".to_string(), " RUST ".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + " Rust Bitcoiners".to_string(), + ); + assert_eq!(feed.name, "Rust Bitcoiners"); + assert_eq!( + feed.feed.tags, + Some(vec!["bitcoin".to_string(), "rust".to_string()]) + ); + } + + #[test] + fn test_try_from_valid() { + let feed_json = r#" + { + "feed": { + "tags": ["bitcoin", "rust"], + "reach": "following", + "layout": "columns", + "sort": "recent", + "content": "video" + }, + "name": "My Feed", + "created_at": 1700000000 + } + "#; + + let feed: PubkyAppFeed = serde_json::from_str(feed_json).unwrap(); + let feed_id = feed.create_id(); + + let blob = feed_json.as_bytes(); + let feed_parsed = ::try_from(&blob, &feed_id).unwrap(); + + assert_eq!(feed_parsed.name, "My Feed"); + assert_eq!( + feed_parsed.feed.tags, + Some(vec!["bitcoin".to_string(), "rust".to_string()]) + ); + } +} diff --git a/src/follow.rs b/src/follow.rs index ad67642..b22305f 100644 --- a/src/follow.rs +++ b/src/follow.rs @@ -55,8 +55,8 @@ mod tests { let follow = PubkyAppFollow::new(); // Check that created_at is recent let now = timestamp(); - assert!(follow.created_at <= now && follow.created_at >= now - 1_000_000); // within 1 second + assert!(follow.created_at <= now && follow.created_at >= now - 1_000_000); } #[test] diff --git a/src/last_read.rs b/src/last_read.rs new file mode 100644 index 0000000..eff2600 --- /dev/null +++ b/src/last_read.rs @@ -0,0 +1,89 @@ +use crate::{ + common::timestamp, + traits::{HasPath, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents the last read timestamp for notifications. +/// URI: /pub/pubky.app/last_read +#[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppLastRead { + pub timestamp: i64, // Unix epoch time in milliseconds +} + +impl PubkyAppLastRead { + /// Creates a new `PubkyAppLastRead` instance. + pub fn new() -> Self { + let timestamp = timestamp() / 1_000; // to millis + Self { timestamp } + } +} + +impl Validatable for PubkyAppLastRead { + fn validate(&self, _id: &str) -> Result<(), String> { + // Validate timestamp is a positive integer + if self.timestamp <= 0 { + return Err("Validation Error: Timestamp must be a positive integer".into()); + } + Ok(()) + } +} + +impl HasPath for PubkyAppLastRead { + fn create_path(&self) -> String { + format!("{}last_read", APP_PATH) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let last_read = PubkyAppLastRead::new(); + let now = timestamp() / 1_000; + // within 1 second + assert!(last_read.timestamp <= now && last_read.timestamp >= now - 1_000); + } + + #[test] + fn test_create_path() { + let last_read = PubkyAppLastRead::new(); + let path = last_read.create_path(); + assert_eq!(path, format!("{}last_read", APP_PATH)); + } + + #[test] + fn test_validate() { + let last_read = PubkyAppLastRead::new(); + let result = last_read.validate(""); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_timestamp() { + let last_read = PubkyAppLastRead { timestamp: -1 }; + let result = last_read.validate(""); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let last_read_json = r#" + { + "timestamp": 1700000000 + } + "#; + + let blob = last_read_json.as_bytes(); + let last_read = ::try_from(&blob, "").unwrap(); + assert_eq!(last_read.timestamp, 1700000000); + } +} diff --git a/src/lib.rs b/src/lib.rs index 3182e68..a3f32c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ mod bookmark; mod common; +mod feed; mod file; mod follow; +mod last_read; mod mute; mod post; mod tag; @@ -10,8 +12,10 @@ mod user; pub use bookmark::PubkyAppBookmark; pub use common::{APP_PATH, PROTOCOL, VERSION}; +pub use feed::{PubkyAppFeed, PubkyAppFeedLayout, PubkyAppFeedReach, PubkyAppFeedSort}; pub use file::PubkyAppFile; pub use follow::PubkyAppFollow; +pub use last_read::PubkyAppLastRead; pub use mute::PubkyAppMute; pub use post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; pub use tag::PubkyAppTag; From 946a1730ca22822102e1c9ba5f1d963e9c8d8c97 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 29 Nov 2024 10:34:23 +0100 Subject: [PATCH 12/13] Improve docs --- README.md | 454 +++++++++++++++++------------------------------------- 1 file changed, 138 insertions(+), 316 deletions(-) diff --git a/README.md b/README.md index c668b4c..48dc363 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,115 @@ # Pubky.app Data Model Specification -_Version 0.2.0_ +_Version 0.2.1_ + +> ⚠️ **Warning: Rapid Development Phase** +> This specification is in an **early development phase** and is evolving quickly. Expect frequent changes and updates as the system matures. Consider this a **v0 draft**. +> +> When we reach the first stable, long-term support version of the schemas, paths will adopt the format: `pubky.app/v1/` to indicate compatibility and stability. + +## Table of Contents + +1. [Introduction](#introduction) +2. [Quick Start](#quick-start) +3. [Data Models](#data-models) + - [PubkyAppUser](#pubkyappuser) + - [PubkyAppFile](#pubkyappfile) + - [PubkyAppPost](#pubkyapppost) + - [PubkyAppTag](#pubkyapptag) + - [PubkyAppBookmark](#pubkyappbookmark) + - [PubkyAppFollow](#pubkyappfollow) + - [PubkyAppMute](#pubkyappmute) + - [PubkyAppFeed](#pubkyappfeed) + - [PubkyAppLastRead](#pubkyapplastread) +4. [Validation Rules](#validation-rules) + - [Common Rules](#common-rules) + - [ID Generation](#id-generation) +5. [Glossary](#glossary) +6. [Examples](#examples) + - [PubkyAppUser](#example-pubkyappuser) + - [PubkyAppPost](#example-pubkyapppost) + - [PubkyAppTag](#example-pubkyapptag) +7. [License](#license) + +--- ## Introduction -This document specifies the data models and validation rules for the Pubky.app client and homeserver interactions. It defines the structures of data entities, their properties, and the validation rules to ensure data integrity and consistency. This specification is intended for developers who wish to implement their own libraries or clients compatible with Pubky.app. +This document specifies the data models and validation rules for the **Pubky.app** clients interactions. It defines the structure of data entities, their properties, and the validation rules to ensure data integrity and consistency. This is intended for developers building compatible libraries or clients. This document intents to be a faithful representation of our [Rust pubky.app models](https://github.com/pubky/pubky-app-specs/tree/main/src). If you intend to develop in Rust, use them directly. In case of disagreement between this document and the Rust implementation, the Rust implementation prevails. -## Data Models - -### PubkyAppUser - -**Description:** Represents a user's profile information. - -**URI:** `/pub/pubky.app/profile.json` +--- -**Fields:** +## Quick Start -- `name` (string, required): The user's name. -- `bio` (string, optional): A short biography. -- `image` (string, optional): A URL to the user's profile image. -- `links` (array of `UserLink`, optional): A list of links associated with the user. -- `status` (string, optional): The user's current status. +Pubky.app models are designed for decentralized content sharing. The system uses a combination of timestamp-based IDs and Blake3-hashed IDs encoded in Crockford Base32 to ensure unique identifiers for each entity. -**`UserLink` Object:** +### Concepts: -- `title` (string, required): The title of the link. -- `url` (string, required): The URL of the link. +- **Timestamp IDs** for sequential objects like posts and files. +- **Hash IDs** for content-based uniqueness (e.g., tags and bookmarks). +- **Validation Rules** ensure consistent and interoperable data formats. -**Validation Rules:** +--- -- **`name`:** +## Data Models - - Must be at least **3** and at most **50** characters. - - Cannot be the keyword `[DELETED]`; this is reserved for deleted profiles. +### PubkyAppUser -- **`bio`:** +**Description:** Represents a user's profile information. - - Maximum length of **160** characters if provided. +**URI:** `/pub/pubky.app/profile.json` -- **`image`:** +| **Field** | **Type** | **Description** | **Validation Rules** | +| --------- | -------- | --------------------------------------- | -------------------------------------------------------------------------------------------- | +| `name` | String | User's name. | Required. Length: 3–50 characters. Cannot be `"[DELETED]"`. | +| `bio` | String | Short biography. | Optional. Maximum length: 160 characters. | +| `image` | String | URL to the user's profile image. | Optional. Valid URL. Maximum length: 300 characters. | +| `links` | Array | List of associated links (title + URL). | Optional. Maximum of 5 links, each with title (100 chars max) and valid URL (300 chars max). | +| `status` | String | User's current status. | Optional. Maximum length: 50 characters. | - - If provided, must be a valid URL. - - Maximum length of **300** characters. +**Validation Notes:** -- **`links`:** +- Reserved keyword `[DELETED]` cannot be used for `name`. +- Each `UserLink` in `links` must have a valid title and URL. - - Maximum of **5** links. - - Each `UserLink` must have: - - `title`: Maximum length of **100** characters. - - `url`: Must be a valid URL, maximum length of **300** characters. +**Example: Valid User** -- **`status`:** - - Maximum length of **50** characters if provided. +```json +{ + "name": "Alice", + "bio": "Toxic maximalist.", + "image": "pubky://user_id/pub/pubky.app/files/0000000000000", + "links": [ + { + "title": "GitHub", + "url": "https://github.com/alice" + } + ], + "status": "Exploring decentralized tech." +} +``` --- ### PubkyAppFile -**Description:** Represents a file uploaded by the user. +**Description:** Represents metadata of file uploaded by the user. **URI:** `/pub/pubky.app/files/:file_id` -**Fields:** - -- `name` (string, required): The name of the file. -- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the file was created. -- `src` (string, required): The source URL or path of the file. -- `content_type` (string, required): The MIME type of the file. -- `size` (integer, required): The size of the file in bytes. +| **Field** | **Type** | **Description** | **Validation Rules** | +| -------------- | -------- | --------------------------- | --------------------------- | +| `name` | String | Name of the file. | Required. | +| `created_at` | Integer | Unix timestamp of creation. | Required. | +| `src` | String | File blob URL | Required. | +| `content_type` | String | MIME type of the file. | Required. | +| `size` | Integer | Size of the file in bytes. | Required. Positive integer. | -**Validation Rules:** +**Validation Notes:** -- **ID Validation:** - - - The `file_id` in the URI must be a valid **Timestamp ID** (see [ID Generation](#id-generation)). - -- **Additional Validation:** - - Validation for `content_type`, `size`, and other fields should be implemented as needed. +- The `file_id` in the URI must be a valid **Timestamp ID**. --- @@ -88,54 +119,37 @@ This document intents to be a faithful representation of our [Rust pubky.app mod **URI:** `/pub/pubky.app/posts/:post_id` -**Fields:** - -- `content` (string, required): The content of the post. -- `kind` (string, required): The type of post. Possible values are: - - - `Short` - - `Long` - - `Image` - - `Video` - - `Link` - - `File` - -- `parent` (string, optional): URI of the parent post if this is a reply. -- `embed` (object, optional): Embedded content. -- `attachments` (array of strings, optional): A list of attachment URIs. - -**`embed` Object:** - -- `kind` (string, required): Type of the embedded content. Same as `kind` in `PubkyAppPost`. -- `uri` (string, required): URI of the embedded content. - -**Validation Rules:** - -- **ID Validation:** +| **Field** | **Type** | **Description** | **Validation Rules** | +| ------------- | -------- | ------------------------------------ | -------------------------------------------------------------------------- | +| `content` | String | Content of the post. | Required. Max length: 1000 (short), 50000 (long). Cannot be `"[DELETED]"`. | +| `kind` | String | Type of post. | Required. Must be a valid `PubkyAppPostKind` value. | +| `parent` | String | URI of the parent post (if a reply). | Optional. Must be a valid URI if present. | +| `embed` | Object | Embedded content (type + URI). | Optional. URI must be valid if present. | +| `attachments` | Array | List of attachment URIs. | Optional. Each must be a valid URI. | - - The `post_id` in the URI must be a valid **Timestamp ID** (see [ID Generation](#id-generation)). +**Post Kinds:** -- **`content`:** +- `short` +- `long` +- `image` +- `video` +- `link` +- `file` - - Must not be the keyword `[DELETED]`; this is reserved for deleted posts. - - **For `kind` of `Short`:** - - Maximum length of **1000** characters. - - **For `kind` of `Long`:** - - Maximum length of **50000** characters. - - **For other `kind` values:** - - Maximum length of **1000** characters. +**Example: Valid Post** -- **`parent`:** - - - If provided, must be a valid URI. - -- **`embed`:** - - - If provided: - - `uri` must be a valid URI. - -- **Additional Validation:** - - Validation for `attachments` and other fields should be implemented as needed. +```json +{ + "content": "Hello world! This is my first post.", + "kind": "short", + "parent": null, + "embed": { + "kind": "short", + "uri": "pubky://user_id/pub/pubky.app/posts/0000000000000" + }, + "attachments": ["pubky://user_id/pub/pubky.app/files/0000000000000"] +} +``` --- @@ -145,25 +159,15 @@ This document intents to be a faithful representation of our [Rust pubky.app mod **URI:** `/pub/pubky.app/tags/:tag_id` -**Fields:** - -- `uri` (string, required): The URI that is tagged. -- `label` (string, required): The tag label. -- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the tag was created. - -**Validation Rules:** - -- **ID Validation:** +| **Field** | **Type** | **Description** | **Validation Rules** | +| ------------ | -------- | --------------------------- | -------------------------------------------------------- | +| `uri` | String | URI of the tagged object. | Required. Must be a valid URI. | +| `label` | String | Label for the tag. | Required. Trimmed, lowercase. Max length: 20 characters. | +| `created_at` | Integer | Unix timestamp of creation. | Required. | - - The `tag_id` in the URI must be a valid **Hash ID** generated from the `uri` and `label` (see [ID Generation](#id-generation)). +**Validation Notes:** -- **`uri`:** - - - Must be a valid URI. - -- **`label`:** - - Must be trimmed and converted to lowercase. - - Maximum length of **20** characters. +- The `tag_id` is a **Hash ID** derived from the `uri` and `label`. --- @@ -173,53 +177,43 @@ This document intents to be a faithful representation of our [Rust pubky.app mod **URI:** `/pub/pubky.app/bookmarks/:bookmark_id` -**Fields:** - -- `uri` (string, required): The URI that is bookmarked. -- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the bookmark was created. - -**Validation Rules:** +| **Field** | **Type** | **Description** | **Validation Rules** | +| ------------ | -------- | ---------------------- | ------------------------------ | +| `uri` | String | URI of the bookmark. | Required. Must be a valid URI. | +| `created_at` | Integer | Timestamp of creation. | Required. | -- **ID Validation:** +**Validation Notes:** - - The `bookmark_id` in the URI must be a valid **Hash ID** generated from the `uri` (see [ID Generation](#id-generation)). - -- **`uri`:** - - Must be a valid URI. +- The `bookmark_id` is a **Hash ID** derived from the `uri`. --- ### PubkyAppFollow -**Description:** Represents a follow relationship to another user. +**Description:** Represents a follow relationship. **URI:** `/pub/pubky.app/follows/:user_id` -**Fields:** - -- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the follow was created. - -**Validation Rules:** - -- **`created_at`:** - - Should be validated as needed. +| **Field** | **Type** | **Description** | **Validation Rules** | +| ------------ | -------- | ---------------------- | -------------------- | +| `created_at` | Integer | Timestamp of creation. | Required. | --- -### PubkyAppMute - -**Description:** Represents a mute relationship to another user. - -**URI:** `/pub/pubky.app/mutes/:user_id` - -**Fields:** +### PubkyAppFeed -- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the mute was created. +**Description:** Represents a feed configuration. -**Validation Rules:** +**URI:** `/pub/pubky.app/feeds/:feed_id` -- **`created_at`:** - - Should be validated as needed. +| **Field** | **Type** | **Description** | **Validation Rules** | +| --------- | -------- | ----------------------------------------- | ---------------------------------- | +| `tags` | Array | List of tags for filtering. | Optional. Strings must be trimmed. | +| `reach` | String | Feed visibility (e.g., `all`, `friends`). | Required. Must be a valid reach. | +| `layout` | String | Feed layout style (e.g., `columns`). | Required. Must be valid layout. | +| `sort` | String | Sort order (e.g., `recent`). | Required. Must be valid sort. | +| `content` | String | Type of content filtered. | Optional. | +| `name` | String | Name of the feed. | Required. | --- @@ -227,184 +221,12 @@ This document intents to be a faithful representation of our [Rust pubky.app mod ### Common Rules -#### IDs - -- **Timestamp IDs**: IDs generated based on the current timestamp, encoded in Crockford Base32. - - - Must be **13** characters long. - - Decoded ID must represent a valid timestamp after **October 1st, 2024**. - - Timestamp must not be more than **2 hours** in the future. - -- **Hash IDs**: IDs generated by hashing certain fields of the object using Blake3 and encoding in Crockford Base32. - - For `PubkyAppTag`: Hash of `uri:label`. - - For `PubkyAppBookmark`: Hash of `uri`. - - The generated ID must match the provided ID. - -### URL Validation - -- All URLs must be valid according to standard URL parsing rules. - -### String Lengths - -- Fields have maximum lengths as specified in their validation rules. - -### Content Restrictions - -- The content of posts and profiles must not be `[DELETED]`. This keyword is reserved for indicating deleted content. - -### Label Formatting - -- Labels for tags must be: - - Trimmed. - - Converted to lowercase. - - Maximum length of 20 characters. - ---- - -### PubkyAppFeed - -**Description:** Represents a feed configuration, allowing users to customize the content they see based on tags, reach, layout, and sort order. - -**URI:** `/feeds/:feed_id` - -**Fields:** - -- `feed` (object, required): The main configuration object for the feed. - - - `tags` (array of strings, optional): Tags used to filter content within the feed. - - `reach` (string, required): Defines the visibility or scope of the feed. Possible values are: - - `following`: Content from followed users. - - `followers`: Content from follower users. - - `friends`: Content from mutual following users. - - `all`: Public content accessible to everyone. - - `layout` (string, required): Specifies the layout of the feed. Options include: - - `columns`: Organizes feed content in a columnar format. - - `wide`: Arranges content in a standard wide format. - - `visual`: Arranges content in visual format. - - `sort` (string, required): Determines the sorting order of the feed content. Supported values are: - - `recent`: Most recent content first. - - `popularity`: Content with the highest engagement. - - `content` (string, optional): Defines the type of content to filter. Possible values are the same as post kinds: - - `short` - - `long` - - `image` - - `video` - - `link` - - `file` - -- `name` (string, required): The user-defined name for this feed configuration. -- `created_at` (integer, required): Timestamp (Unix epoch in milliseconds) representing when the feed was created. - -**Validation Rules:** - -- **ID Validation:** - - The `feed_id` in the URI is a **Hash ID** generated from the serialized feed object (the JSON object for `feed`), computed using Blake3 and encoded in Crockford Base32. - - The generated `feed_id` must match the provided `feed_id`. - ---- - -### PubkyAppLastRead - -**Description:** Represents the last read timestamp for notifications, used to track when the user last checked for new activity. - -**URI:** `/pub/pubky.app/last_read` - -**Fields:** - -- `timestamp` (integer, required): Unix epoch time in milliseconds of the last time the user checked notifications. - -**Validation Rules:** - -- **`timestamp`:** Must be a valid timestamp in milliseconds. - ---- - -## ID Generation - -### TimestampId - -**Description:** Generates an ID based on the current timestamp. - -**Generation Steps:** - -1. Obtain the current timestamp in microseconds. -2. Convert the timestamp to an 8-byte big-endian representation. -3. Encode the bytes using Crockford Base32 to get a 13-character ID. - -**Validation:** - -- The ID must be **13** characters long. -- Decoded timestamp must represent a date after **October 1st, 2024**. -- The timestamp must not be more than **2 hours** in the future. - -### HashId - -**Description:** Generates an ID based on hashing certain fields of the object. - -**Generation Steps:** - -1. Concatenate the relevant fields (e.g., `uri:label` for tags). -2. Compute the Blake3 hash of the concatenated string. -3. Take the first half of the hash bytes. -4. Encode the bytes using Crockford Base32. - -**Validation:** - -- The generated ID must match the provided ID. +1. **Timestamp IDs:** 13-character Crockford Base32 strings derived from timestamps (in microseconds). +2. **Hash IDs:** First half of the bytes from the resulting Blake3-hashed strings encoded in Crockford Base32. +3. **URLs:** All URLs must pass standard validation. --- -## Examples - -### Example of PubkyAppUser - -```json -{ - "name": "Alice", - "bio": "Blockchain enthusiast and developer.", - "image": "https://example.com/images/alice.png", - "links": [ - { - "title": "GitHub", - "url": "https://github.com/alice" - }, - { - "title": "Website", - "url": "https://alice.dev" - } - ], - "status": "Exploring the decentralized web." -} -``` - -### Example of PubkyAppPost - -```json -{ - "content": "Hello world! This is my first post.", - "kind": "short", - "parent": null, - "embed": null, - "attachments": null -} -``` - -### Example of PubkyAppTag - -```json -{ - "uri": "/pub/pubky.app/posts/00321FCW75ZFY", - "label": "blockchain", - "created_at": 1700000000 -} -``` - -## Notes - -- All timestamps are Unix epoch times in seconds. -- Developers should ensure that all validation rules are enforced to maintain data integrity and interoperability between clients. -- This specification may be updated in future versions to include additional fields or validation rules. - ## License This specification is released under the MIT License. From 4060a233c5907bf18f489c43375ea79879e13d38 Mon Sep 17 00:00:00 2001 From: SHAcollision Date: Fri, 29 Nov 2024 11:29:56 +0100 Subject: [PATCH 13/13] Add example creation of user profile --- Cargo.lock | 2155 ++++++++++++++++++++++++++++++++--- Cargo.toml | 6 + a.txt | 2387 +++++++++++++++++++++++++++++++++++++++ examples/create_user.rs | 101 ++ print_files.sh | 24 + 5 files changed, 4485 insertions(+), 188 deletions(-) create mode 100644 a.txt create mode 100644 examples/create_user.rs create mode 100755 print_files.sh diff --git a/Cargo.lock b/Cargo.lock index eec06dd..867f0d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,49 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "anyhow" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] [[package]] name = "arrayref" @@ -14,12 +57,69 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "base32" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "blake3" version = "1.5.4" @@ -33,6 +133,33 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + [[package]] name = "cc" version = "1.2.1" @@ -48,12 +175,183 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -65,12 +363,89 @@ dependencies = [ "syn", ] +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -81,123 +456,1369 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.15.2" +name = "futures" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] [[package]] -name = "icu_collections" -version = "1.5.0" +name = "futures-channel" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", + "futures-core", + "futures-sink", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "icu_locid_transform" -version = "1.5.0" +name = "futures-io" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "icu_locid_transform_data" -version = "1.5.0" +name = "futures-sink" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] -name = "icu_normalizer" -version = "1.5.0" +name = "futures-task" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", ] [[package]] -name = "icu_normalizer_data" -version = "1.5.0" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.166" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" + +[[package]] +name = "mainline" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b751ffb57303217bcae8f490eee6044a5b40eadf6ca05ff476cad37e7b7970d" +dependencies = [ + "bytes", + "crc", + "ed25519-dalek", + "flume", + "lru", + "rand", + "serde", + "serde_bencode", + "serde_bytes", + "sha1_smol", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkarr" +version = "2.2.1-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b59d10828418841f34089b861b33d966b63ffd34fe770f4bc46df2d8aba118f5" +dependencies = [ + "base32", + "bytes", + "document-features", + "dyn-clone", + "ed25519-dalek", + "flume", + "futures", + "js-sys", + "lru", + "mainline", + "rand", + "self_cell", + "serde", + "simple-dns", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postcard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63d01def49fc815900a83e7a4a5083d2abc81b7ddd569a3fa0477778ae9b3ec" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "pubky" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e41b59ac157d121a1e8c6ed46aafc6666b3ff7c5f41488b7923e1e33d2ca73e" +dependencies = [ + "base64", + "bytes", + "js-sys", + "pkarr", + "pubky-common", + "reqwest", + "thiserror 1.0.69", + "tokio", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "pubky-app-specs" +version = "0.2.1" +dependencies = [ + "anyhow", + "base32", + "blake3", + "pubky", + "pubky-common", + "serde", + "serde_json", + "tokio", + "url", + "utoipa", +] + +[[package]] +name = "pubky-common" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2176f9837b635d3fd4d85d36d5fc06baba85df53ca06f584af9686b85eb55177" +dependencies = [ + "argon2", + "base32", + "blake3", + "crypto_secretbox", + "ed25519-dalek", + "js-sys", + "once_cell", + "pkarr", + "postcard", + "pubky-timestamp", + "rand", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "pubky-timestamp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084b6e5bfcc186781b71257d636b660f20e94bb588c3ba52393fd9faf7a7bfda" +dependencies = [ + "base32", + "document-features", + "getrandom", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.3", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.3", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "cookie", + "cookie_store", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bencode" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" +dependencies = [ + "serde", + "serde_bytes", +] + +[[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_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "simple-dns" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01607fe2e61894468c6dc0b26103abb073fb08b79a3d9e4b6d76a1a341549958" +dependencies = [ + "bitflags", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] -name = "icu_properties" -version = "1.5.1" +name = "tinyvec" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", + "tinyvec_macros", ] [[package]] -name = "icu_properties_data" -version = "1.5.0" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "icu_provider" -version = "1.5.0" +name = "tokio" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "tokio-macros" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -205,231 +1826,362 @@ dependencies = [ ] [[package]] -name = "idna" -version = "1.0.3" +name = "tokio-rustls" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", + "rustls", + "rustls-pki-types", + "tokio", ] [[package]] -name = "idna_adapter" -version = "1.2.0" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "icu_normalizer", - "icu_properties", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "indexmap" -version = "2.6.0" +name = "tracing-attributes" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ - "equivalent", - "hashbrown", - "serde", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "itoa" -version = "1.0.14" +name = "tracing-core" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] [[package]] -name = "litemap" -version = "0.7.4" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "memchr" -version = "2.7.4" +name = "typenum" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "percent-encoding" -version = "2.3.1" +name = "unicode-ident" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] -name = "proc-macro2" -version = "1.0.92" +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "unicode-ident", + "crypto-common", + "subtle", ] [[package]] -name = "pubky-app-specs" -version = "0.2.1" +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ - "base32", - "blake3", + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utoipa" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714" +dependencies = [ + "indexmap", "serde", "serde_json", - "url", - "utoipa", + "utoipa-gen", ] [[package]] -name = "quote" -version = "1.0.37" +name = "utoipa-gen" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66" dependencies = [ "proc-macro2", + "quote", + "syn", ] [[package]] -name = "ryu" -version = "1.0.18" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "serde" -version = "1.0.215" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "serde_derive", + "try-lock", ] [[package]] -name = "serde_derive" -version = "1.0.215" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ + "bumpalo", + "log", + "once_cell", "proc-macro2", "quote", "syn", + "wasm-bindgen-shared", ] [[package]] -name = "serde_json" -version = "1.0.133" +name = "wasm-bindgen-futures" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "shlex" -version = "1.3.0" +name = "wasm-bindgen-macro" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] [[package]] -name = "smallvec" -version = "1.13.2" +name = "wasm-bindgen-macro-support" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "wasm-bindgen-shared" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] -name = "syn" -version = "2.0.89" +name = "web-sys" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "synstructure" -version = "0.13.1" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "tinystr" -version = "0.7.6" +name = "webpki-roots" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ - "displaydoc", - "zerovec", + "rustls-pki-types", ] [[package]] -name = "unicode-ident" -version = "1.0.14" +name = "windows-registry" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] [[package]] -name = "url" -version = "2.5.4" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", + "windows-targets", ] [[package]] -name = "utf16_iter" -version = "1.0.5" +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] [[package]] -name = "utoipa" -version = "5.2.0" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "indexmap", - "serde", - "serde_json", - "utoipa-gen", + "windows-targets", ] [[package]] -name = "utoipa-gen" -version = "5.2.0" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "write16" version = "1.0.0" @@ -466,6 +2218,27 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.5" @@ -487,6 +2260,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zerovec" version = "0.10.4" diff --git a/Cargo.toml b/Cargo.toml index bece68f..d7e9e3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,11 @@ base32 = "0.5.1" blake3 = "1.5.4" utoipa = { version = "5.2.0", optional = true } +[dev-dependencies] +pubky = "0.3.0" +pubky-common = "0.1.0" +anyhow = "1" +tokio = { version = "1.41.1", features = ["full"] } + [features] openapi = ["utoipa"] diff --git a/a.txt b/a.txt new file mode 100644 index 0000000..d1c74fa --- /dev/null +++ b/a.txt @@ -0,0 +1,2387 @@ +./src/feed.rs +``` +use crate::{ + common::timestamp, + traits::{HasPath, HashId, Validatable}, + PubkyAppPostKind, APP_PATH, +}; +use serde::{Deserialize, Serialize}; +use serde_json; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Enum representing the reach of the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub enum PubkyAppFeedReach { + Following, + Followers, + Friends, + All, +} + +/// Enum representing the layout of the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub enum PubkyAppFeedLayout { + Columns, + Wide, + Visual, +} + +/// Enum representing the sort order of the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub enum PubkyAppFeedSort { + Recent, + Popularity, +} + +/// Configuration object for the feed. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppFeedConfig { + pub tags: Option>, + pub reach: PubkyAppFeedReach, + pub layout: PubkyAppFeedLayout, + pub sort: PubkyAppFeedSort, + pub content: Option, +} + +/// Represents a feed configuration. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppFeed { + pub feed: PubkyAppFeedConfig, + pub name: String, + pub created_at: i64, +} + +impl PubkyAppFeed { + /// Creates a new `PubkyAppFeed` instance and sanitizes it. + pub fn new( + tags: Option>, + reach: PubkyAppFeedReach, + layout: PubkyAppFeedLayout, + sort: PubkyAppFeedSort, + content: Option, + name: String, + ) -> Self { + let created_at = timestamp(); + let feed = PubkyAppFeedConfig { + tags, + reach, + layout, + sort, + content, + }; + Self { + feed, + name, + created_at, + } + .sanitize() + } +} + +impl HashId for PubkyAppFeed { + /// Generates an ID based on the serialized `feed` object. + fn get_id_data(&self) -> String { + serde_json::to_string(&self.feed).unwrap_or_default() + } +} + +impl HasPath for PubkyAppFeed { + fn create_path(&self) -> String { + format!("{}feeds/{}", APP_PATH, self.create_id()) + } +} + +impl Validatable for PubkyAppFeed { + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; + + // Validate name + if self.name.trim().is_empty() { + return Err("Validation Error: Feed name cannot be empty".into()); + } + + // Additional validations can be added here + Ok(()) + } + + fn sanitize(self) -> Self { + // Sanitize name + let name = self.name.trim().to_string(); + + // Sanitize tags + let feed = PubkyAppFeedConfig { + tags: self.feed.tags.map(|tags| { + tags.into_iter() + .map(|tag| tag.trim().to_lowercase()) + .collect() + }), + ..self.feed + }; + + PubkyAppFeed { + feed, + name, + created_at: self.created_at, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + Some(PubkyAppPostKind::Image), + "Rust Bitcoiners".to_string(), + ); + + let feed_config = PubkyAppFeedConfig { + tags: Some(vec!["bitcoin".to_string(), "rust".to_string()]), + reach: PubkyAppFeedReach::Following, + layout: PubkyAppFeedLayout::Columns, + sort: PubkyAppFeedSort::Recent, + content: Some(PubkyAppPostKind::Image), + }; + assert_eq!(feed.feed, feed_config); + assert_eq!(feed.name, "Rust Bitcoiners"); + // Check that created_at is recent + let now = timestamp(); + assert!(feed.created_at <= now && feed.created_at >= now - 1_000_000); + } + + #[test] + fn test_create_id() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + "Rust Bitcoiners".to_string(), + ); + + let feed_id = feed.create_id(); + println!("Feed ID: {}", feed_id); + // The ID should not be empty + assert!(!feed_id.is_empty()); + } + + #[test] + fn test_validate() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + "Rust Bitcoiners".to_string(), + ); + let feed_id = feed.create_id(); + + let result = feed.validate(&feed_id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let feed = PubkyAppFeed::new( + Some(vec!["bitcoin".to_string(), "rust".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + "Rust Bitcoiners".to_string(), + ); + let invalid_id = "INVALIDID"; + let result = feed.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_sanitize() { + let feed = PubkyAppFeed::new( + Some(vec![" BiTcoin ".to_string(), " RUST ".to_string()]), + PubkyAppFeedReach::Following, + PubkyAppFeedLayout::Columns, + PubkyAppFeedSort::Recent, + None, + " Rust Bitcoiners".to_string(), + ); + assert_eq!(feed.name, "Rust Bitcoiners"); + assert_eq!( + feed.feed.tags, + Some(vec!["bitcoin".to_string(), "rust".to_string()]) + ); + } + + #[test] + fn test_try_from_valid() { + let feed_json = r#" + { + "feed": { + "tags": ["bitcoin", "rust"], + "reach": "following", + "layout": "columns", + "sort": "recent", + "content": "video" + }, + "name": "My Feed", + "created_at": 1700000000 + } + "#; + + let feed: PubkyAppFeed = serde_json::from_str(feed_json).unwrap(); + let feed_id = feed.create_id(); + + let blob = feed_json.as_bytes(); + let feed_parsed = ::try_from(&blob, &feed_id).unwrap(); + + assert_eq!(feed_parsed.name, "My Feed"); + assert_eq!( + feed_parsed.feed.tags, + Some(vec!["bitcoin".to_string(), "rust".to_string()]) + ); + } +} +``` +./src/user.rs +``` +use crate::{ + traits::{HasPath, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; +use url::Url; + +// Validation constants +const MIN_USERNAME_LENGTH: usize = 3; +const MAX_USERNAME_LENGTH: usize = 50; +const MAX_BIO_LENGTH: usize = 160; +const MAX_IMAGE_LENGTH: usize = 300; +const MAX_LINKS: usize = 5; +const MAX_LINK_TITLE_LENGTH: usize = 100; +const MAX_LINK_URL_LENGTH: usize = 300; +const MAX_STATUS_LENGTH: usize = 50; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// URI: /pub/pubky.app/profile.json +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppUser { + pub name: String, + pub bio: Option, + pub image: Option, + pub links: Option>, + pub status: Option, +} + +/// Represents a user's single link with a title and URL. +#[derive(Serialize, Deserialize, Default, Clone, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppUserLink { + pub title: String, + pub url: String, +} + +impl PubkyAppUser { + /// Creates a new `PubkyAppUser` instance and sanitizes it. + pub fn new( + name: String, + bio: Option, + image: Option, + links: Option>, + status: Option, + ) -> Self { + Self { + name, + bio, + image, + links, + status, + } + .sanitize() + } +} + +impl HasPath for PubkyAppUser { + fn create_path(&self) -> String { + format!("{}profile.json", APP_PATH) + } +} + +impl Validatable for PubkyAppUser { + fn sanitize(self) -> Self { + // Sanitize name + let sanitized_name = self.name.trim(); + // Crop name to a maximum length of MAX_USERNAME_LENGTH characters + let mut name = sanitized_name + .chars() + .take(MAX_USERNAME_LENGTH) + .collect::(); + + // We use username keyword `[DELETED]` for a user whose `profile.json` has been deleted + // Therefore this is not a valid username. + if name == *"[DELETED]" { + name = "anonymous".to_string(); // default username + } + + // Sanitize bio + let bio = self + .bio + .map(|b| b.trim().chars().take(MAX_BIO_LENGTH).collect::()); + + // Sanitize image URL with URL parsing + let image = match &self.image { + Some(image_url) => { + let sanitized_image_url = image_url.trim(); + + match Url::parse(sanitized_image_url) { + Ok(_) => { + // Ensure the URL is within the allowed limit + let url = sanitized_image_url + .chars() + .take(MAX_IMAGE_LENGTH) + .collect::(); + Some(url) // Valid image URL + } + Err(_) => None, // Invalid image URL, set to None + } + } + None => None, + }; + + // Sanitize status + let status = self + .status + .map(|s| s.trim().chars().take(MAX_STATUS_LENGTH).collect::()); + + // Sanitize links + let links = self.links.map(|links_vec| { + links_vec + .into_iter() + .take(MAX_LINKS) + .map(|link| link.sanitize()) + .filter(|link| !link.url.is_empty()) + .collect() + }); + + PubkyAppUser { + name, + bio, + image, + links, + status, + } + } + + fn validate(&self, _id: &str) -> Result<(), String> { + // Validate name length + let name_length = self.name.chars().count(); + if !(MIN_USERNAME_LENGTH..=MAX_USERNAME_LENGTH).contains(&name_length) { + return Err("Validation Error: Invalid name length".into()); + } + + // Validate bio length + if let Some(bio) = &self.bio { + if bio.chars().count() > MAX_BIO_LENGTH { + return Err("Validation Error: Bio exceeds maximum length".into()); + } + } + + // Validate image length + if let Some(image) = &self.image { + if image.chars().count() > MAX_IMAGE_LENGTH { + return Err("Validation Error: Image URI exceeds maximum length".into()); + } + } + + // Validate links + if let Some(links) = &self.links { + if links.len() > MAX_LINKS { + return Err("Too many links".to_string()); + } + + for link in links { + link.validate(_id)?; + } + } + + // Validate status length + if let Some(status) = &self.status { + if status.chars().count() > MAX_STATUS_LENGTH { + return Err("Validation Error: Status exceeds maximum length".into()); + } + } + + Ok(()) + } +} + +impl Validatable for PubkyAppUserLink { + fn sanitize(self) -> Self { + let title = self + .title + .trim() + .chars() + .take(MAX_LINK_TITLE_LENGTH) + .collect::(); + + let url = match Url::parse(self.url.trim()) { + Ok(parsed_url) => { + let sanitized_url = parsed_url.to_string(); + sanitized_url + .chars() + .take(MAX_LINK_URL_LENGTH) + .collect::() + } + Err(_) => "".to_string(), // Default to empty string for invalid URLs + }; + + PubkyAppUserLink { title, url } + } + + fn validate(&self, _id: &str) -> Result<(), String> { + if self.title.chars().count() > MAX_LINK_TITLE_LENGTH { + return Err("Validation Error: Link title exceeds maximum length".to_string()); + } + + if self.url.chars().count() > MAX_LINK_URL_LENGTH { + return Err("Validation Error: Link URL exceeds maximum length".to_string()); + } + + match Url::parse(&self.url) { + Ok(_) => Ok(()), + Err(_) => Err("Validation Error: Invalid URL format".to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + use crate::APP_PATH; + + #[test] + fn test_new() { + let user = PubkyAppUser::new( + "Alice".to_string(), + Some("Maximalist".to_string()), + Some("https://example.com/image.png".to_string()), + Some(vec![ + PubkyAppUserLink { + title: "GitHub".to_string(), + url: "https://github.com/alice".to_string(), + }, + PubkyAppUserLink { + title: "Website".to_string(), + url: "https://alice.dev".to_string(), + }, + ]), + Some("Exploring the decentralized web.".to_string()), + ); + + assert_eq!(user.name, "Alice"); + assert_eq!(user.bio.as_deref(), Some("Maximalist")); + assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); + assert_eq!( + user.status.as_deref(), + Some("Exploring the decentralized web.") + ); + assert!(user.links.is_some()); + assert_eq!(user.links.as_ref().unwrap().len(), 2); + } + + #[test] + fn test_create_path() { + let user = PubkyAppUser::default(); + let path = user.create_path(); + assert_eq!(path, format!("{}profile.json", APP_PATH)); + } + + #[test] + fn test_sanitize() { + let user = PubkyAppUser::new( + " Alice ".to_string(), + Some(" Maximalist and developer. ".to_string()), + Some("https://example.com/image.png".to_string()), + Some(vec![ + PubkyAppUserLink { + title: " GitHub ".to_string(), + url: " https://github.com/alice ".to_string(), + }, + PubkyAppUserLink { + title: "Website".to_string(), + url: "invalid_url".to_string(), // Invalid URL + }, + ]), + Some(" Exploring the decentralized web. ".to_string()), + ); + + assert_eq!(user.name, "Alice"); + assert_eq!(user.bio.as_deref(), Some("Maximalist and developer.")); + assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); + assert_eq!( + user.status.as_deref(), + Some("Exploring the decentralized web.") + ); + assert!(user.links.is_some()); + let links = user.links.unwrap(); + assert_eq!(links.len(), 1); // Invalid URL link should be filtered out + assert_eq!(links[0].title, "GitHub"); + assert_eq!(links[0].url, "https://github.com/alice"); + } + + #[test] + fn test_validate_valid() { + let user = PubkyAppUser::new( + "Alice".to_string(), + Some("Maximalist".to_string()), + Some("https://example.com/image.png".to_string()), + None, + Some("Exploring the decentralized web.".to_string()), + ); + + let result = user.validate(""); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_name() { + let user = PubkyAppUser::new( + "Al".to_string(), // Too short + None, + None, + None, + None, + ); + + let result = user.validate(""); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Validation Error: Invalid name length" + ); + } + + #[test] + fn test_try_from_valid() { + let user_json = r#" + { + "name": "Alice", + "bio": "Maximalist", + "image": "https://example.com/image.png", + "links": [ + { + "title": "GitHub", + "url": "https://github.com/alice" + }, + { + "title": "Website", + "url": "https://alice.dev" + } + ], + "status": "Exploring the decentralized web." + } + "#; + + let blob = user_json.as_bytes(); + let user = ::try_from(&blob, "").unwrap(); + + assert_eq!(user.name, "Alice"); + assert_eq!(user.bio.as_deref(), Some("Maximalist")); + assert_eq!(user.image.as_deref(), Some("https://example.com/image.png")); + assert_eq!( + user.status.as_deref(), + Some("Exploring the decentralized web.") + ); + assert!(user.links.is_some()); + assert_eq!(user.links.as_ref().unwrap().len(), 2); + } + + #[test] + fn test_try_from_invalid_link() { + let user_json = r#" + { + "name": "Alice", + "links": [ + { + "title": "GitHub", + "url": "invalid_url" + } + ] + } + "#; + + let blob = user_json.as_bytes(); + let user = ::try_from(&blob, "").unwrap(); + + // Since the link URL is invalid, it should be filtered out + assert!(user.links.is_none() || user.links.as_ref().unwrap().is_empty()); + } +} +``` +./src/last_read.rs +``` +use crate::{ + common::timestamp, + traits::{HasPath, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents the last read timestamp for notifications. +/// URI: /pub/pubky.app/last_read +#[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppLastRead { + pub timestamp: i64, // Unix epoch time in milliseconds +} + +impl PubkyAppLastRead { + /// Creates a new `PubkyAppLastRead` instance. + pub fn new() -> Self { + let timestamp = timestamp() / 1_000; // to millis + Self { timestamp } + } +} + +impl Validatable for PubkyAppLastRead { + fn validate(&self, _id: &str) -> Result<(), String> { + // Validate timestamp is a positive integer + if self.timestamp <= 0 { + return Err("Validation Error: Timestamp must be a positive integer".into()); + } + Ok(()) + } +} + +impl HasPath for PubkyAppLastRead { + fn create_path(&self) -> String { + format!("{}last_read", APP_PATH) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let last_read = PubkyAppLastRead::new(); + let now = timestamp() / 1_000; + // within 1 second + assert!(last_read.timestamp <= now && last_read.timestamp >= now - 1_000); + } + + #[test] + fn test_create_path() { + let last_read = PubkyAppLastRead::new(); + let path = last_read.create_path(); + assert_eq!(path, format!("{}last_read", APP_PATH)); + } + + #[test] + fn test_validate() { + let last_read = PubkyAppLastRead::new(); + let result = last_read.validate(""); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_timestamp() { + let last_read = PubkyAppLastRead { timestamp: -1 }; + let result = last_read.validate(""); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let last_read_json = r#" + { + "timestamp": 1700000000 + } + "#; + + let blob = last_read_json.as_bytes(); + let last_read = ::try_from(&blob, "").unwrap(); + assert_eq!(last_read.timestamp, 1700000000); + } +} +``` +./src/common.rs +``` +use std::time::{SystemTime, UNIX_EPOCH}; + +pub static VERSION: &str = "0.2.1"; +pub static APP_PATH: &str = "/pub/pubky.app/"; +pub static PROTOCOL: &str = "pubky://"; + +/// Returns the current timestamp in microseconds since the UNIX epoch. +pub fn timestamp() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_micros() as i64 +} +``` +./src/file.rs +``` +use crate::{ + common::timestamp, + traits::{HasPath, TimestampId, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents a file uploaded by the user. +/// URI: /pub/pubky.app/files/:file_id +#[derive(Deserialize, Serialize, Debug, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppFile { + pub name: String, + pub created_at: i64, + pub src: String, + pub content_type: String, + pub size: i64, +} + +impl PubkyAppFile { + /// Creates a new `PubkyAppFile` instance. + pub fn new(name: String, src: String, content_type: String, size: i64) -> Self { + let created_at = timestamp(); + Self { + name, + created_at, + src, + content_type, + size, + } + } +} + +impl TimestampId for PubkyAppFile {} + +impl HasPath for PubkyAppFile { + fn create_path(&self) -> String { + format!("{}files/{}", APP_PATH, self.create_id()) + } +} + +impl Validatable for PubkyAppFile { + // TODO: content_type validation. + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; + // TODO: content_type validation. + // TODO: size and other validation. + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "pubky://user_id/pub/pubky.app/blobs/id".to_string(), + "image/png".to_string(), + 1024, + ); + assert_eq!(file.name, "example.png"); + assert_eq!(file.src, "pubky://user_id/pub/pubky.app/blobs/id"); + assert_eq!(file.content_type, "image/png"); + assert_eq!(file.size, 1024); + // Check that created_at is recent + let now = timestamp(); + assert!(file.created_at <= now && file.created_at >= now - 1_000_000); // within 1 second + } + + #[test] + fn test_create_path() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "pubky://user_id/pub/pubky.app/blobs/id".to_string(), + "image/png".to_string(), + 1024, + ); + let file_id = file.create_id(); + let path = file.create_path(); + + // Check if the path starts with the expected prefix + let prefix = format!("{}files/", APP_PATH); + assert!(path.starts_with(&prefix)); + + let expected_path_len = prefix.len() + file_id.len(); + assert_eq!(path.len(), expected_path_len); + } + + #[test] + fn test_validate_valid() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "/uploads/example.png".to_string(), + "image/png".to_string(), + 1024, + ); + let id = file.create_id(); + let result = file.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let file = PubkyAppFile::new( + "example.png".to_string(), + "/uploads/example.png".to_string(), + "image/png".to_string(), + 1024, + ); + let invalid_id = "INVALIDID"; + let result = file.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let file_json = r#" + { + "name": "example.png", + "created_at": 1627849723, + "src": "/uploads/example.png", + "content_type": "image/png", + "size": 1024 + } + "#; + + let file = PubkyAppFile::new( + "example.png".to_string(), + "/uploads/example.png".to_string(), + "image/png".to_string(), + 1024, + ); + let id = file.create_id(); + + let blob = file_json.as_bytes(); + let file_parsed = ::try_from(&blob, &id).unwrap(); + + assert_eq!(file_parsed.name, "example.png"); + assert_eq!(file_parsed.src, "/uploads/example.png"); + assert_eq!(file_parsed.content_type, "image/png"); + assert_eq!(file_parsed.size, 1024); + } +} +``` +./src/traits.rs +``` +use crate::common::timestamp; +use base32::{decode, encode, Alphabet}; +use blake3::Hasher; +use serde::de::DeserializeOwned; + +pub trait TimestampId { + /// Creates a unique identifier based on the current timestamp. + fn create_id(&self) -> String { + // Get current time in microseconds since UNIX epoch + let now = timestamp(); + + // Convert to big-endian bytes + let bytes = now.to_be_bytes(); + + // Encode the bytes using Base32 with the Crockford alphabet + encode(Alphabet::Crockford, &bytes) + } + + /// Validates that the provided ID is a valid Crockford Base32-encoded timestamp, + /// 13 characters long, and represents a reasonable timestamp. + fn validate_id(&self, id: &str) -> Result<(), String> { + // Ensure ID is 13 characters long + if id.len() != 13 { + return Err("Validation Error: Invalid ID length: must be 13 characters".into()); + } + + // Decode the Crockford Base32-encoded ID + let decoded_bytes = + decode(Alphabet::Crockford, id).ok_or("Failed to decode Crockford Base32 ID")?; + + if decoded_bytes.len() != 8 { + return Err("Validation Error: Invalid ID length after decoding".into()); + } + + // Convert the decoded bytes to a timestamp in microseconds + let timestamp_micros = i64::from_be_bytes(decoded_bytes.try_into().unwrap()); + + // Get current time in microseconds + let now_micros = timestamp(); + + // Define October 1st, 2024, in microseconds since UNIX epoch + let oct_first_2024_micros = 1727740800000000; // Timestamp for 2024-10-01 00:00:00 UTC + + // Allowable future duration (2 hours) in microseconds + let max_future_micros = now_micros + 2 * 60 * 60 * 1_000_000; + + // Validate that the ID's timestamp is after October 1st, 2024 + if timestamp_micros < oct_first_2024_micros { + return Err( + "Validation Error: Invalid ID, timestamp must be after October 1st, 2024".into(), + ); + } + + // Validate that the ID's timestamp is not more than 2 hours in the future + if timestamp_micros > max_future_micros { + return Err("Validation Error: Invalid ID, timestamp is too far in the future".into()); + } + + Ok(()) + } +} + +/// Trait for generating an ID based on the struct's data. +pub trait HashId { + fn get_id_data(&self) -> String; + + /// Creates a unique identifier for bookmarks and tag homeserver paths instance. + /// + /// The ID is generated by: + /// 1. Concatenating the `uri` and `label` fields of the `PubkyAppTag` with a colon (`:`) separator. + /// 2. Hashing the concatenated string using the `blake3` hashing algorithm. + /// 3. Taking the first half of the bytes from the resulting `blake3` hash. + /// 4. Encoding those bytes using the Crockford alphabet (Base32 variant). + /// + /// The resulting Crockford-encoded string is returned as the tag ID. + /// + /// # Returns + /// - A `String` representing the Crockford-encoded tag ID derived from the `blake3` hash of the concatenated `uri` and `label`. + fn create_id(&self) -> String { + let data = self.get_id_data(); + + // Create a Blake3 hash of the input data + let mut hasher = Hasher::new(); + hasher.update(data.as_bytes()); + let blake3_hash = hasher.finalize(); + + // Get the first half of the hash bytes + let half_hash_length = blake3_hash.as_bytes().len() / 2; + let half_hash = &blake3_hash.as_bytes()[..half_hash_length]; + + // Encode the first half of the hash in Base32 using the Z-base32 alphabet + encode(Alphabet::Crockford, half_hash) + } + + /// Validates that the provided ID matches the generated ID. + fn validate_id(&self, id: &str) -> Result<(), String> { + let generated_id = self.create_id(); + if generated_id != id { + return Err(format!("Invalid ID: expected {}, found {}", generated_id, id).into()); + } + Ok(()) + } +} + +pub trait Validatable: Sized + DeserializeOwned { + fn try_from(blob: &[u8], id: &str) -> Result { + let mut instance: Self = serde_json::from_slice(blob).map_err(|e| e.to_string())?; + instance = instance.sanitize(); + instance.validate(id)?; + Ok(instance) + } + + fn validate(&self, id: &str) -> Result<(), String>; + + fn sanitize(self) -> Self { + self + } +} + +pub trait HasPath { + fn create_path(&self) -> String; +} + +pub trait HasPubkyIdPath { + fn create_path(&self, pubky_id: &str) -> String; +} +``` +./src/bookmark.rs +``` +use crate::{ + common::timestamp, + traits::{HasPath, HashId, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents raw homeserver bookmark with id +/// URI: /pub/pubky.app/bookmarks/:bookmark_id +/// +/// Example URI: +/// +/// `/pub/pubky.app/bookmarks/AF7KQ6NEV5XV1EG5DVJ2E74JJ4` +/// +/// Where bookmark_id is Crockford-base32(Blake3("{uri_bookmarked}"")[:half]) +#[derive(Serialize, Deserialize, Default)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppBookmark { + pub uri: String, + pub created_at: i64, +} + +impl PubkyAppBookmark { + /// Creates a new `PubkyAppBookmark` instance. + pub fn new(uri: String) -> Self { + let created_at = timestamp(); + Self { uri, created_at }.sanitize() + } +} + +impl HashId for PubkyAppBookmark { + /// Bookmark ID is created based on the hash of the URI bookmarked + fn get_id_data(&self) -> String { + self.uri.clone() + } +} + +impl HasPath for PubkyAppBookmark { + fn create_path(&self) -> String { + format!("{}bookmarks/{}", APP_PATH, self.create_id()) + } +} + +impl Validatable for PubkyAppBookmark { + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; + // TODO: more bookmarks validation? + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_create_bookmark_id() { + let bookmark = PubkyAppBookmark { + uri: "user_id/pub/pubky.app/posts/post_id".to_string(), + created_at: 1627849723, + }; + + let bookmark_id = bookmark.create_id(); + assert_eq!(bookmark_id, "AF7KQ6NEV5XV1EG5DVJ2E74JJ4"); + } + + #[test] + fn test_create_path() { + let bookmark = PubkyAppBookmark { + uri: "pubky://user_id/pub/pubky.app/posts/post_id".to_string(), + created_at: 1627849723, + }; + let expected_id = bookmark.create_id(); + let expected_path = format!("{}bookmarks/{}", APP_PATH, expected_id); + let path = bookmark.create_path(); + assert_eq!(path, expected_path); + } + + #[test] + fn test_validate_valid() { + let bookmark = + PubkyAppBookmark::new("pubky://user_id/pub/pubky.app/posts/post_id".to_string()); + let id = bookmark.create_id(); + let result = bookmark.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let bookmark = PubkyAppBookmark::new("user_id/pub/pubky.app/posts/post_id".to_string()); + let invalid_id = "INVALIDID"; + let result = bookmark.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let bookmark_json = r#" + { + "uri": "user_id/pub/pubky.app/posts/post_id", + "created_at": 1627849723 + } + "#; + + let uri = "user_id/pub/pubky.app/posts/post_id".to_string(); + let bookmark = PubkyAppBookmark::new(uri.clone()); + let id = bookmark.create_id(); + + let blob = bookmark_json.as_bytes(); + let bookmark_parsed = ::try_from(&blob, &id).unwrap(); + + assert_eq!(bookmark_parsed.uri, uri); + } +} +``` +./src/post.rs +``` +use crate::{ + traits::{HasPath, TimestampId, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use url::Url; + +// Validation +const MAX_SHORT_CONTENT_LENGTH: usize = 1000; +const MAX_LONG_CONTENT_LENGTH: usize = 50000; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents the type of pubky-app posted data +/// Used primarily to best display the content in UI +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] +#[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub enum PubkyAppPostKind { + #[default] + Short, + Long, + Image, + Video, + Link, + File, +} + +impl fmt::Display for PubkyAppPostKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let string_repr = serde_json::to_value(self) + .ok() + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_default(); + write!(f, "{}", string_repr) + } +} + +/// Represents embedded content within a post +#[derive(Serialize, Deserialize, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppPostEmbed { + pub kind: PubkyAppPostKind, // Kind of the embedded content + pub uri: String, // URI of the embedded content +} + +/// Represents raw post in homeserver with content and kind +/// URI: /pub/pubky.app/posts/:post_id +/// Where post_id is CrockfordBase32 encoding of timestamp +/// +/// Example URI: +/// +/// `/pub/pubky.app/posts/00321FCW75ZFY` +#[derive(Serialize, Deserialize, Default, Clone)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppPost { + pub content: String, + pub kind: PubkyAppPostKind, + pub parent: Option, // If a reply, the URI of the parent post. + pub embed: Option, + pub attachments: Option>, +} + +impl PubkyAppPost { + /// Creates a new `PubkyAppPost` instance and sanitizes it. + pub fn new( + content: String, + kind: PubkyAppPostKind, + parent: Option, + embed: Option, + attachments: Option>, + ) -> Self { + let post = PubkyAppPost { + content, + kind, + parent, + embed, + attachments, + }; + post.sanitize() + } +} + +impl TimestampId for PubkyAppPost {} + +impl HasPath for PubkyAppPost { + fn create_path(&self) -> String { + format!("{}posts/{}", APP_PATH, self.create_id()) + } +} + +impl Validatable for PubkyAppPost { + fn sanitize(self) -> Self { + // Sanitize content + let mut content = self.content.trim().to_string(); + + // We are using content keyword `[DELETED]` for deleted posts from a homeserver that still have relationships + // placed by other users (replies, tags, etc). This content is exactly matched by the client to apply effects to deleted content. + // Placing posts with content `[DELETED]` is not allowed. + if content == *"[DELETED]" { + content = "empty".to_string() + } + + // Define content length limits based on PubkyAppPostKind + let max_content_length = match self.kind { + PubkyAppPostKind::Short => MAX_SHORT_CONTENT_LENGTH, + PubkyAppPostKind::Long => MAX_LONG_CONTENT_LENGTH, + _ => MAX_SHORT_CONTENT_LENGTH, // Default limit for other kinds + }; + + let content = content.chars().take(max_content_length).collect::(); + + // Sanitize parent URI if present + let parent = if let Some(uri_str) = &self.parent { + match Url::parse(uri_str) { + Ok(url) => Some(url.to_string()), // Valid URI, use normalized version + Err(_) => None, // Invalid URI, discard or handle appropriately + } + } else { + None + }; + + // Sanitize embed if present + let embed = if let Some(embed) = &self.embed { + match Url::parse(&embed.uri) { + Ok(url) => Some(PubkyAppPostEmbed { + kind: embed.kind.clone(), + uri: url.to_string(), // Use normalized version + }), + Err(_) => None, // Invalid URI, discard or handle appropriately + } + } else { + None + }; + + PubkyAppPost { + content, + kind: self.kind, + parent, + embed, + attachments: self.attachments, + } + } + + fn validate(&self, id: &str) -> Result<(), String> { + self.validate_id(id)?; + + // Validate content length + match self.kind { + PubkyAppPostKind::Short => { + if self.content.chars().count() > MAX_SHORT_CONTENT_LENGTH { + return Err( + "Validation Error: Post content exceeds maximum length for Short kind" + .into(), + ); + } + } + PubkyAppPostKind::Long => { + if self.content.chars().count() > MAX_LONG_CONTENT_LENGTH { + return Err( + "Validation Error: Post content exceeds maximum length for Short kind" + .into(), + ); + } + } + _ => (), + }; + + // TODO: additional validation. Attachement URLs...? + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_create_id() { + let post = PubkyAppPost::new( + "Hello World!".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let post_id = post.create_id(); + println!("Generated Post ID: {}", post_id); + + // Assert that the post ID is 13 characters long + assert_eq!(post_id.len(), 13); + } + + #[test] + fn test_new() { + let content = "This is a test post".to_string(); + let kind = PubkyAppPostKind::Short; + let post = PubkyAppPost::new(content.clone(), kind.clone(), None, None, None); + + assert_eq!(post.content, content); + assert_eq!(post.kind, kind); + assert!(post.parent.is_none()); + assert!(post.embed.is_none()); + assert!(post.attachments.is_none()); + } + + #[test] + fn test_create_path() { + let post = PubkyAppPost::new( + "Test post".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let post_id = post.create_id(); + let path = post.create_path(); + + // Check if the path starts with the expected prefix + let prefix = format!("{}posts/", APP_PATH); + assert!(path.starts_with(&prefix)); + + let expected_path_len = prefix.len() + post_id.len(); + assert_eq!(path.len(), expected_path_len); + } + + #[test] + fn test_sanitize() { + let content = " This is a test post with extra whitespace ".to_string(); + let post = PubkyAppPost::new( + content.clone(), + PubkyAppPostKind::Short, + Some("invalid uri".to_string()), + Some(PubkyAppPostEmbed { + kind: PubkyAppPostKind::Link, + uri: "invalid uri".to_string(), + }), + None, + ); + + let sanitized_post = post.sanitize(); + assert_eq!(sanitized_post.content, content.trim()); + assert!(sanitized_post.parent.is_none()); + assert!(sanitized_post.embed.is_none()); + } + + #[test] + fn test_validate_valid() { + let post = PubkyAppPost::new( + "Valid content".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let id = post.create_id(); + let result = post.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_id() { + let post = PubkyAppPost::new( + "Valid content".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ); + + let invalid_id = "INVALIDID12345"; + let result = post.validate(&invalid_id); + assert!(result.is_err()); + } + + #[test] + fn test_try_from_valid() { + let post_json = r#" + { + "content": "Hello World!", + "kind": "short", + "parent": null, + "embed": null, + "attachments": null + } + "#; + + let id = PubkyAppPost::new( + "Hello World!".to_string(), + PubkyAppPostKind::Short, + None, + None, + None, + ) + .create_id(); + + let blob = post_json.as_bytes(); + let post = ::try_from(&blob, &id).unwrap(); + + assert_eq!(post.content, "Hello World!"); + } + + #[test] + fn test_try_from_invalid_content() { + let content = "[DELETED]".to_string(); + let post_json = format!( + r#"{{ + "content": "{}", + "kind": "short", + "parent": null, + "embed": null, + "attachments": null + }}"#, + content + ); + + let id = PubkyAppPost::new(content.clone(), PubkyAppPostKind::Short, None, None, None) + .create_id(); + + let blob = post_json.as_bytes(); + let post = ::try_from(&blob, &id).unwrap(); + + assert_eq!(post.content, "empty"); // After sanitization + } +} +``` +./src/tag.rs +``` +use crate::{ + common::timestamp, + traits::{HasPath, HashId, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; +use url::Url; + +// Validation +const MAX_TAG_LABEL_LENGTH: usize = 20; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents raw homeserver tag with id +/// URI: /pub/pubky.app/tags/:tag_id +/// +/// Example URI: +/// +/// `/pub/pubky.app/tags/FPB0AM9S93Q3M1GFY1KV09GMQM` +/// +/// Where tag_id is Crockford-base32(Blake3("{uri_tagged}:{label}")[:half]) +#[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppTag { + pub uri: String, + pub label: String, + pub created_at: i64, +} + +impl PubkyAppTag { + pub fn new(uri: String, label: String) -> Self { + let created_at = timestamp(); + Self { + uri, + label, + created_at, + } + .sanitize() + } +} + +impl HasPath for PubkyAppTag { + fn create_path(&self) -> String { + format!("{}tags/{}", APP_PATH, self.create_id()) + } +} + +impl HashId for PubkyAppTag { + /// Tag ID is created based on the hash of the URI tagged and the label used + fn get_id_data(&self) -> String { + format!("{}:{}", self.uri, self.label) + } +} + +impl Validatable for PubkyAppTag { + fn sanitize(self) -> Self { + // Convert label to lowercase and trim + let label = self.label.trim().to_lowercase(); + + // Enforce maximum label length safely + let label = label.chars().take(MAX_TAG_LABEL_LENGTH).collect::(); + + // Sanitize URI + let uri = match Url::parse(&self.uri) { + Ok(url) => { + // If the URL is valid, reformat it to a sanitized string representation + url.to_string() + } + Err(_) => { + // If the URL is invalid, return as-is for error reporting later + self.uri.trim().to_string() + } + }; + + PubkyAppTag { + uri, + label, + created_at: self.created_at, + } + } + + fn validate(&self, id: &str) -> Result<(), String> { + // Validate the tag ID + self.validate_id(id)?; + + // Validate label length + if self.label.chars().count() > MAX_TAG_LABEL_LENGTH { + return Err("Validation Error: Tag label exceeds maximum length".to_string()); + } + + // Validate URI format + match Url::parse(&self.uri) { + Ok(_) => Ok(()), + Err(_) => Err(format!( + "Validation Error: Invalid URI format: {}", + self.uri + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{traits::Validatable, APP_PATH}; + + #[test] + fn test_create_id() { + let tag = PubkyAppTag { + uri: "https://example.com/post/1".to_string(), + created_at: 1627849723000, + label: "cool".to_string(), + }; + + let tag_id = tag.create_id(); + println!("Generated Tag ID: {}", tag_id); + + // Assert that the tag ID is of expected length + // The length depends on your implementation of create_id + assert!(!tag_id.is_empty()); + } + + #[test] + fn test_new() { + let uri = "https://example.com/post/1".to_string(); + let label = "interesting".to_string(); + let tag = PubkyAppTag::new(uri.clone(), label.clone()); + + assert_eq!(tag.uri, uri); + assert_eq!(tag.label, label); + // Check that created_at is recent + let now = timestamp(); + println!("TIMESTAMP {}", tag.created_at); + println!("TIMESTAMP {}", now); + + assert!(tag.created_at <= now && tag.created_at >= now - 1_000_000); // within 1 second + } + + #[test] + fn test_create_path() { + let tag = PubkyAppTag { + uri: "pubky://operrr8wsbpr3ue9d4qj41ge1kcc6r7fdiy6o3ugjrrhi4y77rdo/pub/pubky.app/posts/0032FNCGXE3R0".to_string(), + created_at: 1627849723000, + label: "cool".to_string(), + }; + + let expected_id = tag.create_id(); + let expected_path = format!("{}tags/{}", APP_PATH, expected_id); + let path = tag.create_path(); + + assert_eq!(path, expected_path); + } + + #[test] + fn test_sanitize() { + let tag = PubkyAppTag { + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), + label: " CoOl ".to_string(), + created_at: 1627849723000, + }; + + let sanitized_tag = tag.sanitize(); + assert_eq!(sanitized_tag.label, "cool"); + } + + #[test] + fn test_validate_valid() { + let tag = PubkyAppTag { + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), + label: "cool".to_string(), + created_at: 1627849723000, + }; + + let id = tag.create_id(); + let result = tag.validate(&id); + assert!(result.is_ok()); + } + + #[test] + fn test_validate_invalid_label_length() { + let tag = PubkyAppTag { + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), + label: "a".repeat(MAX_TAG_LABEL_LENGTH + 1), + created_at: 1627849723000, + }; + + let id = tag.create_id(); + let result = tag.validate(&id); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Validation Error: Tag label exceeds maximum length" + ); + } + + #[test] + fn test_validate_invalid_id() { + let tag = PubkyAppTag { + uri: "pubky://user_id/pub/pubky.app/posts/0000000000000".to_string(), + label: "cool".to_string(), + created_at: 1627849723000, + }; + + let invalid_id = "INVALIDID"; + let result = tag.validate(&invalid_id); + assert!(result.is_err()); + // You can check the specific error message if necessary + } + + #[test] + fn test_try_from_valid() { + let tag_json = r#" + { + "uri": "pubky://user_pubky_id/pub/pubky.app/profile.json", + "label": "Cool Tag", + "created_at": 1627849723000 + } + "#; + + let id = PubkyAppTag::new( + "pubky://user_pubky_id/pub/pubky.app/profile.json".to_string(), + "Cool Tag".to_string(), + ) + .create_id(); + + let blob = tag_json.as_bytes(); + let tag = ::try_from(&blob, &id).unwrap(); + assert_eq!(tag.uri, "pubky://user_pubky_id/pub/pubky.app/profile.json"); + assert_eq!(tag.label, "cool tag"); // After sanitization + } + + #[test] + fn test_try_from_invalid_uri() { + let tag_json = r#" + { + "uri": "invalid_uri", + "label": "Cool Tag", + "created_at": 1627849723000 + } + "#; + + let id = "B55PGPFV1E5E0HQ2PB76EQGXPR"; + let blob = tag_json.as_bytes(); + let result = ::try_from(&blob, &id); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Validation Error: Invalid URI format: invalid_uri" + ); + } +} +``` +./src/follow.rs +``` +use crate::{ + common::timestamp, + traits::{HasPubkyIdPath, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents raw homeserver follow object with timestamp +/// +/// On follow objects, the main data is encoded in the path +/// +/// URI: /pub/pubky.app/follows/:user_id +/// +/// Example URI: +/// +/// `/pub/pubky.app/follows/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` +/// +#[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppFollow { + pub created_at: i64, +} + +impl PubkyAppFollow { + /// Creates a new `PubkyAppFollow` instance. + pub fn new() -> Self { + let created_at = timestamp(); + Self { created_at } + } +} + +impl Validatable for PubkyAppFollow { + fn validate(&self, _id: &str) -> Result<(), String> { + // TODO: additional follow validation? E.g., validate `created_at`? + Ok(()) + } +} + +impl HasPubkyIdPath for PubkyAppFollow { + fn create_path(&self, pubky_id: &str) -> String { + format!("{}follows/{}", APP_PATH, pubky_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let follow = PubkyAppFollow::new(); + // Check that created_at is recent + let now = timestamp(); + // within 1 second + assert!(follow.created_at <= now && follow.created_at >= now - 1_000_000); + } + + #[test] + fn test_create_path_with_id() { + let mute = PubkyAppFollow::new(); + let path = mute.create_path("user_id123"); + assert_eq!(path, "/pub/pubky.app/follows/user_id123"); + } + + #[test] + fn test_validate() { + let follow = PubkyAppFollow::new(); + let result = follow.validate("some_user_id"); + assert!(result.is_ok()); + } + + #[test] + fn test_try_from_valid() { + let follow_json = r#" + { + "created_at": 1627849723 + } + "#; + + let blob = follow_json.as_bytes(); + let follow_parsed = + ::try_from(&blob, "some_user_id").unwrap(); + + assert_eq!(follow_parsed.created_at, 1627849723); + } +} +``` +./src/mute.rs +``` +use crate::{ + common::timestamp, + traits::{HasPubkyIdPath, Validatable}, + APP_PATH, +}; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "openapi")] +use utoipa::ToSchema; + +/// Represents raw homeserver Mute object with timestamp +/// URI: /pub/pubky.app/mutes/:user_id +/// +/// Example URI: +/// +/// `/pub/pubky.app/mutes/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy` +/// +#[derive(Serialize, Deserialize, Default, Debug)] +#[cfg_attr(feature = "openapi", derive(ToSchema))] +pub struct PubkyAppMute { + pub created_at: i64, +} + +impl PubkyAppMute { + /// Creates a new `PubkyAppMute` instance. + pub fn new() -> Self { + let created_at = timestamp(); + Self { created_at } + } +} + +impl Validatable for PubkyAppMute { + fn validate(&self, _id: &str) -> Result<(), String> { + // TODO: additional Mute validation? E.g., validate `created_at` ? + Ok(()) + } +} + +impl HasPubkyIdPath for PubkyAppMute { + fn create_path(&self, pubky_id: &str) -> String { + format!("{}mutes/{}", APP_PATH, pubky_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::timestamp; + use crate::traits::Validatable; + + #[test] + fn test_new() { + let mute = PubkyAppMute::new(); + // Check that created_at is recent + let now = timestamp(); + assert!(mute.created_at <= now && mute.created_at >= now - 1_000_000); + // within 1 second + } + + #[test] + fn test_create_path_with_id() { + let mute = PubkyAppMute::new(); + let path = mute.create_path("user_id123"); + assert_eq!(path, "/pub/pubky.app/mutes/user_id123"); + } + + #[test] + fn test_validate() { + let mute = PubkyAppMute::new(); + let result = mute.validate("some_user_id"); + assert!(result.is_ok()); + } + + #[test] + fn test_try_from_valid() { + let mute_json = r#" + { + "created_at": 1627849723 + } + "#; + + let blob = mute_json.as_bytes(); + let mute_parsed = ::try_from(&blob, "some_user_id").unwrap(); + + assert_eq!(mute_parsed.created_at, 1627849723); + } +} +``` +./src/lib.rs +``` +mod bookmark; +mod common; +mod feed; +mod file; +mod follow; +mod last_read; +mod mute; +mod post; +mod tag; +pub mod traits; +mod user; + +pub use bookmark::PubkyAppBookmark; +pub use common::{APP_PATH, PROTOCOL, VERSION}; +pub use feed::{PubkyAppFeed, PubkyAppFeedLayout, PubkyAppFeedReach, PubkyAppFeedSort}; +pub use file::PubkyAppFile; +pub use follow::PubkyAppFollow; +pub use last_read::PubkyAppLastRead; +pub use mute::PubkyAppMute; +pub use post::{PubkyAppPost, PubkyAppPostEmbed, PubkyAppPostKind}; +pub use tag::PubkyAppTag; +pub use user::{PubkyAppUser, PubkyAppUserLink}; +``` +./README.md +``` +# Pubky.app Data Model Specification + +_Version 0.2.0_ + +## Introduction + +This document specifies the data models and validation rules for the Pubky.app client and homeserver interactions. It defines the structures of data entities, their properties, and the validation rules to ensure data integrity and consistency. This specification is intended for developers who wish to implement their own libraries or clients compatible with Pubky.app. + +This document intents to be a faithful representation of our [Rust pubky.app models](https://github.com/pubky/pubky-app-specs/tree/main/src). If you intend to develop in Rust, use them directly. In case of disagreement between this document and the Rust implementation, the Rust implementation prevails. + +## Data Models + +### PubkyAppUser + +**Description:** Represents a user's profile information. + +**URI:** `/pub/pubky.app/profile.json` + +**Fields:** + +- `name` (string, required): The user's name. +- `bio` (string, optional): A short biography. +- `image` (string, optional): A URL to the user's profile image. +- `links` (array of `UserLink`, optional): A list of links associated with the user. +- `status` (string, optional): The user's current status. + +**`UserLink` Object:** + +- `title` (string, required): The title of the link. +- `url` (string, required): The URL of the link. + +**Validation Rules:** + +- **`name`:** + + - Must be at least **3** and at most **50** characters. + - Cannot be the keyword `[DELETED]`; this is reserved for deleted profiles. + +- **`bio`:** + + - Maximum length of **160** characters if provided. + +- **`image`:** + + - If provided, must be a valid URL. + - Maximum length of **300** characters. + +- **`links`:** + + - Maximum of **5** links. + - Each `UserLink` must have: + - `title`: Maximum length of **100** characters. + - `url`: Must be a valid URL, maximum length of **300** characters. + +- **`status`:** + - Maximum length of **50** characters if provided. + +--- + +### PubkyAppFile + +**Description:** Represents a file uploaded by the user. + +**URI:** `/pub/pubky.app/files/:file_id` + +**Fields:** + +- `name` (string, required): The name of the file. +- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the file was created. +- `src` (string, required): The source URL or path of the file. +- `content_type` (string, required): The MIME type of the file. +- `size` (integer, required): The size of the file in bytes. + +**Validation Rules:** + +- **ID Validation:** + + - The `file_id` in the URI must be a valid **Timestamp ID** (see [ID Generation](#id-generation)). + +- **Additional Validation:** + - Validation for `content_type`, `size`, and other fields should be implemented as needed. + +--- + +### PubkyAppPost + +**Description:** Represents a user's post. + +**URI:** `/pub/pubky.app/posts/:post_id` + +**Fields:** + +- `content` (string, required): The content of the post. +- `kind` (string, required): The type of post. Possible values are: + + - `Short` + - `Long` + - `Image` + - `Video` + - `Link` + - `File` + +- `parent` (string, optional): URI of the parent post if this is a reply. +- `embed` (object, optional): Embedded content. +- `attachments` (array of strings, optional): A list of attachment URIs. + +**`embed` Object:** + +- `kind` (string, required): Type of the embedded content. Same as `kind` in `PubkyAppPost`. +- `uri` (string, required): URI of the embedded content. + +**Validation Rules:** + +- **ID Validation:** + + - The `post_id` in the URI must be a valid **Timestamp ID** (see [ID Generation](#id-generation)). + +- **`content`:** + + - Must not be the keyword `[DELETED]`; this is reserved for deleted posts. + - **For `kind` of `Short`:** + - Maximum length of **1000** characters. + - **For `kind` of `Long`:** + - Maximum length of **50000** characters. + - **For other `kind` values:** + - Maximum length of **1000** characters. + +- **`parent`:** + + - If provided, must be a valid URI. + +- **`embed`:** + + - If provided: + - `uri` must be a valid URI. + +- **Additional Validation:** + - Validation for `attachments` and other fields should be implemented as needed. + +--- + +### PubkyAppTag + +**Description:** Represents a tag applied to a URI. + +**URI:** `/pub/pubky.app/tags/:tag_id` + +**Fields:** + +- `uri` (string, required): The URI that is tagged. +- `label` (string, required): The tag label. +- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the tag was created. + +**Validation Rules:** + +- **ID Validation:** + + - The `tag_id` in the URI must be a valid **Hash ID** generated from the `uri` and `label` (see [ID Generation](#id-generation)). + +- **`uri`:** + + - Must be a valid URI. + +- **`label`:** + - Must be trimmed and converted to lowercase. + - Maximum length of **20** characters. + +--- + +### PubkyAppBookmark + +**Description:** Represents a bookmark to a URI. + +**URI:** `/pub/pubky.app/bookmarks/:bookmark_id` + +**Fields:** + +- `uri` (string, required): The URI that is bookmarked. +- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the bookmark was created. + +**Validation Rules:** + +- **ID Validation:** + + - The `bookmark_id` in the URI must be a valid **Hash ID** generated from the `uri` (see [ID Generation](#id-generation)). + +- **`uri`:** + - Must be a valid URI. + +--- + +### PubkyAppFollow + +**Description:** Represents a follow relationship to another user. + +**URI:** `/pub/pubky.app/follows/:user_id` + +**Fields:** + +- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the follow was created. + +**Validation Rules:** + +- **`created_at`:** + - Should be validated as needed. + +--- + +### PubkyAppMute + +**Description:** Represents a mute relationship to another user. + +**URI:** `/pub/pubky.app/mutes/:user_id` + +**Fields:** + +- `created_at` (integer, required): Timestamp (Unix epoch in seconds) of when the mute was created. + +**Validation Rules:** + +- **`created_at`:** + - Should be validated as needed. + +--- + +## Validation Rules + +### Common Rules + +#### IDs + +- **Timestamp IDs**: IDs generated based on the current timestamp, encoded in Crockford Base32. + + - Must be **13** characters long. + - Decoded ID must represent a valid timestamp after **October 1st, 2024**. + - Timestamp must not be more than **2 hours** in the future. + +- **Hash IDs**: IDs generated by hashing certain fields of the object using Blake3 and encoding in Crockford Base32. + - For `PubkyAppTag`: Hash of `uri:label`. + - For `PubkyAppBookmark`: Hash of `uri`. + - The generated ID must match the provided ID. + +### URL Validation + +- All URLs must be valid according to standard URL parsing rules. + +### String Lengths + +- Fields have maximum lengths as specified in their validation rules. + +### Content Restrictions + +- The content of posts and profiles must not be `[DELETED]`. This keyword is reserved for indicating deleted content. + +### Label Formatting + +- Labels for tags must be: + - Trimmed. + - Converted to lowercase. + - Maximum length of 20 characters. + +--- + +### PubkyAppFeed + +**Description:** Represents a feed configuration, allowing users to customize the content they see based on tags, reach, layout, and sort order. + +**URI:** `/feeds/:feed_id` + +**Fields:** + +- `feed` (object, required): The main configuration object for the feed. + + - `tags` (array of strings, optional): Tags used to filter content within the feed. + - `reach` (string, required): Defines the visibility or scope of the feed. Possible values are: + - `following`: Content from followed users. + - `followers`: Content from follower users. + - `friends`: Content from mutual following users. + - `all`: Public content accessible to everyone. + - `layout` (string, required): Specifies the layout of the feed. Options include: + - `columns`: Organizes feed content in a columnar format. + - `wide`: Arranges content in a standard wide format. + - `visual`: Arranges content in visual format. + - `sort` (string, required): Determines the sorting order of the feed content. Supported values are: + - `recent`: Most recent content first. + - `popularity`: Content with the highest engagement. + - `content` (string, optional): Defines the type of content to filter. Possible values are the same as post kinds: + - `short` + - `long` + - `image` + - `video` + - `link` + - `file` + +- `name` (string, required): The user-defined name for this feed configuration. +- `created_at` (integer, required): Timestamp (Unix epoch in milliseconds) representing when the feed was created. + +**Validation Rules:** + +- **ID Validation:** + - The `feed_id` in the URI is a **Hash ID** generated from the serialized feed object (the JSON object for `feed`), computed using Blake3 and encoded in Crockford Base32. + - The generated `feed_id` must match the provided `feed_id`. + +--- + +### PubkyAppLastRead + +**Description:** Represents the last read timestamp for notifications, used to track when the user last checked for new activity. + +**URI:** `/pub/pubky.app/last_read` + +**Fields:** + +- `timestamp` (integer, required): Unix epoch time in milliseconds of the last time the user checked notifications. + +**Validation Rules:** + +- **`timestamp`:** Must be a valid timestamp in milliseconds. + +--- + +## ID Generation + +### TimestampId + +**Description:** Generates an ID based on the current timestamp. + +**Generation Steps:** + +1. Obtain the current timestamp in microseconds. +2. Convert the timestamp to an 8-byte big-endian representation. +3. Encode the bytes using Crockford Base32 to get a 13-character ID. + +**Validation:** + +- The ID must be **13** characters long. +- Decoded timestamp must represent a date after **October 1st, 2024**. +- The timestamp must not be more than **2 hours** in the future. + +### HashId + +**Description:** Generates an ID based on hashing certain fields of the object. + +**Generation Steps:** + +1. Concatenate the relevant fields (e.g., `uri:label` for tags). +2. Compute the Blake3 hash of the concatenated string. +3. Take the first half of the hash bytes. +4. Encode the bytes using Crockford Base32. + +**Validation:** + +- The generated ID must match the provided ID. + +--- + +## Examples + +### Example of PubkyAppUser + +```json +{ + "name": "Alice", + "bio": "Blockchain enthusiast and developer.", + "image": "https://example.com/images/alice.png", + "links": [ + { + "title": "GitHub", + "url": "https://github.com/alice" + }, + { + "title": "Website", + "url": "https://alice.dev" + } + ], + "status": "Exploring the decentralized web." +} +``` + +### Example of PubkyAppPost + +```json +{ + "content": "Hello world! This is my first post.", + "kind": "short", + "parent": null, + "embed": null, + "attachments": null +} +``` + +### Example of PubkyAppTag + +```json +{ + "uri": "/pub/pubky.app/posts/00321FCW75ZFY", + "label": "blockchain", + "created_at": 1700000000 +} +``` + +## Notes + +- All timestamps are Unix epoch times in seconds. +- Developers should ensure that all validation rules are enforced to maintain data integrity and interoperability between clients. +- This specification may be updated in future versions to include additional fields or validation rules. + +## License + +This specification is released under the MIT License. +``` +./Cargo.toml +``` +[package] +name = "pubky-app-specs" +version = "0.2.1" +edition = "2021" +description = "Pubky.app Data Model Specifications" +homepage = "https://pubky.app" +repository = "https://github.com/pubky/pubky-app-specs" +license = "MIT" +documentation = "https://github.com/pubky/pubky-app-specs" + +[dependencies] +serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.133" +url = "2.5.4" +base32 = "0.5.1" +blake3 = "1.5.4" +utoipa = { version = "5.2.0", optional = true } + +[features] +openapi = ["utoipa"] +``` diff --git a/examples/create_user.rs b/examples/create_user.rs new file mode 100644 index 0000000..8e24fcd --- /dev/null +++ b/examples/create_user.rs @@ -0,0 +1,101 @@ +/// cargo run --example create_user +use anyhow::Result; +use pubky::PubkyClient; +use pubky_app_specs::{ + traits::{HasPath, Validatable}, + PubkyAppUser, PROTOCOL, +}; +use pubky_common::crypto::{Keypair, PublicKey}; +use serde_json::to_vec; + +// Replace this with your actual homeserver public key +const HOMESERVER: &str = "ufibwbmed6jeq9k4p583go95wofakh9fwpp4k734trq79pd9u1uy"; + +#[tokio::main] +async fn main() -> Result<()> { + // Print an introduction for the developer + println!("Welcome to the Pubky User Creator Example!"); + + // Step 1: Initialize the Pubky client + println!("\nStep 1: Initializing the Pubky client..."); + + let client = PubkyClient::default(); + let homeserver = PublicKey::try_from(HOMESERVER).expect("Invalid homeserver public key."); + + println!("Pubky client initialized successfully."); + + // Step 2: Generate a keypair for the new user + println!("\nStep 2: Generating a random keypair for the new user..."); + + let keypair = Keypair::random(); + let user_id = keypair.public_key().to_z32(); + + println!("Generated keypair with User ID: {}", user_id); + + // Step 3: Sign up a new identity on the homeserver + println!("\nStep 3: Signing up the new identity on the homeserver..."); + + client + .signup(&keypair, &homeserver) + .await + .expect("Failed to sign up the user on the homeserver."); + + println!("User signed up successfully!"); + + // Step 4: Create a new user profile + println!("\nStep 4: Creating a new user profile..."); + + let user_profile = PubkyAppUser::new( + "Test User".to_string(), // User display name + None, // Optional fields set to None + None, + None, + None, + ); + + println!("User profile created: {:?}", user_profile); + + // Step 5: Write the user profile to the homeserver + println!("\nStep 5: Writing the user profile to the homeserver..."); + + let url = format!( + "{protocol}{pubky_id}{path}", + protocol = PROTOCOL, + pubky_id = user_id, + path = user_profile.create_path() + ); + let content = to_vec(&user_profile)?; + + client + .put(url.as_str(), &content) + .await + .expect("Failed to write the user profile to the homeserver."); + + println!( + "User profile written successfully to:\nURL: {}\nContent: {}", + url, + String::from_utf8_lossy(&content) + ); + + // Step 6: Retrieve the user profile from the homeserver + println!("\nStep 6: Retrieving the user profile from the homeserver..."); + + let retrieved_content = client + .get(url.as_str()) + .await + .expect("Failed to retrieve the user profile from the homeserver.") + .unwrap(); + + let retrieved_profile = ::try_from(&retrieved_content, "") + .expect("Failed to deserialize the retrieved user profile."); + + println!( + "User profile retrieved successfully:\n{}", + serde_json::to_string_pretty(&retrieved_profile).unwrap() + ); + + // Final message to indicate completion + println!("\nAll steps completed successfully! The new user is now registered and their profile is stored on the homeserver."); + + Ok(()) +} diff --git a/print_files.sh b/print_files.sh new file mode 100755 index 0000000..480b372 --- /dev/null +++ b/print_files.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Array of directories to skip +skip_dirs=(./target ./benches ./examples ./pubky) + +# Build the find command with exclusion patterns +find_cmd="find ." + +for dir in "${skip_dirs[@]}"; do + find_cmd+=" -path $dir -prune -o" +done + +# Add the file types to include and the actions to perform +find_cmd+=" \( -name '*.rs' -o -name '*.toml' -o -name '*.md' \) -print" + +# Execute the constructed find command +eval $find_cmd | while read -r file; do + # Print the path to the file + echo "$file" + echo '```' + # Print the content of the file + cat "$file" + echo '```' +done