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

@@ -12,16 +12,13 @@ settings = { path = "../settings" }
gpui.workspace = true
nostr.workspace = true
nostr-sdk.workspace = true
nostr-lmdb.workspace = true
anyhow.workspace = true
itertools.workspace = true
smallvec.workspace = true
smol.workspace = true
log.workspace = true
flume.workspace = true
futures.workspace = true
serde.workspace = true
serde_json.workspace = true
fuzzy-matcher = "0.3.7"
rustls = "0.23.23"

View File

@@ -1,191 +0,0 @@
use std::any::Any;
use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use anyhow::Result;
use futures::FutureExt as _;
use gpui::AsyncApp;
use states::paths::config_dir;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyItem {
User,
Bunker,
Client,
Encryption,
}
impl Display for KeyItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::User => write!(f, "coop-user"),
Self::Bunker => write!(f, "coop-bunker"),
Self::Client => write!(f, "coop-client"),
Self::Encryption => write!(f, "coop-encryption"),
}
}
}
impl From<KeyItem> for String {
fn from(item: KeyItem) -> Self {
item.to_string()
}
}
pub trait KeyStore: Any + Send + Sync {
fn name(&self) -> &str;
/// Reads the credentials from the provider.
#[allow(clippy::type_complexity)]
fn read_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>>;
/// Writes the credentials to the provider.
fn write_credentials<'a>(
&'a self,
url: &'a str,
username: &'a str,
password: &'a [u8],
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
/// Deletes the credentials from the provider.
fn delete_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
}
/// A credentials provider that stores credentials in the system keychain.
pub struct KeyringProvider;
impl KeyStore for KeyringProvider {
fn name(&self) -> &str {
"keyring"
}
fn read_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
async move { cx.update(|cx| cx.read_credentials(url))?.await }.boxed_local()
}
fn write_credentials<'a>(
&'a self,
url: &'a str,
username: &'a str,
password: &'a [u8],
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
cx.update(move |cx| cx.write_credentials(url, username, password))?
.await
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move { cx.update(move |cx| cx.delete_credentials(url))?.await }.boxed_local()
}
}
/// A credentials provider that stores credentials in a local file.
pub struct FileProvider {
path: PathBuf,
}
impl FileProvider {
pub fn new() -> Self {
let path = config_dir().join(".keys");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
Self { path }
}
pub fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
let json = std::fs::read(&self.path)?;
let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
Ok(credentials)
}
pub fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
let json = serde_json::to_string(credentials)?;
std::fs::write(&self.path, json)?;
Ok(())
}
}
impl Default for FileProvider {
fn default() -> Self {
Self::new()
}
}
impl KeyStore for FileProvider {
fn name(&self) -> &str {
"file"
}
fn read_credentials<'a>(
&'a self,
url: &'a str,
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
async move {
Ok(self
.load_credentials()
.unwrap_or_default()
.get(url)
.cloned())
}
.boxed_local()
}
fn write_credentials<'a>(
&'a self,
url: &'a str,
username: &'a str,
password: &'a [u8],
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
let mut credentials = self.load_credentials().unwrap_or_default();
credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
self.save_credentials(&credentials)
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
url: &'a str,
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
let mut credentials = self.load_credentials()?;
credentials.remove(url);
self.save_credentials(&credentials)
}
.boxed_local()
}
}

View File

@@ -1,6 +1,5 @@
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, LazyLock};
use anyhow::Error;
use common::event::EventUtils;
@@ -14,19 +13,12 @@ use room::RoomKind;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use states::app_state;
use states::constants::KEYRING_URL;
use states::state::UnwrappingStatus;
use crate::keystore::{FileProvider, KeyStore, KeyringProvider};
use crate::room::Room;
pub mod keystore;
pub mod message;
pub mod room;
pub static DISABLE_KEYRING: LazyLock<bool> =
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
pub fn init(cx: &mut App) {
Registry::set_global(cx.new(Registry::new), cx);
}
@@ -49,14 +41,8 @@ pub struct Registry {
/// Collection of all persons (user profiles)
pub persons: HashMap<PublicKey, Entity<Profile>>,
/// Status of the unwrapping process
pub unwrapping_status: Entity<UnwrappingStatus>,
/// Key Store for storing credentials
pub keystore: Arc<dyn KeyStore>,
/// Whether the keystore has been initialized
pub initialized_keystore: bool,
/// Loading status of the registry
pub loading: bool,
/// Public Key of the currently activated signer
signer_pubkey: Option<PublicKey>,
@@ -85,39 +71,8 @@ impl Registry {
/// Create a new registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let unwrapping_status = cx.new(|_| UnwrappingStatus::default());
let read_credential = cx.read_credentials(KEYRING_URL);
let initialized_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING;
let keystore: Arc<dyn KeyStore> = if cfg!(debug_assertions) || *DISABLE_KEYRING {
Arc::new(FileProvider::default())
} else {
Arc::new(KeyringProvider)
};
let mut tasks = smallvec![];
if !(cfg!(debug_assertions) || *DISABLE_KEYRING) {
tasks.push(
// Verify the keyring access
cx.spawn(async move |this, cx| {
let result = read_credential.await;
this.update(cx, |this, cx| {
if let Err(e) = result {
log::error!("Keyring error: {e}");
// For Linux:
// The user has not installed secret service on their system
// Fall back to the file provider
this.keystore = Arc::new(FileProvider::default());
}
this.initialized_keystore = true;
cx.notify();
})
.ok();
}),
);
}
tasks.push(
// Load all user profiles from the database
cx.spawn(async move |this, cx| {
@@ -136,12 +91,10 @@ impl Registry {
);
Self {
unwrapping_status,
keystore,
initialized_keystore,
rooms: vec![],
persons: HashMap::new(),
signer_pubkey: None,
loading: true,
_tasks: tasks,
}
}
@@ -165,16 +118,6 @@ impl Registry {
})
}
/// Returns the keystore.
pub fn keystore(&self) -> Arc<dyn KeyStore> {
Arc::clone(&self.keystore)
}
/// Returns true if the keystore is a file keystore.
pub fn is_using_file_keystore(&self) -> bool {
self.keystore.name() == "file"
}
/// Returns the public key of the currently activated signer.
pub fn signer_pubkey(&self) -> Option<PublicKey> {
self.signer_pubkey
@@ -233,6 +176,11 @@ impl Registry {
}
}
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading;
cx.notify();
}
/// Get a room by its ID.
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
self.rooms
@@ -297,24 +245,13 @@ impl Registry {
pub fn search_by_public_key(&self, public_key: PublicKey, cx: &App) -> Vec<Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).members.contains(&public_key))
.filter(|room| room.read(cx).members.contains_key(&public_key))
.cloned()
.collect()
}
/// Set the loading status of the registry.
pub fn set_unwrapping_status(&mut self, status: UnwrappingStatus, cx: &mut Context<Self>) {
self.unwrapping_status.update(cx, |this, cx| {
*this = status;
cx.notify();
});
}
/// Reset the registry.
pub fn reset(&mut self, cx: &mut Context<Self>) {
// Reset the unwrapping status
self.set_unwrapping_status(UnwrappingStatus::default(), cx);
// Clear the current identity
self.signer_pubkey = None;
@@ -339,10 +276,7 @@ impl Registry {
let authored_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.custom_tag(
SingleLetterTag::lowercase(Alphabet::A),
public_key.to_hex(),
);
.custom_tag(SingleLetterTag::lowercase(Alphabet::A), public_key);
let addressed_filter = Filter::new()
.kind(Kind::ApplicationSpecificData)

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::*;
}