use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::channel;
use std::ops::DerefMut;
use standard_paths::{LocationType, StandardPaths};
use windmark_titanesque::{context::HookContext, router::Router};
use nostr_sdk::prelude::*;
use openssl::hash::MessageDigest;
use crate::events::{handle_nostr_notifications, user_subscribe};
use crate::storage::Storage;
use crate::user::{CaracalUser, USERS};
use crate::nostru;
#[derive(Default)]
pub struct Hookstr {
data_path: PathBuf,
identities_path: PathBuf,
visits: usize,
users: HashMap<String, CaracalUser>,
}
pub const DEFAULT_RELAYS: &[&str] = &[
"wss://nostr.bitcoiner.social",
"wss://nos.lol",
"wss://relay.damus.io",
"wss://offchain.pub",
];
impl Hookstr {
pub async fn lookup_client(
&mut self,
fingerprint: String,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
#[allow(static_mut_refs)]
unsafe {
let us = USERS.deref_mut();
if let Some(user) = us.get_mut(&fingerprint) {
if let Ok(_msg) = user.rx.try_recv() {
user_subscribe(user).await;
}
return Err(Box::from("User already exists"));
}
}
let i_path = self.identities_path.join(fingerprint.clone());
let Ok(_) = std::fs::create_dir_all(i_path.clone()) else {
return Err(Box::from("Cannot create directory for identity"));
};
let lmdbp = i_path.clone().join("nostr-lmdb");
let storep = i_path.clone().join("userdb");
let Ok(database) = NostrLMDB::open(lmdbp.display().to_string()) else {
return Err(Box::from("Cannot open nostr database"));
};
let Ok(storage) = Storage::new(storep.as_path()) else {
return Err(Box::from("Cannot create storage"));
};
storage.create_following_db()?;
storage.create_drafts_db()?;
storage.create_keys_db()?;
storage.create_hashtags_db()?;
let keys = {
let mut rtxn = storage.get_read_txn()?;
match storage.get_default_keys(&mut rtxn) {
Ok(def_keys) => def_keys,
Err(_err) => {
storage.create_default_keys()?
}
}
};
let opts = ClientOptions::default()
.autoconnect(true)
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(90),
})
.gossip(true);
let client = Client::builder()
.opts(opts)
.signer(keys.clone())
.database(database)
.build();
for rurl in DEFAULT_RELAYS {
client.add_relay(rurl.to_string()).await?;
}
client.connect().await;
if let Err(err) = nostru::advertise_relay_list(&client).await {
eprintln!("Failed to advertise relay list: {err}")
}
#[allow(static_mut_refs)]
unsafe {
let us = USERS.deref_mut();
let (tx, rx) = channel(16);
let client = Arc::new(client);
tokio::spawn(handle_nostr_notifications(client.clone()));
let user = CaracalUser {
client,
upload_keys: Keys::generate(),
signer: Arc::new(keys),
connect_uri: None,
storage,
tx,
rx,
};
user_subscribe(&user).await;
us.insert(fingerprint.clone(), user);
}
Ok(())
}
}
fn fingerprint_from_hook(context: &HookContext) -> Option<String> {
use std::fmt::Write;
if let Some(cert) = &context.certificate {
match cert.digest(MessageDigest::sha256()) {
Ok(dbytes) => {
let digest: String =
dbytes.iter().fold(String::new(), |mut out, x| {
let _ = write!(out, "{:02x}", x);
out
});
Some(digest)
}
Err(_error) => None,
}
} else {
None
}
}
#[async_trait::async_trait]
impl windmark_titanesque::module::AsyncModule for Hookstr {
async fn on_attach(&mut self, _router: &mut Router) {
let sp = StandardPaths::without_org(env!("CARGO_PKG_NAME"));
self.data_path = sp
.writable_location(LocationType::AppLocalDataLocation)
.unwrap();
self.identities_path = self.data_path.clone().join("identitites");
let _ = std::fs::create_dir_all(self.identities_path.clone());
}
async fn on_pre_route(&mut self, context: HookContext) {
if let Some(fp) = fingerprint_from_hook(&context) {
let _ = self.lookup_client(fp).await;
}
self.visits += 1;
}
}