use std::{collections::HashSet, path::Path, sync::Arc};
use anyhow::{Context, Result, bail};
use nostr::{PublicKey, Url, event::Tag, signer::NostrSigner};
use nostr_sdk::{Alphabet, JsonUtil, Kind, SingleLetterTag, Timestamp, ToBech32};
use serde::{self, Deserialize, Serialize};
#[cfg(not(test))]
use crate::client::Client;
#[cfg(test)]
use crate::client::MockConnect;
use crate::{
client::{Connect, get_event_from_global_cache, is_verbose, sign_event},
git_events::KIND_USER_GRASP_LIST,
};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct UserRef {
pub public_key: PublicKey,
pub metadata: UserMetadata,
pub relays: UserRelays,
pub grasp_list: UserGraspList,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct UserMetadata {
pub name: String,
pub created_at: Timestamp,
pub nip05: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct UserRelays {
pub relays: Vec<UserRelayRef>,
pub created_at: Timestamp,
}
impl UserRelays {
pub fn write(&self) -> Vec<String> {
self.relays
.iter()
.filter(|r| r.write)
.map(|r| r.url.clone())
.collect()
}
pub fn read(&self) -> Vec<String> {
self.relays
.iter()
.filter(|r| r.read)
.map(|r| r.url.clone())
.collect()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct UserGraspList {
pub urls: Vec<Url>,
pub created_at: Timestamp,
}
impl UserGraspList {
pub async fn to_event(&mut self, signer: &Arc<dyn NostrSigner>) -> Result<nostr::Event> {
let event = sign_event(
nostr_sdk::EventBuilder::new(KIND_USER_GRASP_LIST, "").tags(
self.urls
.iter()
.map(|url| {
Tag::custom(
nostr::TagKind::Custom(std::borrow::Cow::Borrowed("g")),
vec![url.to_string()],
)
})
.collect::<Vec<_>>(),
),
signer,
"user grasp list".to_string(),
)
.await?;
self.created_at = event.created_at;
Ok(event)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct UserRelayRef {
pub url: String,
pub read: bool,
pub write: bool,
}
pub async fn get_user_details(
public_key: &PublicKey,
#[cfg(test)] client: Option<&MockConnect>,
#[cfg(not(test))] client: Option<&Client>,
git_repo_path: Option<&Path>,
cache_only: bool,
fetch_profile_updates: bool,
) -> Result<UserRef> {
if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await {
if fetch_profile_updates {
if let Some(client) = client {
let term = console::Term::stderr();
if is_verbose() {
term.write_line("searching for profile updates...")?;
}
let (reports, progress_reporter) = client
.fetch_all(git_repo_path, None, &HashSet::from_iter(vec![*public_key]))
.await?;
if !reports.iter().any(|r| r.is_err()) {
progress_reporter.clear()?;
if is_verbose() {
term.clear_last_lines(1)?;
}
}
return get_user_ref_from_cache(git_repo_path, public_key).await;
}
}
Ok(user_ref)
} else {
let empty = UserRef {
public_key: public_key.to_owned(),
metadata: extract_user_metadata(public_key, &[])?,
relays: extract_user_relays(public_key, &[]),
grasp_list: extract_user_grasp_list(public_key, &[]),
};
if cache_only {
Ok(empty)
} else if let Some(client) = client {
let term = console::Term::stderr();
if is_verbose() {
term.write_line("searching for profile...")?;
}
let (_, progress_reporter) = client
.fetch_all(git_repo_path, None, &HashSet::from_iter(vec![*public_key]))
.await?;
if let Ok(user_ref) = get_user_ref_from_cache(git_repo_path, public_key).await {
progress_reporter.clear()?;
Ok(user_ref)
} else {
Ok(empty)
}
} else {
Ok(empty)
}
}
}
pub async fn get_user_ref_from_cache(
git_repo_path: Option<&Path>,
public_key: &PublicKey,
) -> Result<UserRef> {
let filters = vec![
nostr::Filter::default()
.author(*public_key)
.kind(Kind::Metadata),
nostr::Filter::default()
.author(*public_key)
.kind(Kind::RelayList),
nostr::Filter::default()
.author(*public_key)
.kind(KIND_USER_GRASP_LIST),
];
let events = get_event_from_global_cache(git_repo_path, filters.clone()).await?;
if events.is_empty() {
bail!("no metadata and profile list in cache for selected public key");
}
Ok(UserRef {
public_key: public_key.to_owned(),
metadata: extract_user_metadata(public_key, &events)?,
relays: extract_user_relays(public_key, &events),
grasp_list: extract_user_grasp_list(public_key, &events),
})
}
pub fn extract_user_metadata(
public_key: &nostr::PublicKey,
events: &[nostr::Event],
) -> Result<UserMetadata> {
let event = events
.iter()
.filter(|e| e.kind.eq(&nostr::Kind::Metadata) && e.pubkey.eq(public_key))
.max_by_key(|e| e.created_at);
let metadata: Option<nostr::Metadata> = if let Some(event) = event {
Some(
nostr::Metadata::from_json(event.content.clone())
.context("metadata cannot be found in kind 0 event content")?,
)
} else {
None
};
Ok(UserMetadata {
name: if let Some(metadata) = metadata.clone() {
if let Some(n) = metadata.name {
n
} else if let Some(n) = metadata.custom.get("displayName") {
let binding = n.to_string();
let mut chars = binding.chars();
chars.next();
chars.next_back();
chars.as_str().to_string()
} else if let Some(n) = metadata.display_name {
n
} else {
public_key.to_bech32()?
}
} else {
public_key.to_bech32()?
},
nip05: if let Some(metadata) = metadata {
metadata.nip05
} else {
None
},
created_at: if let Some(event) = event {
event.created_at
} else {
Timestamp::from(0)
},
})
}
pub fn extract_user_relays(public_key: &nostr::PublicKey, events: &[nostr::Event]) -> UserRelays {
let event = events
.iter()
.filter(|e| e.kind.eq(&nostr::Kind::RelayList) && e.pubkey.eq(public_key))
.max_by_key(|e| e.created_at);
UserRelays {
relays: if let Some(event) = event {
event
.tags
.iter()
.filter(|t| {
t.as_slice().len() > 1
&& t.kind()
.eq(&nostr::TagKind::SingleLetter(SingleLetterTag::lowercase(
Alphabet::R,
)))
})
.map(|t| UserRelayRef {
url: t.as_slice()[1].clone(),
read: t.as_slice().len() == 2 || t.as_slice()[2].eq("read"),
write: t.as_slice().len() == 2 || t.as_slice()[2].eq("write"),
})
.collect()
} else {
vec![]
},
created_at: if let Some(event) = event {
event.created_at
} else {
Timestamp::from(0)
},
}
}
pub fn extract_user_grasp_list(
public_key: &nostr::PublicKey,
events: &[nostr::Event],
) -> UserGraspList {
let event = events
.iter()
.filter(|e| e.kind.eq(&KIND_USER_GRASP_LIST) && e.pubkey.eq(public_key))
.max_by_key(|e| e.created_at);
UserGraspList {
urls: if let Some(event) = event {
event
.tags
.iter()
.filter_map(|t| {
if t.as_slice().len() > 1 && t.as_slice()[0] == "g" {
Url::parse(&t.as_slice()[1]).ok()
} else {
None
}
})
.collect()
} else {
vec![]
},
created_at: if let Some(event) = event {
event.created_at
} else {
Timestamp::from(0)
},
}
}