oku_fs/database/posts/
core.rs

1use super::super::core::*;
2use super::super::users::*;
3use crate::fs::FS_PATH;
4use iroh_docs::rpc::client::docs::Entry;
5use iroh_docs::AuthorId;
6use native_db::*;
7use native_model::{native_model, Model};
8use serde::{Deserialize, Serialize};
9use std::hash::{Hash, Hasher};
10use std::{
11    collections::{HashMap, HashSet},
12    path::PathBuf,
13    str::FromStr,
14    sync::{Arc, LazyLock},
15    time::SystemTime,
16};
17use tantivy::{
18    directory::MmapDirectory,
19    schema::{Field, Schema, Value, FAST, STORED, TEXT},
20    Directory, Index, IndexReader, IndexWriter, TantivyDocument, Term,
21};
22use tokio::sync::Mutex;
23use url::Url;
24
25pub(crate) static POST_INDEX_PATH: LazyLock<PathBuf> =
26    LazyLock::new(|| PathBuf::from(FS_PATH).join("POST_INDEX"));
27pub(crate) static POST_SCHEMA: LazyLock<(Schema, HashMap<&str, Field>)> = LazyLock::new(|| {
28    let mut schema_builder = Schema::builder();
29    let fields = HashMap::from([
30        ("id", schema_builder.add_bytes_field("id", STORED)),
31        (
32            "author_id",
33            schema_builder.add_text_field("author_id", TEXT | STORED),
34        ),
35        ("path", schema_builder.add_text_field("path", TEXT | STORED)),
36        ("url", schema_builder.add_text_field("url", TEXT | STORED)),
37        (
38            "title",
39            schema_builder.add_text_field("title", TEXT | STORED),
40        ),
41        ("body", schema_builder.add_text_field("body", TEXT | STORED)),
42        ("tag", schema_builder.add_text_field("tag", TEXT | STORED)),
43        (
44            "timestamp",
45            schema_builder.add_date_field("timestamp", FAST),
46        ),
47    ]);
48    let schema = schema_builder.build();
49    (schema, fields)
50});
51pub(crate) static POST_INDEX: LazyLock<Index> = LazyLock::new(|| {
52    let _ = std::fs::create_dir_all(&*POST_INDEX_PATH);
53    let mmap_directory: Box<dyn Directory> =
54        Box::new(MmapDirectory::open(&*POST_INDEX_PATH).unwrap());
55    Index::open_or_create(mmap_directory, POST_SCHEMA.0.clone()).unwrap()
56});
57pub(crate) static POST_INDEX_READER: LazyLock<IndexReader> =
58    LazyLock::new(|| POST_INDEX.reader().unwrap());
59pub(crate) static POST_INDEX_WRITER: LazyLock<Arc<Mutex<IndexWriter>>> =
60    LazyLock::new(|| Arc::new(Mutex::new(POST_INDEX.writer(50_000_000).unwrap())));
61
62#[derive(Serialize, Deserialize, Debug, Clone)]
63#[native_model(id = 2, version = 2)]
64#[native_db(
65    primary_key(primary_key -> (Vec<u8>, Vec<u8>))
66)]
67/// An OkuNet post.
68pub struct OkuPost {
69    /// A record of a version of the post file.
70    pub entry: Entry,
71    /// The content of the post on OkuNet.
72    pub note: OkuNote,
73}
74
75impl PartialEq for OkuPost {
76    fn eq(&self, other: &Self) -> bool {
77        self.primary_key() == other.primary_key()
78    }
79}
80impl Eq for OkuPost {}
81impl Hash for OkuPost {
82    fn hash<H: Hasher>(&self, state: &mut H) {
83        self.primary_key().hash(state);
84    }
85}
86
87impl From<OkuPost> for TantivyDocument {
88    fn from(value: OkuPost) -> Self {
89        let post_key: [Vec<u8>; 2] = value.primary_key().into();
90        let post_key_bytes = post_key.concat();
91
92        let mut doc = TantivyDocument::default();
93        doc.add_bytes(POST_SCHEMA.1["id"], post_key_bytes);
94        doc.add_text(
95            POST_SCHEMA.1["author_id"],
96            crate::fs::util::fmt(value.entry.author()),
97        );
98        doc.add_text(
99            POST_SCHEMA.1["path"],
100            String::from_utf8_lossy(value.entry.key()),
101        );
102        doc.add_text(POST_SCHEMA.1["url"], value.note.url.to_string());
103        doc.add_text(POST_SCHEMA.1["title"], value.note.title);
104        doc.add_text(POST_SCHEMA.1["body"], value.note.body);
105        for tag in value.note.tags {
106            doc.add_text(POST_SCHEMA.1["tag"], tag);
107        }
108        doc.add_date(
109            POST_SCHEMA.1["timestamp"],
110            tantivy::DateTime::from_timestamp_micros(value.entry.timestamp() as i64),
111        );
112        doc
113    }
114}
115
116impl TryFrom<TantivyDocument> for OkuPost {
117    type Error = anyhow::Error;
118
119    fn try_from(value: TantivyDocument) -> Result<Self, Self::Error> {
120        let author_id = AuthorId::from_str(
121            value
122                .get_first(POST_SCHEMA.1["author_id"])
123                .ok_or(anyhow::anyhow!("No author ID for document in index … "))?
124                .as_str()
125                .ok_or(anyhow::anyhow!("No author ID for document in index … "))?,
126        )?;
127        let path = value
128            .get_first(POST_SCHEMA.1["path"])
129            .ok_or(anyhow::anyhow!("No path for document in index … "))?
130            .as_str()
131            .ok_or(anyhow::anyhow!("No path for document in index … "))?
132            .to_string();
133        DATABASE
134            .get_post(&author_id, &path.clone().into())
135            .ok()
136            .flatten()
137            .ok_or(anyhow::anyhow!(
138                "No post with author {} and path {} found … ",
139                author_id,
140                path
141            ))
142    }
143}
144
145impl OkuPost {
146    pub(crate) fn primary_key(&self) -> (Vec<u8>, Vec<u8>) {
147        (
148            self.entry.author().as_bytes().to_vec(),
149            self.entry.key().to_vec(),
150        )
151    }
152
153    pub(crate) fn index_term(&self) -> Term {
154        let post_key: [Vec<u8>; 2] = self.primary_key().into();
155        let post_key_bytes = post_key.concat();
156        Term::from_field_bytes(POST_SCHEMA.1["id"], &post_key_bytes)
157    }
158
159    /// Obtain the author of this post from the OkuNet database.
160    pub fn user(&self) -> OkuUser {
161        match DATABASE.get_user(&self.entry.author()).ok().flatten() {
162            Some(user) => user,
163            None => OkuUser {
164                author_id: self.entry.author(),
165                last_fetched: SystemTime::now(),
166                posts: vec![self.entry.clone()],
167                identity: None,
168            },
169        }
170    }
171}
172
173#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
174/// A note left by an Oku user regarding some URL-addressed content.
175pub struct OkuNote {
176    /// The URL the note is regarding.
177    pub url: Url,
178    /// The title of the note.
179    pub title: String,
180    /// The body of the note.
181    pub body: String,
182    /// A list of tags associated with the note.
183    pub tags: HashSet<String>,
184}
185
186impl OkuNote {
187    /// Generate a suggested post path for the note.
188    pub fn suggested_post_path(&self) -> String {
189        Self::suggested_post_path_from_url(&self.url.to_string())
190    }
191
192    /// Generate a suggested post path using a URL.
193    pub fn suggested_post_path_from_url(url: &String) -> String {
194        format!(
195            "/posts/{}.okupost",
196            bs58::encode(url.as_bytes()).into_string()
197        )
198    }
199}