feat: manually handle NIP-42 auth request (#132)

* improve fetch relays

* .

* .

* .

* refactor

* refactor

* remove identity

* manually auth

* auth

* prevent duplicate message

* clean up
This commit is contained in:
reya
2025-08-30 14:38:00 +07:00
committed by GitHub
parent 49a3dedd9c
commit 807851518a
33 changed files with 1810 additions and 1443 deletions

View File

@@ -1,14 +1,12 @@
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use anyhow::Error;
use common::event::EventUtils;
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use global::nostr_client;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task, WeakEntity, Window,
};
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task, WeakEntity, Window};
use hashbrown::{HashMap, HashSet};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use room::RoomKind;
@@ -29,7 +27,7 @@ struct GlobalRegistry(Entity<Registry>);
impl Global for GlobalRegistry {}
#[derive(Debug)]
pub enum RegistrySignal {
pub enum RegistryEvent {
Open(WeakEntity<Room>),
Close(u64),
NewRequest(RoomKind),
@@ -41,19 +39,21 @@ pub struct Registry {
pub rooms: Vec<Entity<Room>>,
/// Collection of all persons (user profiles)
pub persons: BTreeMap<PublicKey, Entity<Profile>>,
pub persons: HashMap<PublicKey, Entity<Profile>>,
/// Indicates if rooms are currently being loaded
///
/// Always equal to `true` when the app starts
pub loading: bool,
/// Subscriptions for observing changes
#[allow(dead_code)]
subscriptions: SmallVec<[Subscription; 2]>,
/// Public Key of the current user
pub identity: Option<PublicKey>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
}
impl EventEmitter<RegistrySignal> for Registry {}
impl EventEmitter<RegistryEvent> for Registry {}
impl Registry {
/// Retrieve the Global Registry state
@@ -73,74 +73,68 @@ impl Registry {
/// Create a new Registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
// Load all user profiles from the database when the Registry is created
subscriptions.push(cx.observe_new::<Self>(|this, _window, cx| {
let task = this.load_local_person(cx);
this.set_persons_from_task(task, cx);
}));
let load_local_persons: Task<Result<Vec<Profile>, Error>> =
cx.background_spawn(async move {
let client = nostr_client();
let filter = Filter::new().kind(Kind::Metadata).limit(200);
let events = client.database().query(filter).await?;
let mut profiles = vec![];
// When any Room is created, load members metadata
subscriptions.push(cx.observe_new::<Room>(|this, _window, cx| {
let state = Self::global(cx);
let task = this.load_metadata(cx);
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
profiles.push(profile);
}
state.update(cx, |this, cx| {
this.set_persons_from_task(task, cx);
Ok(profiles)
});
}));
tasks.push(
// Load all user profiles from the database when the Registry is created
cx.spawn(async move |this, cx| {
if let Ok(profiles) = load_local_persons.await {
this.update(cx, |this, cx| {
this.set_persons(profiles, cx);
})
.ok();
}
}),
);
Self {
rooms: vec![],
persons: BTreeMap::new(),
persons: HashMap::new(),
identity: None,
loading: true,
subscriptions,
_tasks: tasks,
}
}
pub fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms = vec![];
self.loading = true;
/// Returns the identity of the user.
///
/// WARNING: This method will panic if user is not logged in.
pub fn identity(&self, cx: &App) -> Profile {
self.get_person(&self.identity.unwrap(), cx)
}
/// Sets the identity of the user.
pub fn set_identity(&mut self, identity: PublicKey, cx: &mut Context<Self>) {
self.identity = Some(identity);
cx.notify();
}
pub(crate) fn set_persons_from_task(
&mut self,
task: Task<Result<Vec<Profile>, Error>>,
cx: &mut Context<Self>,
) {
cx.spawn(async move |this, cx| {
if let Ok(profiles) = task.await {
this.update(cx, |this, cx| {
for profile in profiles {
this.persons
.insert(profile.public_key(), cx.new(|_| profile));
}
cx.notify();
})
.ok();
}
})
.detach();
}
pub(crate) fn load_local_person(&self, cx: &App) -> Task<Result<Vec<Profile>, Error>> {
cx.background_spawn(async move {
let filter = Filter::new().kind(Kind::Metadata).limit(100);
let events = nostr_client().database().query(filter).await?;
let mut profiles = vec![];
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
profiles.push(profile);
}
Ok(profiles)
})
/// Insert batch of persons
pub fn set_persons(&mut self, profiles: Vec<Profile>, cx: &mut Context<Self>) {
for profile in profiles.into_iter() {
self.persons
.insert(profile.public_key(), cx.new(|_| profile));
}
cx.notify();
}
/// Get single person
pub fn get_person(&self, public_key: &PublicKey, cx: &App) -> Profile {
self.persons
.get(public_key)
@@ -149,6 +143,7 @@ impl Registry {
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
}
/// Get group of persons
pub fn get_group_person(&self, public_keys: &[PublicKey], cx: &App) -> Vec<Profile> {
let mut profiles = vec![];
@@ -160,6 +155,7 @@ impl Registry {
profiles
}
/// Insert or update a person
pub fn insert_or_update_person(&mut self, event: Event, cx: &mut App) {
let public_key = event.pubkey;
let Ok(metadata) = Metadata::from_json(event.content) else {
@@ -213,7 +209,7 @@ impl Registry {
/// Close a room.
pub fn close_room(&mut self, id: u64, cx: &mut Context<Self>) {
if self.rooms.iter().any(|r| r.read(cx).id == id) {
cx.emit(RegistrySignal::Close(id));
cx.emit(RegistryEvent::Close(id));
}
}
@@ -253,6 +249,14 @@ impl Registry {
cx.notify();
}
/// Reset the registry.
pub fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms = vec![];
self.loading = true;
self.identity = None;
cx.notify();
}
/// Load all rooms from the database.
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
log::info!("Starting to load chat rooms...");
@@ -260,7 +264,7 @@ impl Registry {
// Get the contact bypass setting
let contact_bypass = AppSettings::get_contact_bypass(cx);
let task: Task<Result<BTreeSet<Room>, Error>> = cx.background_spawn(async move {
let task: Task<Result<HashSet<Room>, Error>> = cx.background_spawn(async move {
let client = nostr_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
@@ -279,7 +283,7 @@ impl Registry {
let recv_events = client.database().query(recv).await?;
let events = send_events.merge(recv_events);
let mut rooms: BTreeSet<Room> = BTreeSet::new();
let mut rooms: HashSet<Room> = HashSet::new();
// Process each event and group by room hash
for event in events
@@ -343,7 +347,7 @@ impl Registry {
.detach();
}
pub(crate) fn extend_rooms(&mut self, rooms: BTreeSet<Room>, cx: &mut Context<Self>) {
pub(crate) fn extend_rooms(&mut self, rooms: HashSet<Room>, cx: &mut Context<Self>) {
let mut room_map: HashMap<u64, usize> = HashMap::with_capacity(self.rooms.len());
for (index, room) in self.rooms.iter().enumerate() {
@@ -380,7 +384,7 @@ impl Registry {
weak_room
};
cx.emit(RegistrySignal::Open(weak_room));
cx.emit(RegistryEvent::Open(weak_room));
}
/// Refresh messages for a room in the global registry
@@ -400,7 +404,7 @@ impl Registry {
/// Updates room ordering based on the most recent messages.
pub fn event_to_message(
&mut self,
identity: PublicKey,
gift_wrap_id: EventId,
event: Event,
window: &mut Window,
cx: &mut Context<Self>,
@@ -408,6 +412,10 @@ impl Registry {
let id = event.uniq_id();
let author = event.pubkey;
let Some(identity) = self.identity else {
return;
};
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
// Update room
room.update(cx, |this, cx| {
@@ -420,7 +428,7 @@ impl Registry {
// Emit the new message to the room
cx.defer_in(window, move |this, _window, cx| {
this.emit_message(event, cx);
this.emit_message(gift_wrap_id, event, cx);
});
});
@@ -436,7 +444,7 @@ impl Registry {
// Notify the UI about the new room
cx.defer_in(window, move |_this, _window, cx| {
cx.emit(RegistrySignal::NewRequest(RoomKind::default()));
cx.emit(RegistryEvent::NewRequest(RoomKind::default()));
});
}
}

View File

@@ -1,7 +1,5 @@
use std::hash::Hash;
use chrono::{Local, TimeZone};
use gpui::SharedString;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone)]
@@ -10,8 +8,8 @@ pub struct RenderedMessage {
/// Author's public key
pub author: PublicKey,
/// The content/text of the message
pub content: SharedString,
/// When the message was created
pub content: String,
/// Message created time as unix timestamp
pub created_at: Timestamp,
/// List of mentioned public keys in the message
pub mentions: Vec<PublicKey>,
@@ -27,7 +25,7 @@ impl From<Event> for RenderedMessage {
Self {
id: inner.id,
author: inner.pubkey,
content: inner.content.into(),
content: inner.content,
created_at: inner.created_at,
mentions,
replies_to,
@@ -44,7 +42,7 @@ impl From<UnsignedEvent> for RenderedMessage {
// Event ID must be known
id: inner.id.unwrap(),
author: inner.pubkey,
content: inner.content.into(),
content: inner.content,
created_at: inner.created_at,
mentions,
replies_to,
@@ -90,30 +88,6 @@ impl Hash for RenderedMessage {
}
}
impl RenderedMessage {
/// Returns a human-readable string representing how long ago the message was created
pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "Invalid timestamp".into(),
};
let now = Local::now();
let input_date = input_time.date_naive();
let now_date = now.date_naive();
let yesterday_date = (now - chrono::Duration::days(1)).date_naive();
let time_format = input_time.format("%H:%M %p");
match input_date {
date if date == now_date => format!("Today at {time_format}"),
date if date == yesterday_date => format!("Yesterday at {time_format}"),
_ => format!("{}, {time_format}", input_time.format("%d/%m/%y")),
}
.into()
}
}
fn extract_mentions(content: &str) -> Vec<PublicKey> {
let parser = NostrParser::new();
let tokens = parser.parse(content);

View File

@@ -1,23 +1,16 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use anyhow::{anyhow, Error};
use chrono::{Local, TimeZone};
use common::display::DisplayProfile;
use anyhow::Error;
use common::display::ReadableProfile;
use common::event::EventUtils;
use global::nostr_client;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::SmallVec;
use crate::Registry;
pub(crate) const NOW: &str = "now";
pub(crate) const SECONDS_IN_MINUTE: i64 = 60;
pub(crate) const MINUTES_IN_HOUR: i64 = 60;
pub(crate) const HOURS_IN_DAY: i64 = 24;
pub(crate) const DAYS_IN_MONTH: i64 = 30;
#[derive(Debug, Clone)]
pub struct SendReport {
pub receiver: PublicKey,
@@ -69,7 +62,7 @@ impl SendReport {
#[derive(Debug, Clone)]
pub enum RoomSignal {
NewMessage(Box<Event>),
NewMessage((EventId, Box<Event>)),
Refresh,
}
@@ -85,11 +78,11 @@ pub struct Room {
pub id: u64,
pub created_at: Timestamp,
/// Subject of the room
pub subject: Option<SharedString>,
pub subject: Option<String>,
/// Picture of the room
pub picture: Option<SharedString>,
pub picture: Option<String>,
/// All members of the room
pub members: SmallVec<[PublicKey; 2]>,
pub members: Vec<PublicKey>,
/// Kind
pub kind: RoomKind,
}
@@ -112,6 +105,12 @@ impl PartialEq for Room {
}
}
impl Hash for Room {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl Eq for Room {}
impl EventEmitter<RoomSignal> for Room {}
@@ -120,21 +119,25 @@ impl Room {
pub fn new(event: &Event) -> Self {
let id = event.uniq_id();
let created_at = event.created_at;
let public_keys = event.all_pubkeys();
// Convert pubkeys into members
let members = public_keys.into_iter().unique().sorted().collect();
// Get the members from the event's tags and event's pubkey
let members = event
.all_pubkeys()
.into_iter()
.unique()
.sorted()
.collect_vec();
// Get the subject from the event's tags
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
tag.content().map(|s| s.to_owned().into())
tag.content().map(|s| s.to_owned())
} else {
None
};
// Get the picture from the event's tags
let picture = if let Some(tag) = event.tags.find(TagKind::custom("picture")) {
tag.content().map(|s| s.to_owned().into())
tag.content().map(|s| s.to_owned())
} else {
None
};
@@ -177,11 +180,9 @@ impl Room {
///
/// The modified Room instance with the new member list after rearrangement
pub fn rearrange_by(mut self, rearrange_by: PublicKey) -> Self {
let (not_match, matches): (Vec<PublicKey>, Vec<PublicKey>) = self
.members
.into_iter()
.partition(|key| key != &rearrange_by);
self.members = not_match.into();
let (not_match, matches): (Vec<PublicKey>, Vec<PublicKey>) =
self.members.iter().partition(|&key| key != &rearrange_by);
self.members = not_match;
self.members.extend(matches);
self
}
@@ -224,8 +225,8 @@ impl Room {
///
/// * `subject` - The new subject to set
/// * `cx` - The context to notify about the update
pub fn subject(&mut self, subject: impl Into<SharedString>, cx: &mut Context<Self>) {
self.subject = Some(subject.into());
pub fn subject(&mut self, subject: String, cx: &mut Context<Self>) {
self.subject = Some(subject);
cx.notify();
}
@@ -235,42 +236,11 @@ impl Room {
///
/// * `picture` - The new subject to set
/// * `cx` - The context to notify about the update
pub fn picture(&mut self, picture: impl Into<SharedString>, cx: &mut Context<Self>) {
self.picture = Some(picture.into());
pub fn picture(&mut self, picture: String, cx: &mut Context<Self>) {
self.picture = Some(picture);
cx.notify();
}
/// Returns a human-readable string representing how long ago the room was created
///
/// The string will be formatted differently based on the time elapsed:
/// - Less than a minute: "now"
/// - Less than an hour: "Xm" (minutes)
/// - Less than a day: "Xh" (hours)
/// - Less than a month: "Xd" (days)
/// - More than a month: "MMM DD" (month abbreviation and day)
///
/// # Returns
///
/// A SharedString containing the formatted time representation
pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "1m".into(),
};
let now = Local::now();
let duration = now.signed_duration_since(input_time);
match duration {
d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(),
d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()),
d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()),
d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()),
_ => input_time.format("%b %d").to_string(),
}
.into()
}
/// Gets the display name for the room
///
/// If the room has a subject set, that will be used as the display name.
@@ -282,8 +252,8 @@ impl Room {
///
/// # Returns
///
/// A SharedString containing the display name
pub fn display_name(&self, cx: &App) -> SharedString {
/// A string containing the display name
pub fn display_name(&self, cx: &App) -> String {
if let Some(subject) = self.subject.clone() {
subject
} else {
@@ -305,8 +275,8 @@ impl Room {
///
/// # Returns
///
/// A SharedString containing the image path or URL
pub fn display_image(&self, proxy: bool, cx: &App) -> SharedString {
/// A string containing the image path or URL
pub fn display_image(&self, proxy: bool, cx: &App) -> String {
if let Some(picture) = self.picture.as_ref() {
picture.clone()
} else if !self.is_group() {
@@ -325,7 +295,7 @@ impl Room {
}
/// Merge the names of the first two members of the room.
pub(crate) fn merge_name(&self, cx: &App) -> SharedString {
pub(crate) fn merge_name(&self, cx: &App) -> String {
let registry = Registry::read_global(cx);
if self.is_group() {
@@ -346,37 +316,12 @@ impl Room {
name = format!("{}, +{}", name, profiles.len() - 2);
}
name.into()
name
} else {
self.first_member(cx).display_name()
}
}
/// Loads all profiles for this room members from the database
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<Profile>, Error> containing all profiles for this room
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<Vec<Profile>, Error>> {
let public_keys = self.members.clone();
cx.background_spawn(async move {
let database = nostr_client().database();
let mut profiles = vec![];
for public_key in public_keys.into_iter() {
let metadata = database.metadata(public_key).await?.unwrap_or_default();
profiles.push(Profile::new(public_key, metadata));
}
Ok(profiles)
})
}
/// Loads all messages for this room from the database
///
/// # Arguments
@@ -397,22 +342,21 @@ impl Room {
.authors(members.clone())
.pubkeys(members.clone());
let events = client
let events: Vec<Event> = client
.database()
.query(filter)
.await?
.into_iter()
.sorted_by_key(|ev| ev.created_at)
.filter(|ev| ev.compare_pubkeys(&members))
.collect::<Vec<_>>();
.collect();
Ok(events)
})
}
/// Emits a new message signal to the current room
pub fn emit_message(&self, event: Event, cx: &mut Context<Self>) {
cx.emit(RoomSignal::NewMessage(Box::new(event)));
pub fn emit_message(&self, gift_wrap_id: EventId, event: Event, cx: &mut Context<Self>) {
cx.emit(RoomSignal::NewMessage((gift_wrap_id, Box::new(event))));
}
/// Emits a signal to refresh the current room's messages.
@@ -473,7 +417,7 @@ impl Room {
let content = content.to_owned();
let subject = self.subject.clone();
let picture = self.picture.clone();
let public_keys = self.members.clone();
let mut public_keys = self.members.clone();
cx.background_spawn(async move {
let client = nostr_client();
@@ -516,26 +460,25 @@ impl Room {
tags.push(Tag::custom(TagKind::custom("picture"), vec![picture]));
}
let Some((current_user, receivers)) = public_keys.split_last() else {
return Err(anyhow!("Something is wrong. Cannot get receivers list."));
};
// Remove the current public key from the list of receivers
public_keys.retain(|&pk| pk != public_key);
// Stored all send errors
let mut reports = vec![];
for receiver in receivers.iter() {
for receiver in public_keys.into_iter() {
match client
.send_private_msg(*receiver, &content, tags.clone())
.send_private_msg(receiver, &content, tags.clone())
.await
{
Ok(output) => {
reports.push(SendReport::output(*receiver, output));
reports.push(SendReport::output(receiver, output));
}
Err(e) => {
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
reports.push(SendReport::nip17_relays_not_found(*receiver));
reports.push(SendReport::nip17_relays_not_found(receiver));
} else {
reports.push(SendReport::error(*receiver, e.to_string()));
reports.push(SendReport::error(receiver, e.to_string()));
}
}
}
@@ -544,17 +487,17 @@ impl Room {
// Only send a backup message to current user if sent successfully to others
if reports.iter().all(|r| r.is_sent_success()) && backup {
match client
.send_private_msg(*current_user, &content, tags.clone())
.send_private_msg(public_key, &content, tags.clone())
.await
{
Ok(output) => {
reports.push(SendReport::output(*current_user, output));
reports.push(SendReport::output(public_key, output));
}
Err(e) => {
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
reports.push(SendReport::nip17_relays_not_found(*current_user));
reports.push(SendReport::nip17_relays_not_found(public_key));
} else {
reports.push(SendReport::error(*current_user, e.to_string()));
reports.push(SendReport::error(public_key, e.to_string()));
}
}
}