feat: nip4e (#188)

* encryption keys

* .

* .

* move nip4e to device crate

* .

* .

* use i18n for device crate

* refactor

* refactor

* .

* add reset button

* send message with encryption keys

* clean up

* .

* choose signer

* fix

* update i18n

* fix sending
This commit is contained in:
reya
2025-10-26 18:10:40 +07:00
committed by GitHub
parent 83687e5448
commit 15bbe82a87
29 changed files with 1856 additions and 851 deletions

View File

@@ -7,20 +7,57 @@ use anyhow::{anyhow, Error};
use common::display::RenderedProfile;
use common::event::EventUtils;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, SharedUri, Task};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use states::app_state;
use states::constants::SEND_RETRY;
use crate::Registry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)]
pub enum SignerKind {
Encryption,
User,
#[default]
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SendOptions {
pub backup: bool,
pub signer_kind: SignerKind,
}
impl SendOptions {
pub fn new() -> Self {
Self {
backup: true,
signer_kind: SignerKind::default(),
}
}
pub fn backup(&self) -> bool {
self.backup
}
}
impl Default for SendOptions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SendReport {
pub receiver: PublicKey,
pub status: Option<Output<EventId>>,
pub error: Option<SharedString>,
pub on_hold: Option<Event>,
pub relays_not_found: bool,
pub device_not_found: bool,
pub on_hold: Option<Event>,
}
impl SendReport {
@@ -31,18 +68,17 @@ impl SendReport {
error: None,
on_hold: None,
relays_not_found: false,
device_not_found: false,
}
}
pub fn status(mut self, output: Output<EventId>) -> Self {
self.status = Some(output);
self.relays_not_found = false;
self
}
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
self.error = Some(error.into());
self.relays_not_found = false;
self
}
@@ -51,11 +87,16 @@ impl SendReport {
self
}
pub fn not_found(mut self) -> Self {
pub fn relays_not_found(mut self) -> Self {
self.relays_not_found = true;
self
}
pub fn device_not_found(mut self) -> Self {
self.device_not_found = true;
self
}
pub fn is_relay_error(&self) -> bool {
self.error.is_some() || self.relays_not_found
}
@@ -82,6 +123,8 @@ pub enum RoomKind {
Request,
}
type DevicePublicKey = PublicKey;
#[derive(Debug)]
pub struct Room {
pub id: u64,
@@ -89,7 +132,7 @@ pub struct Room {
/// Subject of the room
pub subject: Option<String>,
/// All members of the room
pub members: Vec<PublicKey>,
pub members: HashMap<PublicKey, Option<DevicePublicKey>>,
/// Kind
pub kind: RoomKind,
}
@@ -128,7 +171,11 @@ impl From<&Event> for Room {
let created_at = val.created_at;
// Get the members from the event's tags and event's pubkey
let members = val.all_pubkeys();
let members: HashMap<PublicKey, Option<DevicePublicKey>> = val
.all_pubkeys()
.into_iter()
.map(|public_key| (public_key, None))
.collect();
// Get subject from tags
let subject = val
@@ -152,7 +199,11 @@ impl From<&UnsignedEvent> for Room {
let created_at = val.created_at;
// Get the members from the event's tags and event's pubkey
let members = val.all_pubkeys();
let members: HashMap<PublicKey, Option<DevicePublicKey>> = val
.all_pubkeys()
.into_iter()
.map(|public_key| (public_key, None))
.collect();
// Get subject from tags
let subject = val
@@ -233,8 +284,8 @@ impl Room {
}
/// Returns the members of the room
pub fn members(&self) -> &Vec<PublicKey> {
&self.members
pub fn members(&self) -> Vec<PublicKey> {
self.members.keys().cloned().collect()
}
/// Checks if the room has more than two members (group)
@@ -264,17 +315,17 @@ impl Room {
///
/// This member is always different from the current user.
fn display_member(&self, cx: &App) -> Profile {
let registry = Registry::read_global(cx);
let registry = Registry::global(cx);
let signer_pubkey = registry.read(cx).signer_pubkey();
if let Some(public_key) = registry.signer_pubkey() {
for member in self.members() {
if member != &public_key {
return registry.get_person(member, cx);
}
}
}
let target_member = self
.members
.keys()
.find(|&member| Some(member) != signer_pubkey.as_ref())
.or_else(|| self.members.keys().next())
.expect("Room should have at least one member");
registry.get_person(&self.members[0], cx)
registry.read(cx).get_person(target_member, cx)
}
/// Merge the names of the first two members of the room.
@@ -284,7 +335,7 @@ impl Room {
if self.is_group() {
let profiles: Vec<Profile> = self
.members
.iter()
.keys()
.map(|public_key| registry.get_person(public_key, cx))
.collect();
@@ -305,91 +356,9 @@ impl Room {
}
}
/// Connects to all members's messaging relays
pub fn connect(&self, cx: &App) -> Task<Result<HashMap<PublicKey, Vec<RelayUrl>>, Error>> {
let members = self.members.clone();
cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let mut relays = HashMap::new();
let mut processed = HashSet::new();
for member in members.into_iter() {
if member == public_key {
continue;
};
relays.insert(member, vec![]);
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(member)
.limit(1);
let mut stream = client
.stream_events(filter, Duration::from_secs(10))
.await?;
if let Some(event) = stream.next().await {
if processed.insert(event.id) {
let public_key = event.pubkey;
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
// Check if at least one URL exists
if urls.is_empty() {
continue;
}
// Connect to relays
for url in urls.iter() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
relays.entry(public_key).and_modify(|v| v.extend(urls));
}
}
}
Ok(relays)
})
}
/// Loads all messages for this room from the database
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
let conversation_id = self.id.to_string();
cx.background_spawn(async move {
let client = app_state().client();
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(
SingleLetterTag::lowercase(Alphabet::C),
conversation_id.as_str(),
);
let stored = client.database().query(filter).await?;
let mut messages = Vec::with_capacity(stored.len());
for event in stored {
match UnsignedEvent::from_json(&event.content) {
Ok(rumor) => messages.push(rumor),
Err(e) => log::warn!("Failed to parse stored rumor: {e}"),
}
}
messages.sort_by_key(|message| message.created_at);
Ok(messages)
})
}
/// Emits a new message signal to the current room
pub fn emit_message(&self, gift_wrap_id: EventId, event: UnsignedEvent, cx: &mut Context<Self>) {
cx.emit(RoomSignal::NewMessage((gift_wrap_id, event)));
pub fn emit_message(&self, id: EventId, event: UnsignedEvent, cx: &mut Context<Self>) {
cx.emit(RoomSignal::NewMessage((id, event)));
}
/// Emits a signal to refresh the current room's messages.
@@ -397,9 +366,69 @@ impl Room {
cx.emit(RoomSignal::Refresh);
}
/// Get messaging relays and encryption keys announcement for each member
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
let members = self.members();
cx.background_spawn(async move {
let client = app_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
for member in members.into_iter() {
if member == public_key {
continue;
};
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(member)
.limit(1);
// Subscribe to get members messaging relays
client.subscribe(filter, Some(opts)).await?;
let filter = Filter::new()
.kind(Kind::Custom(10044))
.author(member)
.limit(1);
// Subscribe to get members encryption keys announcement
client.subscribe(filter, Some(opts)).await?;
}
Ok(())
})
}
/// Get all messages belonging to the room
pub fn get_messages(&self, cx: &App) -> Task<Result<Vec<UnsignedEvent>, Error>> {
let conversation_id = self.id.to_string();
cx.background_spawn(async move {
let client = app_state().client();
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
let stored = client.database().query(filter).await?;
let mut messages: Vec<UnsignedEvent> = stored
.into_iter()
.filter_map(|event| UnsignedEvent::from_json(&event.content).ok())
.collect();
messages.sort_by_key(|message| message.created_at);
Ok(messages)
})
}
/// Create a new message event (unsigned)
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent {
let public_key = Registry::read_global(cx).signer_pubkey().unwrap();
let registry = Registry::global(cx);
let public_key = registry.read(cx).signer_pubkey().unwrap();
let subject = self.subject.clone();
let mut tags = vec![];
@@ -407,7 +436,7 @@ impl Room {
// Add receivers
//
// NOTE: current user will be removed from the list of receivers
for member in self.members.iter() {
for (member, _) in self.members.iter() {
tags.push(Tag::public_key(member.to_owned()));
}
@@ -447,34 +476,42 @@ impl Room {
/// Create a task to send a message to all room members
pub fn send_message(
&self,
rumor: UnsignedEvent,
backup: bool,
rumor: &UnsignedEvent,
opts: &SendOptions,
cx: &App,
) -> Task<Result<Vec<SendReport>, Error>> {
let mut members = self.members.clone();
let rumor = rumor.to_owned();
let opts = opts.to_owned();
cx.background_spawn(async move {
let states = app_state();
let client = states.client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let device = states.device.read().await.encryption_keys.clone();
let user_signer = client.signer().await?;
let user_pubkey = user_signer.get_public_key().await?;
// Collect relay hints for all participants (including current user)
let mut participants = members.clone();
if !participants.contains(&public_key) {
participants.push(public_key);
let mut participants: Vec<PublicKey> = members.keys().cloned().collect();
if !participants.contains(&user_pubkey) {
participants.push(user_pubkey);
}
// Initialize relay cache
let mut relay_cache: HashMap<PublicKey, Vec<RelayUrl>> = HashMap::new();
for participant in participants.iter().cloned() {
let urls = Self::messaging_relays(participant).await;
let urls = states.messaging_relays(participant).await;
relay_cache.insert(participant, urls);
}
// Update rumor with relay hints for each receiver
let mut rumor = rumor;
let mut tags_with_hints = Vec::new();
for tag in rumor.tags.to_vec() {
for tag in rumor.tags.into_iter() {
if let Some(standard) = tag.as_standardized().cloned() {
match standard {
TagStandard::PublicKey {
@@ -483,18 +520,18 @@ impl Room {
uppercase,
..
} => {
let relay_url =
relay_cache
.get(&public_key)
.and_then(|urls| urls.first().cloned());
let relay_url = relay_cache
.get(&public_key)
.and_then(|urls| urls.first().cloned());
let updated = TagStandard::PublicKey {
public_key,
relay_url,
alias,
uppercase,
};
tags_with_hints
.push(Tag::from_standardized_without_cell(updated));
tags_with_hints.push(Tag::from_standardized_without_cell(updated));
}
_ => tags_with_hints.push(tag),
}
@@ -506,29 +543,42 @@ impl Room {
// Remove the current user's public key from the list of receivers
// Current user will be handled separately
members.retain(|&pk| pk != public_key);
let (public_key, device_pubkey) = members.remove_entry(&user_pubkey).unwrap();
// Determine the signer will be used based on the provided options
let signer = Self::select_signer(&opts.signer_kind, device, user_signer)?;
// Collect the send reports
let mut reports: Vec<SendReport> = vec![];
for receiver in members.into_iter() {
let rumor = rumor.clone();
let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?;
for (receiver, device_pubkey) in members.into_iter() {
let urls = relay_cache.get(&receiver).cloned().unwrap_or_default();
// Check if there are any relays to send the event to
// Check if there are any relays to send the message to
if urls.is_empty() {
reports.push(SendReport::new(receiver).not_found());
reports.push(SendReport::new(receiver).relays_not_found());
continue;
}
// Skip sending if using encryption keys but device not found
if device_pubkey.is_none() && matches!(opts.signer_kind, SignerKind::Encryption) {
reports.push(SendReport::new(receiver).device_not_found());
continue;
}
// Determine the receiver based on the signer kind
let rumor = rumor.clone();
let target = Self::select_receiver(&opts.signer_kind, receiver, device_pubkey);
let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?;
// Send the event to the messaging relays
match client.send_event_to(urls, &event).await {
Ok(output) => {
let id = output.id().to_owned();
let auth_required = output.failed.iter().any(|m| m.1.starts_with("auth-"));
let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-"));
let report = SendReport::new(receiver).status(output);
if auth_required {
if auth {
// Wait for authenticated and resent event successfully
for attempt in 0..=SEND_RETRY {
let retry_manager = states.tracker().read().await;
@@ -561,15 +611,16 @@ impl Room {
// Construct a gift wrap to back up to current user's owned messaging relays
let rumor = rumor.clone();
let event = EventBuilder::gift_wrap(&signer, &public_key, rumor, vec![]).await?;
let target = Self::select_receiver(&opts.signer_kind, public_key, device_pubkey);
let event = EventBuilder::gift_wrap(&signer, &target, rumor, vec![]).await?;
// Only send a backup message to current user if sent successfully to others
if reports.iter().all(|r| r.is_sent_success()) && backup {
if opts.backup() && reports.iter().all(|r| r.is_sent_success()) {
let urls = relay_cache.get(&public_key).cloned().unwrap_or_default();
// Check if there are any relays to send the event to
if urls.is_empty() {
reports.push(SendReport::new(public_key).not_found());
reports.push(SendReport::new(public_key).relays_not_found());
} else {
// Send the event to the messaging relays
match client.send_event_to(urls, &event).await {
@@ -596,7 +647,8 @@ impl Room {
cx: &App,
) -> Task<Result<Vec<SendReport>, Error>> {
cx.background_spawn(async move {
let client = app_state().client();
let states = app_state();
let client = states.client();
let mut resend_reports = vec![];
for report in reports.into_iter() {
@@ -625,11 +677,11 @@ impl Room {
// Process the on hold event if it exists
if let Some(event) = report.on_hold {
let urls = Self::messaging_relays(receiver).await;
let urls = states.messaging_relays(receiver).await;
// Check if there are any relays to send the event to
if urls.is_empty() {
resend_reports.push(SendReport::new(receiver).not_found());
resend_reports.push(SendReport::new(receiver).relays_not_found());
} else {
// Send the event to the messaging relays
match client.send_event_to(urls, &event).await {
@@ -648,36 +700,24 @@ impl Room {
})
}
/// Gets messaging relays for public key
async fn messaging_relays(public_key: PublicKey) -> Vec<RelayUrl> {
let client = app_state().client();
let mut relay_urls = vec![];
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
if let Ok(events) = client.database().query(filter).await {
if let Some(event) = events.first_owned() {
let urls: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
// Connect to relays
for url in urls.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
relay_urls.extend(urls.into_iter().take(3).unique());
fn select_signer<T>(kind: &SignerKind, device: Option<T>, user: T) -> Result<T, Error>
where
T: NostrSigner,
{
match kind {
SignerKind::Encryption => {
Ok(device.ok_or_else(|| anyhow!("No encryption keys found"))?)
}
SignerKind::User => Ok(user),
SignerKind::Auto => Ok(device.unwrap_or(user)),
}
}
relay_urls
fn select_receiver(kind: &SignerKind, user: PublicKey, device: Option<PublicKey>) -> PublicKey {
match kind {
SignerKind::Encryption => device.unwrap(),
SignerKind::User => user,
SignerKind::Auto => device.unwrap_or(user),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
}