1use nostr_sdk::prelude::*;
2use std::path::PathBuf;
3use thiserror::Error;
4
5pub mod note;
6pub mod query;
7pub mod watch;
8
9pub use note::Note;
10
11#[derive(Error, Debug)]
12pub enum DialogError {
13 #[error("Nostr error: {0}")]
14 Nostr(#[from] nostr_sdk::client::Error),
15 #[error("Keys error: {0}")]
16 Keys(#[from] nostr_sdk::key::Error),
17 #[error("NIP-44 error: {0}")]
18 Nip44(#[from] nostr_sdk::nips::nip44::Error),
19 #[error("IO error: {0}")]
20 Io(#[from] std::io::Error),
21 #[error("Database error: {0}")]
22 Database(String),
23 #[error("Event builder error: {0}")]
24 EventBuilder(#[from] nostr_sdk::event::builder::Error),
25 #[error("Failed to get project directories")]
26 ProjectDirs,
27}
28
29pub type Result<T> = std::result::Result<T, DialogError>;
30
31pub struct Dialog {
32 pub client: Client,
33 pub keys: Keys,
34}
35
36impl Dialog {
37 pub async fn new(nsec: &str) -> Result<Self> {
38 let keys = Keys::parse(nsec)?;
39
40 let db_path = get_data_dir(&keys.public_key().to_hex())?;
42 let database = NdbDatabase::open(db_path.to_string_lossy())
43 .map_err(|e| DialogError::Database(e.to_string()))?;
44
45 let client = Client::builder()
46 .signer(keys.clone())
47 .database(database)
48 .build();
49
50 Ok(Self { client, keys })
51 }
52
53 pub async fn new_with_relay(nsec: &str, relay_url: &str) -> Result<Self> {
54 let dialog = Self::new(nsec).await?;
55 dialog.connect_relay(relay_url).await?;
56 Ok(dialog)
57 }
58
59 pub async fn connect_relay(&self, url: &str) -> Result<()> {
60 eprintln!("[lib] connect_relay: adding {url}");
61 self.client.add_relay(url).await?;
62 eprintln!("[lib] connect_relay: connecting");
63 self.client.connect().await;
64 eprintln!("[lib] connect_relay: connected");
65 Ok(())
66 }
67
68 pub fn public_key(&self) -> PublicKey {
69 self.keys.public_key()
70 }
71}
72
73fn get_data_dir(pubkey: &str) -> Result<PathBuf> {
74 if let Ok(p) = std::env::var("DIALOG_DATA_DIR") {
76 let p = PathBuf::from(p).join(pubkey);
77 std::fs::create_dir_all(&p)?;
78 return Ok(p.join("nostrdb"));
79 }
80
81 if let Some(dirs) = directories::ProjectDirs::from("", "", "dialog") {
83 let data_dir = dirs.data_dir().join(pubkey);
84 std::fs::create_dir_all(&data_dir)?;
85 return Ok(data_dir.join("nostrdb"));
86 }
87
88 let p = std::env::temp_dir().join("dialog").join(pubkey);
90 std::fs::create_dir_all(&p)?;
91 Ok(p.join("nostrdb"))
92}
93
94pub fn clean_test_storage(pubkey: &str) -> Result<()> {
95 let data_dir = if let Ok(p) = std::env::var("DIALOG_DATA_DIR") {
97 PathBuf::from(p).join(pubkey)
98 } else if let Some(dirs) = directories::ProjectDirs::from("", "", "dialog") {
99 dirs.data_dir().join(pubkey)
100 } else {
101 std::env::temp_dir().join("dialog").join(pubkey)
102 };
103
104 if data_dir.exists() {
105 std::fs::remove_dir_all(data_dir)?;
106 }
107 Ok(())
108}
109
110pub fn validate_nsec(nsec: &str) -> Result<()> {
111 Keys::parse(nsec)?;
112 Ok(())
113}
114
115impl Dialog {
116 pub async fn mark_as_read(&self, note_id: &EventId) -> Result<()> {
118 let content = serde_json::json!({
120 "type": "read_status",
121 "note_id": note_id.to_hex(),
122 "is_read": true,
123 "timestamp": Timestamp::now().as_u64()
124 })
125 .to_string();
126
127 let event = EventBuilder::new(Kind::from(30078), content)
128 .tag(Tag::custom(
129 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::D)),
130 vec!["dialog_local_state"],
131 ))
132 .sign(&self.keys)
133 .await?;
134
135 self.client
137 .database()
138 .save_event(&event)
139 .await
140 .map_err(|e| DialogError::Database(e.to_string()))?;
141
142 Ok(())
143 }
144
145 pub async fn get_read_status(&self, note_id: &EventId) -> bool {
147 let filter = Filter::new()
149 .author(self.keys.public_key())
150 .kind(Kind::from(30078))
151 .custom_tag(
152 SingleLetterTag::lowercase(Alphabet::D),
153 vec!["dialog_local_state"],
154 )
155 .limit(1000); if let Ok(events) = self.client.database().query(vec![filter]).await {
158 let events_vec: Vec<_> = events.into_iter().collect();
160 for event in events_vec.iter().rev() {
162 if let Ok(data) = serde_json::from_str::<serde_json::Value>(&event.content) {
164 if data["type"] == "read_status" && data["note_id"] == note_id.to_hex() {
165 return data["is_read"].as_bool().unwrap_or(false);
166 }
167 }
168 }
169 }
170
171 false }
173
174 pub async fn mark_as_synced(&self, note_id: &EventId) -> Result<()> {
176 let content = serde_json::json!({
177 "type": "sync_status",
178 "note_id": note_id.to_hex(),
179 "is_synced": true,
180 "timestamp": Timestamp::now().as_u64()
181 })
182 .to_string();
183
184 let event = EventBuilder::new(Kind::from(30078), content)
185 .tag(Tag::custom(
186 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::D)),
187 vec!["dialog_local_state"],
188 ))
189 .sign(&self.keys)
190 .await?;
191
192 self.client
193 .database()
194 .save_event(&event)
195 .await
196 .map_err(|e| DialogError::Database(e.to_string()))?;
197
198 Ok(())
199 }
200}