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)]
67pub struct OkuPost {
69 pub entry: Entry,
71 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 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)]
174pub struct OkuNote {
176 pub url: Url,
178 pub title: String,
180 pub body: String,
182 pub tags: HashSet<String>,
184}
185
186impl OkuNote {
187 pub fn suggested_post_path(&self) -> String {
189 Self::suggested_post_path_from_url(&self.url.to_string())
190 }
191
192 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}