Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PubkyApp schemas #90

Merged
merged 5 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ chrono = "0.4.38"
pkarr = { version = "2.2.0", features = ["async"], default-features = false }
pubky = { path = "pubky/pubky" }
reqwest = "0.12.7"
base32 = "0.5.1"
blake3 = "1.5.4"

[dev-dependencies]
anyhow = "1.0.86"
Expand Down
4 changes: 2 additions & 2 deletions benches/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use criterion::{criterion_group, criterion_main, Criterion};
use pkarr::{mainline::Testnet, Keypair};
use pubky::PubkyClient;
use pubky_homeserver::Homeserver;
use pubky_nexus::models::homeserver::{HomeserverUser, UserLink};
use pubky_nexus::models::pubky_app::{PubkyAppUser, UserLink};
use pubky_nexus::EventProcessor;
use setup::run_setup;
use std::time::Duration;
Expand Down Expand Up @@ -32,7 +32,7 @@ async fn create_homeserver_with_events() -> (Testnet, String) {
.await
.unwrap();

let user = HomeserverUser {
let user = PubkyAppUser {
bio: Some("This is an example bio".to_string()),
image: Some("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjiO4O+w8ABL0CPPcYQa4AAAAASUVORK5CYII=".to_string()),
links: Some(vec![UserLink {
Expand Down
4 changes: 2 additions & 2 deletions examples/populate_homeserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use log::info;
use pkarr::{mainline::Testnet, Keypair, PublicKey};
use pubky::PubkyClient;
use pubky_nexus::{
models::homeserver::{HomeserverUser, UserLink},
models::pubky_app::{PubkyAppUser, UserLink},
setup, Config,
};

Expand Down Expand Up @@ -36,7 +36,7 @@ async fn main() -> Result<()> {
client.signup(&keypair, &homeserver).await?;

// Create a new profile
let user = HomeserverUser {
let user = PubkyAppUser {
bio: Some("This is an example bio".to_string()),
image: Some("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjiO4O+w8ABL0CPPcYQa4AAAAASUVORK5CYII=".to_string()),
links: Some(vec![UserLink {
Expand Down
4 changes: 2 additions & 2 deletions src/events/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::models::{
homeserver::HomeserverUser,
pubky_app::PubkyAppUser,
traits::Collection,
user::{UserCounts, UserDetails},
};
Expand Down Expand Up @@ -125,7 +125,7 @@ impl Event {
debug!("Processing User resource at {}", self.uri.path);

// Serialize and validate
let user = HomeserverUser::try_from(&blob).await?;
let user = PubkyAppUser::try_from(&blob).await?;

// Create UserDetails object
let user_details = match self.get_user_id() {
Expand Down
4 changes: 0 additions & 4 deletions src/models/homeserver/mod.rs

This file was deleted.

2 changes: 1 addition & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub mod homeserver;
pub mod info;
pub mod post;
pub mod pubky_app;
pub mod tag;
pub mod traits;
pub mod user;
17 changes: 2 additions & 15 deletions src/models/post/details.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
use super::PostStream;
use crate::db::connectors::neo4j::get_neo4j_graph;
use crate::models::pubky_app::PostKind;
use crate::{queries, RedisOps};
use neo4rs::Node;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use super::PostStream;

/// Represents the type of pubky-app posted data
/// Used primarily to best display the content in UI
#[derive(Serialize, Deserialize, ToSchema, Default)]
pub enum PostKind {
#[default]
Short,
Long,
Image,
Video,
Link,
File,
}

/// Represents post data with content, bio, image, links, and status.
#[derive(Serialize, Deserialize, ToSchema, Default)]
pub struct PostDetails {
Expand Down
2 changes: 1 addition & 1 deletion src/models/post/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ mod view;

pub use bookmark::Bookmark;
pub use counts::PostCounts;
pub use details::{PostDetails, PostKind};
pub use details::PostDetails;
pub use relationships::PostRelationships;
pub use stream::{PostStream, PostStreamReach, PostStreamSorting};
pub use thread::PostThread;
Expand Down
33 changes: 33 additions & 0 deletions src/models/pubky_app/bookmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use super::traits::GenerateId;
use serde::{Deserialize, Serialize};

/// Represents raw homeserver bookmark with id
/// URI: /pub/pubky.app/bookmarks/:bookmark_id
///
/// Example URI:
///
/// `/pub/pubky.app/bookmarks/kx8uzgiq5f75bqofp51nq8r11r`
///
#[derive(Serialize, Deserialize, Default)]
pub struct PubkyAppBookmark {
pub uri: String,
pub created_at: i64,
}

impl GenerateId for PubkyAppBookmark {
/// Bookmark ID is created based on the hash of the URI bookmarked
fn get_id_data(&self) -> String {
self.uri.clone()
}
}

#[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);
}
13 changes: 13 additions & 0 deletions src/models/pubky_app/follow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};

/// Represents raw homeserver follow object with timestamp
/// URI: /pub/pubky.app/follows/:user_id
///
/// Example URI:
///
/// `/pub/pubky.app/follows/pxnu33x7jtpx9ar1ytsi4yxbp6a5o36gwhffs8zoxmbuptici1jy``
///
#[derive(Serialize, Deserialize, Default)]
pub struct PubkyAppFollow {
pub created_at: i64,
}
13 changes: 13 additions & 0 deletions src/models/pubky_app/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Raw Pubky.App schemas as stored on homeserver.
mod bookmark;
mod follow;
mod post;
mod tag;
pub mod traits;
mod user;

pub use bookmark::PubkyAppBookmark;
pub use follow::PubkyAppFollow;
pub use post::{PostKind, PubkyAppPost};
pub use tag::PubkyAppTag;
pub use user::{PubkyAppUser, UserLink};
32 changes: 32 additions & 0 deletions src/models/pubky_app/post.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

/// Represents the type of pubky-app posted data
/// Used primarily to best display the content in UI
#[derive(Serialize, Deserialize, ToSchema, Default)]
pub enum PostKind {
#[default]
Short,
Long,
Image,
Video,
Link,
File,
}

/// Used primarily to best display the content in UI
#[derive(Serialize, Deserialize, Default)]
pub struct PostEmbed {
pub r#type: String, //e.g., "post", we have to define a type for this.
pub uri: String,
}

/// Represents raw post in homeserver with content and kind
/// URI: /pub/pubky.app/posts/:post_id
/// Where post_id is CrockfordBase32 encoding of timestamp
#[derive(Serialize, Deserialize, Default)]
pub struct PubkyAppPost {
pub content: String,
pub kind: PostKind,
pub embed: PostEmbed,
}
71 changes: 71 additions & 0 deletions src/models/pubky_app/tag.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use super::traits::GenerateId;
use base32::{encode, Alphabet};
use blake3::Hasher;
use serde::{Deserialize, Serialize};

/// Represents raw homeserver tag with id
/// URI: /pub/pubky.app/tags/:tag_id
///
/// Example URI:
///
/// `/pub/pubky.app/tags/xsmykwj3jdzdwbox6bu5yjowzw`
///
/// Where tag_id is z-base32(Sha256("{uri_tagged}:{")))[:8]
#[derive(Serialize, Deserialize, Default)]
pub struct PubkyAppTag {
pub uri: String,
pub label: String,
pub created_at: i64,
}

impl GenerateId 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 PubkyAppTag {
/// Creates a unique identifier (tag ID) for the `PubkyAppTag` instance.
///
/// The tag 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 Z-base32 alphabet (Base32 variant).
///
/// The resulting Base32-encoded string is returned as the tag ID.
///
/// # Returns
/// - A `String` representing the Base32-encoded tag ID derived from the `blake3` hash of the concatenated `uri` and `label`.
pub fn create_id(&self) -> String {
// Concatenate the URI and label with a colon in between
let data = format!("{}:{}", self.uri, self.label);

// Create a Blake3 hash of the concatenated string
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

// Return the Base32 encoded string as the tag ID
encode(Alphabet::Z, half_hash)
}
}

#[test]
fn testcreate_id() {
let tag = PubkyAppTag {
uri: "user_id/pub/pubky.app/posts/post_id".to_string(),
created_at: 1627849723,
label: "cool".to_string(),
};

let tag_id = tag.create_id();
println!("Generated Tag ID: {}", tag_id);
}
35 changes: 35 additions & 0 deletions src/models/pubky_app/traits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use base32::{encode, Alphabet};
use blake3::Hasher;

/// Trait for generating an ID based on the struct's data.
pub trait GenerateId {
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 Z-base32 alphabet (Base32 variant).
///
/// The resulting Base32-encoded string is returned as the tag ID.
///
/// # Returns
/// - A `String` representing the Base32-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::Z, half_hash)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

/// Profile schema
/// URI: /pub/pubky.app/profile.json
#[derive(Deserialize, Serialize, Debug)]
pub struct HomeserverUser {
pub struct PubkyAppUser {
pub name: String,
pub bio: Option<String>,
pub image: Option<String>,
Expand All @@ -19,7 +20,7 @@ pub struct UserLink {
pub url: String,
}

impl HomeserverUser {
impl PubkyAppUser {
pub async fn try_from(blob: &Bytes) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let user: Self = serde_json::from_slice(blob)?;
user.validate().await?;
Expand Down
4 changes: 2 additions & 2 deletions src/models/user/details.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::UserSearch;
use crate::db::graph::exec::exec_single_row;
use crate::models::homeserver::{HomeserverUser, UserLink};
use crate::models::pubky_app::{PubkyAppUser, UserLink};
use crate::models::traits::Collection;
use crate::{queries, RedisOps};
use axum::async_trait;
Expand Down Expand Up @@ -67,7 +67,7 @@ impl UserDetails {

pub async fn from_homeserver(
user_id: &str,
homeserver_user: HomeserverUser,
homeserver_user: PubkyAppUser,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
// Validate user_id is a valid pkarr public key
PublicKey::try_from(user_id)?;
Expand Down
3 changes: 2 additions & 1 deletion src/routes/v0/post/details.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::models::post::{PostDetails, PostKind};
use crate::models::post::PostDetails;
use crate::models::pubky_app::PostKind;
use crate::routes::v0::endpoints::POST_DETAILS_ROUTE;
use crate::{Error, Result};
use axum::extract::Path;
Expand Down
2 changes: 1 addition & 1 deletion src/routes/v0/user/details.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::models::homeserver::UserLink;
use crate::models::pubky_app::UserLink;
use crate::models::user::UserDetails;
use crate::routes::v0::endpoints::USER_DETAILS_ROUTE;
use crate::{Error, Result};
Expand Down
4 changes: 2 additions & 2 deletions tests/watcher/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use pubky::PubkyClient;
use pubky_homeserver::Homeserver;
use pubky_nexus::{
models::{
homeserver::{HomeserverUser, UserLink},
pubky_app::{PubkyAppUser, UserLink},
user::UserView,
},
setup, Config, EventProcessor,
Expand Down Expand Up @@ -34,7 +34,7 @@ async fn test_homeserver_user() -> Result<()> {
.unwrap();

// Create a user sticking to the homeserver schema for pubky-app profiles
let user = HomeserverUser {
let user = PubkyAppUser {
bio: Some("This is an example bio".to_string()),
image: Some("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdjiO4O+w8ABL0CPPcYQa4AAAAASUVORK5CYII=".to_string()),
links: Some(vec![UserLink {
Expand Down
Loading