Skip to content

Commit

Permalink
Improve pubky app post
Browse files Browse the repository at this point in the history
  • Loading branch information
SHAcollision committed Nov 27, 2024
1 parent 65083d0 commit e1087a1
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 27 deletions.
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod post;
mod tag;
pub mod traits;
mod user;
mod version;

pub use bookmark::PubkyAppBookmark;
pub use file::PubkyAppFile;
Expand All @@ -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};
195 changes: 189 additions & 6 deletions src/post.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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]
Expand All @@ -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
Expand All @@ -55,8 +58,34 @@ pub struct PubkyAppPost {
attachments: Option<Vec<String>>,
}

impl PubkyAppPost {
/// Creates a new `PubkyAppPost` instance and sanitizes it.
pub fn new(
content: String,
kind: PubkyAppPostKind,
parent: Option<String>,
embed: Option<PubkyAppPostEmbed>,
attachments: Option<Vec<String>>,
) -> 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
Expand Down Expand Up @@ -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 = <PubkyAppPost as Validatable>::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 = <PubkyAppPost as Validatable>::try_from(&blob, &id).unwrap();

assert_eq!(post.content, "empty"); // After sanitization
}
}
38 changes: 20 additions & 18 deletions src/tag.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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 {
Expand All @@ -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())
}
}

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -133,20 +136,22 @@ 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
}

#[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);
Expand All @@ -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,
};
Expand All @@ -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,
};
Expand All @@ -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,
};
Expand All @@ -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,
};
Expand All @@ -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 = <PubkyAppTag as Validatable>::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
}

Expand Down
Loading

0 comments on commit e1087a1

Please sign in to comment.