./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<Vec<String>>,
    pub reach: PubkyAppFeedReach,
    pub layout: PubkyAppFeedLayout,
    pub sort: PubkyAppFeedSort,
    pub content: Option<PubkyAppPostKind>,
}

/// 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<Vec<String>>,
        reach: PubkyAppFeedReach,
        layout: PubkyAppFeedLayout,
        sort: PubkyAppFeedSort,
        content: Option<PubkyAppPostKind>,
        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 = <PubkyAppFeed as Validatable>::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<String>,
    pub image: Option<String>,
    pub links: Option<Vec<PubkyAppUserLink>>,
    pub status: Option<String>,
}

/// 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<String>,
        image: Option<String>,
        links: Option<Vec<PubkyAppUserLink>>,
        status: Option<String>,
    ) -> 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::<String>();

        // 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::<String>());

        // 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::<String>();
                        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::<String>());

        // 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::<String>();

        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::<String>()
            }
            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 = <PubkyAppUser as Validatable>::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 = <PubkyAppUser as Validatable>::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 = <PubkyAppLastRead as Validatable>::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 = <PubkyAppFile as Validatable>::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<Self, String> {
        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 = <PubkyAppBookmark as Validatable>::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<String>, // If a reply, the URI of the parent post.
    pub embed: Option<PubkyAppPostEmbed>,
    pub 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 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::<String>();

        // 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 = <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 = post_json.as_bytes();
        let post = <PubkyAppPost as Validatable>::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::<String>();

        // 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 = <PubkyAppTag as Validatable>::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 = <PubkyAppTag as Validatable>::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 =
            <PubkyAppFollow as Validatable>::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 = <PubkyAppMute as Validatable>::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"]
```
