Skip to main content

pubky_app_specs/
traits.rs

1use crate::common::timestamp;
2use base32::{decode, encode, Alphabet};
3use blake3::Hasher;
4use serde::de::DeserializeOwned;
5
6pub trait TimestampId {
7    /// Creates a unique identifier based on the current timestamp.
8    fn create_id(&self) -> String {
9        // Get current time in microseconds since UNIX epoch
10        let now = timestamp();
11
12        // Convert to big-endian bytes
13        let bytes = now.to_be_bytes();
14
15        // Encode the bytes using Base32 with the Crockford alphabet
16        encode(Alphabet::Crockford, &bytes)
17    }
18
19    /// Validates that the provided ID is a valid Crockford Base32-encoded timestamp,
20    /// 13 characters long, and represents a reasonable timestamp.
21    fn validate_id(&self, id: &str) -> Result<(), String> {
22        // Ensure ID is 13 characters long
23        if id.len() != 13 {
24            return Err("Validation Error: Invalid ID length: must be 13 characters".into());
25        }
26
27        // Decode the Crockford Base32-encoded ID
28        let decoded_bytes =
29            decode(Alphabet::Crockford, id).ok_or("Failed to decode Crockford Base32 ID")?;
30
31        if decoded_bytes.len() != 8 {
32            return Err("Validation Error: Invalid ID length after decoding".into());
33        }
34
35        // Convert the decoded bytes to a timestamp in microseconds
36        let timestamp_micros = i64::from_be_bytes(decoded_bytes.try_into().unwrap());
37
38        // Get current time in microseconds
39        let now_micros = timestamp();
40
41        // Define October 1st, 2024, in microseconds since UNIX epoch
42        let oct_first_2024_micros = 1727740800000000; // Timestamp for 2024-10-01 00:00:00 UTC
43
44        // Allowable future duration (2 hours) in microseconds
45        let max_future_micros = now_micros + 2 * 60 * 60 * 1_000_000;
46
47        // Validate that the ID's timestamp is after October 1st, 2024
48        if timestamp_micros < oct_first_2024_micros {
49            return Err(
50                "Validation Error: Invalid ID, timestamp must be after October 1st, 2024".into(),
51            );
52        }
53
54        // Validate that the ID's timestamp is not more than 2 hours in the future
55        if timestamp_micros > max_future_micros {
56            return Err("Validation Error: Invalid ID, timestamp is too far in the future".into());
57        }
58
59        Ok(())
60    }
61}
62
63/// Trait for generating an ID based on the struct's data.
64pub trait HashId {
65    fn get_id_data(&self) -> String;
66
67    /// Creates a unique identifier for bookmarks and tag homeserver paths instance.
68    ///
69    /// The ID is generated by:
70    /// 1. Concatenating the `uri` and `label` fields of the `PubkyAppTag` with a colon (`:`) separator.
71    /// 2. Hashing the concatenated string using the `blake3` hashing algorithm.
72    /// 3. Taking the first half of the bytes from the resulting `blake3` hash.
73    /// 4. Encoding those bytes using the Crockford alphabet (Base32 variant).
74    ///
75    /// The resulting Crockford-encoded string is returned as the tag ID.
76    ///
77    /// # Returns
78    /// - A `String` representing the Crockford-encoded tag ID derived from the `blake3` hash of the concatenated `uri` and `label`.
79    fn create_id(&self) -> String {
80        let data = self.get_id_data();
81
82        // Create a Blake3 hash of the input data
83        let mut hasher = Hasher::new();
84        hasher.update(data.as_bytes());
85        let blake3_hash = hasher.finalize();
86
87        // Get the first half of the hash bytes
88        let half_hash_length = blake3_hash.as_bytes().len() / 2;
89        let half_hash = &blake3_hash.as_bytes()[..half_hash_length];
90
91        // Encode the first half of the hash in Base32 using the Z-base32 alphabet
92        encode(Alphabet::Crockford, half_hash)
93    }
94
95    /// Validates that the provided ID matches the generated ID.
96    fn validate_id(&self, id: &str) -> Result<(), String> {
97        let generated_id = self.create_id();
98        if generated_id != id {
99            return Err(format!("Invalid ID: expected {generated_id}, found {id}"));
100        }
101        Ok(())
102    }
103}
104
105pub trait Validatable: Sized + DeserializeOwned {
106    fn try_from(blob: &[u8], id: &str) -> Result<Self, String> {
107        let mut instance: Self = serde_json::from_slice(blob).map_err(|e| e.to_string())?;
108        instance = instance.sanitize();
109        instance.validate(Some(id))?;
110        Ok(instance)
111    }
112
113    fn validate(&self, id: Option<&str>) -> Result<(), String>;
114
115    fn sanitize(self) -> Self {
116        self
117    }
118}
119
120pub trait HasPath {
121    const PATH_SEGMENT: &'static str;
122    fn create_path() -> String;
123}
124
125pub trait HasIdPath {
126    const PATH_SEGMENT: &'static str;
127    fn create_path(id: &str) -> String;
128}
129
130#[cfg(target_arch = "wasm32")]
131use serde::Serialize;
132#[cfg(target_arch = "wasm32")]
133use serde_wasm_bindgen::{from_value, to_value};
134#[cfg(target_arch = "wasm32")]
135use wasm_bindgen::JsValue;
136
137/// Provides a `.to_json()` method returning a `JsValue` with all fields in plain JSON.
138#[cfg(target_arch = "wasm32")]
139pub trait Json: Serialize + DeserializeOwned + Validatable {
140    fn export_json(&self) -> Result<JsValue, String> {
141        to_value(&self).map_err(|e| format!("JSON serialization error: {}", e))
142    }
143
144    fn import_json(js_value: &JsValue) -> Result<Self, String> {
145        let object: Self =
146            from_value(js_value.clone()).map_err(|e| format!("Error parsing js object: {}", e))?;
147        let object = object.sanitize();
148        object.validate(None)?;
149        Ok(object)
150    }
151}