use nostr_sdk::prelude::*;
use std::path::PathBuf;
use thiserror::Error;
pub mod note;
pub mod query;
pub mod watch;
pub use note::Note;
#[derive(Error, Debug)]
pub enum DialogError {
#[error("Nostr error: {0}")]
Nostr(#[from] nostr_sdk::client::Error),
#[error("Keys error: {0}")]
Keys(#[from] nostr_sdk::key::Error),
#[error("NIP-44 error: {0}")]
Nip44(#[from] nostr_sdk::nips::nip44::Error),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Database error: {0}")]
Database(String),
#[error("Event builder error: {0}")]
EventBuilder(#[from] nostr_sdk::event::builder::Error),
#[error("Failed to get project directories")]
ProjectDirs,
}
pub type Result<T> = std::result::Result<T, DialogError>;
pub struct Dialog {
pub client: Client,
pub keys: Keys,
}
impl Dialog {
pub async fn new(nsec: &str) -> Result<Self> {
let keys = Keys::parse(nsec)?;
let db_path = get_data_dir(&keys.public_key().to_hex())?;
let database = NdbDatabase::open(db_path.to_string_lossy())
.map_err(|e| DialogError::Database(e.to_string()))?;
let client = Client::builder()
.signer(keys.clone())
.database(database)
.build();
Ok(Self { client, keys })
}
pub async fn new_with_relay(nsec: &str, relay_url: &str) -> Result<Self> {
let dialog = Self::new(nsec).await?;
dialog.connect_relay(relay_url).await?;
Ok(dialog)
}
pub async fn connect_relay(&self, url: &str) -> Result<()> {
eprintln!("[lib] connect_relay: adding {url}");
self.client.add_relay(url).await?;
eprintln!("[lib] connect_relay: connecting");
self.client.connect().await;
eprintln!("[lib] connect_relay: connected");
Ok(())
}
pub fn public_key(&self) -> PublicKey {
self.keys.public_key()
}
}
fn get_data_dir(pubkey: &str) -> Result<PathBuf> {
if let Ok(p) = std::env::var("DIALOG_DATA_DIR") {
let p = PathBuf::from(p).join(pubkey);
std::fs::create_dir_all(&p)?;
return Ok(p.join("nostrdb"));
}
if let Some(dirs) = directories::ProjectDirs::from("", "", "dialog") {
let data_dir = dirs.data_dir().join(pubkey);
std::fs::create_dir_all(&data_dir)?;
return Ok(data_dir.join("nostrdb"));
}
let p = std::env::temp_dir().join("dialog").join(pubkey);
std::fs::create_dir_all(&p)?;
Ok(p.join("nostrdb"))
}
pub fn clean_test_storage(pubkey: &str) -> Result<()> {
let data_dir = if let Ok(p) = std::env::var("DIALOG_DATA_DIR") {
PathBuf::from(p).join(pubkey)
} else if let Some(dirs) = directories::ProjectDirs::from("", "", "dialog") {
dirs.data_dir().join(pubkey)
} else {
std::env::temp_dir().join("dialog").join(pubkey)
};
if data_dir.exists() {
std::fs::remove_dir_all(data_dir)?;
}
Ok(())
}
pub fn validate_nsec(nsec: &str) -> Result<()> {
Keys::parse(nsec)?;
Ok(())
}
impl Dialog {
pub async fn mark_as_read(&self, note_id: &EventId) -> Result<()> {
let content = serde_json::json!({
"type": "read_status",
"note_id": note_id.to_hex(),
"is_read": true,
"timestamp": Timestamp::now().as_u64()
})
.to_string();
let event = EventBuilder::new(Kind::from(30078), content)
.tag(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::D)),
vec!["dialog_local_state"],
))
.sign(&self.keys)
.await?;
self.client
.database()
.save_event(&event)
.await
.map_err(|e| DialogError::Database(e.to_string()))?;
Ok(())
}
pub async fn get_read_status(&self, note_id: &EventId) -> bool {
let filter = Filter::new()
.author(self.keys.public_key())
.kind(Kind::from(30078))
.custom_tag(
SingleLetterTag::lowercase(Alphabet::D),
vec!["dialog_local_state"],
)
.limit(1000);
if let Ok(events) = self.client.database().query(vec![filter]).await {
let events_vec: Vec<_> = events.into_iter().collect();
for event in events_vec.iter().rev() {
if let Ok(data) = serde_json::from_str::<serde_json::Value>(&event.content) {
if data["type"] == "read_status" && data["note_id"] == note_id.to_hex() {
return data["is_read"].as_bool().unwrap_or(false);
}
}
}
}
false }
pub async fn mark_as_synced(&self, note_id: &EventId) -> Result<()> {
let content = serde_json::json!({
"type": "sync_status",
"note_id": note_id.to_hex(),
"is_synced": true,
"timestamp": Timestamp::now().as_u64()
})
.to_string();
let event = EventBuilder::new(Kind::from(30078), content)
.tag(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::D)),
vec!["dialog_local_state"],
))
.sign(&self.keys)
.await?;
self.client
.database()
.save_event(&event)
.await
.map_err(|e| DialogError::Database(e.to_string()))?;
Ok(())
}
}