chore: simplify chat send reports (#55)
Reviewed-on: #55
This commit was merged in pull request #55.
This commit is contained in:
Generated
-2
@@ -1097,8 +1097,6 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"chat",
|
"chat",
|
||||||
"common",
|
"common",
|
||||||
"flume 0.11.1",
|
|
||||||
"futures",
|
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
+17
-26
@@ -14,7 +14,7 @@ use gpui::{
|
|||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner};
|
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP};
|
||||||
|
|
||||||
mod message;
|
mod message;
|
||||||
mod room;
|
mod room;
|
||||||
@@ -73,41 +73,29 @@ impl Signal {
|
|||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
/// Chat rooms
|
/// Chat rooms
|
||||||
rooms: Vec<Entity<Room>>,
|
rooms: Vec<Entity<Room>>,
|
||||||
|
|
||||||
/// O(1) room lookup by room ID
|
/// O(1) room lookup by room ID
|
||||||
room_index: HashMap<u64, Entity<Room>>,
|
room_index: HashMap<u64, Entity<Room>>,
|
||||||
|
|
||||||
/// Events that failed to unwrap for any reason
|
/// Events that failed to unwrap for any reason
|
||||||
trash: Entity<BTreeSet<FailedMessage>>,
|
trash: Entity<BTreeSet<FailedMessage>>,
|
||||||
|
|
||||||
/// Tracking events seen on which relays in the current session
|
/// Tracking events seen on which relays in the current session
|
||||||
seen: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>,
|
seen: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>,
|
||||||
|
|
||||||
/// Mapping of unwrapped event ids to their gift wrap event ids
|
/// Mapping of unwrapped event ids to their gift wrap event ids
|
||||||
event_map: Arc<RwLock<HashMap<EventId, EventId>>>,
|
event_map: Arc<RwLock<HashMap<EventId, EventId>>>,
|
||||||
|
|
||||||
/// True while the initial event backlog is still loading
|
/// True while the initial event backlog is still loading
|
||||||
tracking: Arc<AtomicBool>,
|
tracking: Arc<AtomicBool>,
|
||||||
|
|
||||||
/// Channel for sending signals to the UI.
|
/// Channel for sending signals to the UI.
|
||||||
signal_tx: flume::Sender<Signal>,
|
signal_tx: flume::Sender<Signal>,
|
||||||
|
|
||||||
/// Channel for receiving signals from the UI.
|
/// Channel for receiving signals from the UI.
|
||||||
signal_rx: flume::Receiver<Signal>,
|
signal_rx: flume::Receiver<Signal>,
|
||||||
|
|
||||||
/// Async tasks
|
/// Async tasks
|
||||||
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
||||||
|
|
||||||
/// Notification listener task (cancelled on signer change)
|
/// Notification listener task (cancelled on signer change)
|
||||||
notification_listener: Option<Task<Result<(), Error>>>,
|
notification_listener: Option<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
/// Signal consumer task (cancelled on signer change)
|
/// Signal consumer task (cancelled on signer change)
|
||||||
signal_consumer: Option<Task<Result<(), Error>>>,
|
signal_consumer: Option<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
/// Fuzzy matcher for room search (cached; intentionally excluded from Debug)
|
/// Fuzzy matcher for room search (cached; intentionally excluded from Debug)
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
matcher: CachedMatcher,
|
matcher: CachedMatcher,
|
||||||
|
|
||||||
/// Subscriptions
|
/// Subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
}
|
}
|
||||||
@@ -144,8 +132,9 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
/// Create a new chat registry instance
|
/// Create a new chat registry instance
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let entity = cx.entity().downgrade();
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let entity = cx.entity().downgrade();
|
||||||
|
|
||||||
let (tx, rx) = flume::unbounded::<Signal>();
|
let (tx, rx) = flume::unbounded::<Signal>();
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
@@ -162,11 +151,9 @@ impl ChatRegistry {
|
|||||||
);
|
);
|
||||||
|
|
||||||
cx.defer(move |cx| {
|
cx.defer(move |cx| {
|
||||||
entity
|
let _ = entity.update(cx, |this, cx| {
|
||||||
.update(cx, |this, cx| {
|
this.get_rooms(cx);
|
||||||
this.get_rooms(cx);
|
});
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -237,6 +224,7 @@ impl ChatRegistry {
|
|||||||
if processed_events.len() >= MAX_PROCESSED {
|
if processed_events.len() >= MAX_PROCESSED {
|
||||||
processed_events.clear();
|
processed_events.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !processed_events.insert(event.id) {
|
if !processed_events.insert(event.id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -752,11 +740,14 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Unwraps a gift-wrapped event and processes its contents.
|
/// Unwraps a gift-wrapped event and processes its contents.
|
||||||
async fn extract_rumor(
|
async fn extract_rumor<S>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &UniversalSigner,
|
signer: &S,
|
||||||
gift_wrap: &Event,
|
gift_wrap: &Event,
|
||||||
) -> Result<UnsignedEvent, Error> {
|
) -> Result<UnsignedEvent, Error>
|
||||||
|
where
|
||||||
|
S: AsyncNip44 + ?Sized,
|
||||||
|
{
|
||||||
// Try to get cached rumor first
|
// Try to get cached rumor first
|
||||||
if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
|
if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
|
||||||
return Ok(rumor);
|
return Ok(rumor);
|
||||||
@@ -783,10 +774,10 @@ async fn extract_rumor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to unwrap a gift wrap event with a given signer.
|
/// Attempts to unwrap a gift wrap event with a given signer.
|
||||||
async fn try_unwrap_with(
|
async fn try_unwrap_with<S>(signer: &S, gift_wrap: &Event) -> Result<UnwrappedGift, Error>
|
||||||
signer: &UniversalSigner,
|
where
|
||||||
gift_wrap: &Event,
|
S: AsyncNip44 + ?Sized,
|
||||||
) -> Result<UnwrappedGift, Error> {
|
{
|
||||||
// Get the sealed event
|
// Get the sealed event
|
||||||
let seal = signer
|
let seal = signer
|
||||||
.nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content)
|
.nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content)
|
||||||
|
|||||||
+125
-166
@@ -10,92 +10,45 @@ use itertools::Itertools;
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry};
|
use person::{Person, PersonRegistry};
|
||||||
use settings::{RoomConfig, SignerKind};
|
use settings::{RoomConfig, SignerKind};
|
||||||
use state::{NostrRegistry, TIMEOUT, UniversalSigner};
|
use state::{NostrRegistry, TIMEOUT};
|
||||||
|
|
||||||
use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage};
|
use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage};
|
||||||
|
|
||||||
const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet.";
|
const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet.";
|
||||||
const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key or it's not available.";
|
const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key or it's not available.";
|
||||||
|
|
||||||
|
/// The outcome of delivering an encrypted rumor to a single receiver.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SendReport {
|
pub struct SendReport {
|
||||||
|
/// The receiver the rumor was addressed to.
|
||||||
pub receiver: PublicKey,
|
pub receiver: PublicKey,
|
||||||
pub gift_wrap_id: Option<EventId>,
|
/// The per-relay send output, or the error that prevented the send.
|
||||||
pub error: Option<SharedString>,
|
result: Result<Output<EventId, EventSendStatus>, SharedString>,
|
||||||
pub output: Option<Output<EventId, EventSendStatus>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SendReport {
|
impl SendReport {
|
||||||
pub fn new(receiver: PublicKey) -> Self {
|
/// The error that prevented the send, if any.
|
||||||
Self {
|
pub fn error(&self) -> Option<&SharedString> {
|
||||||
receiver,
|
self.result.as_ref().err()
|
||||||
gift_wrap_id: None,
|
|
||||||
error: None,
|
|
||||||
output: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the gift wrap ID.
|
/// The per-relay send output, if the gift wrap was dispatched.
|
||||||
pub fn gift_wrap_id(mut self, gift_wrap_id: EventId) -> Self {
|
pub fn output(&self) -> Option<&Output<EventId, EventSendStatus>> {
|
||||||
self.gift_wrap_id = Some(gift_wrap_id);
|
self.result.as_ref().ok()
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the output.
|
/// Whether at least one relay accepted the event.
|
||||||
pub fn output(mut self, output: Output<EventId, EventSendStatus>) -> Self {
|
|
||||||
self.output = Some(output);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the error message.
|
|
||||||
pub fn error<T>(mut self, error: T) -> Self
|
|
||||||
where
|
|
||||||
T: Into<SharedString>,
|
|
||||||
{
|
|
||||||
self.error = Some(error.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the send is pending.
|
|
||||||
pub fn pending(&self) -> bool {
|
|
||||||
self.error.is_none()
|
|
||||||
&& self
|
|
||||||
.output
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|o| o.success.is_empty() && o.failed.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the send was successful.
|
|
||||||
pub fn success(&self) -> bool {
|
pub fn success(&self) -> bool {
|
||||||
self.error.is_none() && self.output.as_ref().is_some_and(|o| !o.success.is_empty())
|
self.output()
|
||||||
|
.is_some_and(|output| !output.success.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the send failed.
|
/// Whether no relay accepted the event.
|
||||||
pub fn failed(&self) -> bool {
|
pub fn failed(&self) -> bool {
|
||||||
self.error.is_some() && self.output.as_ref().is_some_and(|o| !o.failed.is_empty())
|
match &self.result {
|
||||||
}
|
Err(_) => true,
|
||||||
}
|
Ok(output) => output.success.is_empty(),
|
||||||
|
}
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum SendStatus {
|
|
||||||
Ok {
|
|
||||||
id: EventId,
|
|
||||||
relay: RelayUrl,
|
|
||||||
},
|
|
||||||
Failed {
|
|
||||||
id: EventId,
|
|
||||||
relay: RelayUrl,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SendStatus {
|
|
||||||
pub fn ok(id: EventId, relay: RelayUrl) -> Self {
|
|
||||||
Self::Ok { id, relay }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn failed(id: EventId, relay: RelayUrl, message: String) -> Self {
|
|
||||||
Self::Failed { id, relay, message }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,30 +473,28 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Select the appropriate signer based on signer kind and available keys.
|
/// Select the appropriate signer based on signer kind and available keys.
|
||||||
fn select_signer(
|
fn select_signer<'a, S>(
|
||||||
signer_kind: &SignerKind,
|
signer_kind: &SignerKind,
|
||||||
has_announcement: bool,
|
has_announcement: bool,
|
||||||
encryption_signer: &Option<UniversalSigner>,
|
encryption_signer: &'a Option<S>,
|
||||||
user_signer: &UniversalSigner,
|
user_signer: &'a S,
|
||||||
) -> UniversalSigner {
|
) -> &'a S {
|
||||||
match signer_kind {
|
match signer_kind {
|
||||||
SignerKind::Auto => {
|
SignerKind::Auto => {
|
||||||
if has_announcement {
|
if has_announcement {
|
||||||
encryption_signer
|
encryption_signer.as_ref().unwrap_or(user_signer)
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| user_signer.clone())
|
|
||||||
} else {
|
} else {
|
||||||
user_signer.clone()
|
user_signer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SignerKind::Encryption => encryption_signer
|
SignerKind::Encryption => encryption_signer
|
||||||
.clone()
|
.as_ref()
|
||||||
.expect("encryption signer must be set"),
|
.expect("encryption signer must be set"),
|
||||||
SignerKind::User => user_signer.clone(),
|
SignerKind::User => user_signer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send rumor event to all members's messaging relays
|
/// Send the rumor to every member's messaging relays.
|
||||||
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
|
||||||
let config = self.config.clone();
|
let config = self.config.clone();
|
||||||
|
|
||||||
@@ -560,7 +511,7 @@ impl Room {
|
|||||||
let sender = persons.read(cx).get(¤t_user, cx);
|
let sender = persons.read(cx).get(¤t_user, cx);
|
||||||
|
|
||||||
// Get all members (excluding sender)
|
// Get all members (excluding sender)
|
||||||
let members: Vec<Person> = self
|
let receivers: Vec<Person> = self
|
||||||
.members
|
.members
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|public_key| public_key != &&sender.public_key())
|
.filter(|public_key| public_key != &&sender.public_key())
|
||||||
@@ -569,71 +520,33 @@ impl Room {
|
|||||||
|
|
||||||
Some(cx.background_spawn(async move {
|
Some(cx.background_spawn(async move {
|
||||||
let signer_kind = config.signer_kind();
|
let signer_kind = config.signer_kind();
|
||||||
let backup = config.backup();
|
|
||||||
|
|
||||||
let mut sents = 0;
|
// Deliver to every receiver concurrently
|
||||||
let mut reports = Vec::new();
|
let sends = receivers.into_iter().map(|receiver| {
|
||||||
|
deliver(
|
||||||
// Process each member
|
&client,
|
||||||
for member in members {
|
|
||||||
let announcement = member.announcement();
|
|
||||||
let public_key = member.public_key();
|
|
||||||
|
|
||||||
// Handle encryption signer requirements
|
|
||||||
if signer_kind.encryption() {
|
|
||||||
// Receiver didn't set up a decoupled encryption key
|
|
||||||
if announcement.is_none() {
|
|
||||||
reports.push(SendReport::new(public_key).error(NO_DEKEY));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender didn't set up a decoupled encryption key
|
|
||||||
if encryption_signer.is_none() {
|
|
||||||
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine the signer to use
|
|
||||||
let signer = Self::select_signer(
|
|
||||||
signer_kind,
|
signer_kind,
|
||||||
announcement.is_some(),
|
receiver,
|
||||||
|
&rumor,
|
||||||
&encryption_signer,
|
&encryption_signer,
|
||||||
&user_signer,
|
&user_signer,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let mut reports: Vec<SendReport> = futures::future::join_all(sends).await;
|
||||||
|
|
||||||
|
// Back up the message to ourselves once at least one receiver got it
|
||||||
|
if config.backup() && reports.iter().any(SendReport::success) {
|
||||||
|
reports.push(
|
||||||
|
deliver(
|
||||||
|
&client,
|
||||||
|
signer_kind,
|
||||||
|
sender,
|
||||||
|
&rumor,
|
||||||
|
&encryption_signer,
|
||||||
|
&user_signer,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send the gift wrap event and collect the report
|
|
||||||
match send_gift_wrap(&client, &signer, &member, &rumor, signer_kind).await {
|
|
||||||
Ok(report) => {
|
|
||||||
reports.push(report);
|
|
||||||
sents += 1;
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
let report = SendReport::new(public_key).error(error.to_string());
|
|
||||||
reports.push(report);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send backup to current user if needed
|
|
||||||
if backup && sents >= 1 {
|
|
||||||
let public_key = sender.public_key();
|
|
||||||
|
|
||||||
// Determine the signer to use
|
|
||||||
let signer = Self::select_signer(
|
|
||||||
signer_kind,
|
|
||||||
sender.announcement().is_some(),
|
|
||||||
&encryption_signer,
|
|
||||||
&user_signer,
|
|
||||||
);
|
|
||||||
|
|
||||||
match send_gift_wrap(&client, &signer, &sender, &rumor, signer_kind).await {
|
|
||||||
Ok(report) => reports.push(report),
|
|
||||||
Err(error) => {
|
|
||||||
let report = SendReport::new(public_key).error(error.to_string());
|
|
||||||
reports.push(report);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reports
|
reports
|
||||||
@@ -641,55 +554,101 @@ impl Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to send a gift-wrapped event
|
/// Deliver a rumor to a single receiver.
|
||||||
async fn send_gift_wrap(
|
async fn deliver<S>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
signer: &UniversalSigner,
|
signer_kind: &SignerKind,
|
||||||
|
receiver: Person,
|
||||||
|
rumor: &UnsignedEvent,
|
||||||
|
encryption_signer: &Option<S>,
|
||||||
|
user_signer: &S,
|
||||||
|
) -> SendReport
|
||||||
|
where
|
||||||
|
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
|
||||||
|
{
|
||||||
|
let public_key = receiver.public_key();
|
||||||
|
let announcement = receiver.announcement();
|
||||||
|
|
||||||
|
if signer_kind.encryption() {
|
||||||
|
// Receiver didn't set up a decoupled encryption key
|
||||||
|
if announcement.is_none() {
|
||||||
|
return SendReport {
|
||||||
|
receiver: public_key,
|
||||||
|
result: Err(NO_DEKEY.into()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sender didn't set up a decoupled encryption key
|
||||||
|
if encryption_signer.is_none() {
|
||||||
|
return SendReport {
|
||||||
|
receiver: public_key,
|
||||||
|
result: Err(USER_NO_DEKEY.into()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the signer to use
|
||||||
|
let signer = Room::select_signer(
|
||||||
|
signer_kind,
|
||||||
|
announcement.is_some(),
|
||||||
|
encryption_signer,
|
||||||
|
user_signer,
|
||||||
|
);
|
||||||
|
|
||||||
|
match send_gift_wrap(client, signer, &receiver, rumor, signer_kind).await {
|
||||||
|
Ok(output) => SendReport {
|
||||||
|
receiver: public_key,
|
||||||
|
result: Ok(output),
|
||||||
|
},
|
||||||
|
Err(error) => SendReport {
|
||||||
|
receiver: public_key,
|
||||||
|
result: Err(error.to_string().into()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the gift-wrapped event for a rumor and send it.
|
||||||
|
async fn send_gift_wrap<S>(
|
||||||
|
client: &Client,
|
||||||
|
signer: &S,
|
||||||
receiver: &Person,
|
receiver: &Person,
|
||||||
rumor: &UnsignedEvent,
|
rumor: &UnsignedEvent,
|
||||||
config: &SignerKind,
|
config: &SignerKind,
|
||||||
) -> Result<SendReport, Error> {
|
) -> Result<Output<EventId, EventSendStatus>, Error>
|
||||||
let k_tag = Tag::custom("k", [rumor.kind.to_string()]);
|
where
|
||||||
let mut extra_tags = vec![k_tag];
|
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
|
||||||
|
{
|
||||||
|
let mut extra_tags = vec![Tag::custom("k", [rumor.kind.to_string()])];
|
||||||
|
|
||||||
// Determine the receiver public key based on the config
|
// Determine the receiver public key based on the config
|
||||||
let receiver = match config {
|
let receiver_key = match config {
|
||||||
SignerKind::Auto => {
|
SignerKind::Auto => match receiver.announcement().as_ref() {
|
||||||
if let Some(announcement) = receiver.announcement().as_ref() {
|
Some(announcement) => {
|
||||||
extra_tags.push(Tag::public_key(receiver.public_key()));
|
extra_tags.push(Tag::public_key(receiver.public_key()));
|
||||||
announcement.public_key()
|
announcement.public_key()
|
||||||
} else {
|
|
||||||
receiver.public_key()
|
|
||||||
}
|
}
|
||||||
}
|
None => receiver.public_key(),
|
||||||
SignerKind::Encryption => {
|
},
|
||||||
if let Some(announcement) = receiver.announcement().as_ref() {
|
SignerKind::Encryption => match receiver.announcement().as_ref() {
|
||||||
|
Some(announcement) => {
|
||||||
extra_tags.push(Tag::public_key(receiver.public_key()));
|
extra_tags.push(Tag::public_key(receiver.public_key()));
|
||||||
announcement.public_key()
|
announcement.public_key()
|
||||||
} else {
|
|
||||||
return Err(anyhow!("User has no encryption announcement"));
|
|
||||||
}
|
}
|
||||||
}
|
None => return Err(anyhow!("User has no encryption announcement")),
|
||||||
|
},
|
||||||
SignerKind::User => receiver.public_key(),
|
SignerKind::User => receiver.public_key(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construct the gift wrap event
|
// Construct the gift wrap event
|
||||||
let event = nip59::GiftWrapBuilder::new(receiver, rumor.clone())
|
let event = nip59::GiftWrapBuilder::new(receiver_key, rumor.clone())
|
||||||
.extra_tags(extra_tags)
|
.extra_tags(extra_tags)
|
||||||
.finalize_async(signer)
|
.finalize_async(signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Send the gift wrap event and collect the report
|
// Send to the receiver's NIP-17 relays.
|
||||||
let report = client
|
client
|
||||||
.send_event(&event)
|
.send_event(&event)
|
||||||
.to_nip17()
|
.to_nip17()
|
||||||
.ack_policy(AckPolicy::none())
|
|
||||||
.await
|
.await
|
||||||
.map(|output| {
|
.map_err(Into::into)
|
||||||
SendReport::new(receiver)
|
|
||||||
.gift_wrap_id(event.id)
|
|
||||||
.output(output)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(report)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ nostr-sdk.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
flume.workspace = true
|
|
||||||
futures.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ use std::sync::{Arc, LazyLock, RwLock};
|
|||||||
|
|
||||||
pub use actions::*;
|
pub use actions::*;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus};
|
use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport};
|
||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
use futures::lock::Mutex;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
||||||
@@ -95,9 +94,6 @@ pub struct ChatPanel {
|
|||||||
/// Subject bar visibility
|
/// Subject bar visibility
|
||||||
subject_bar: Entity<bool>,
|
subject_bar: Entity<bool>,
|
||||||
|
|
||||||
/// Sent message ids
|
|
||||||
sent_ids: Arc<Mutex<Vec<EventId>>>,
|
|
||||||
|
|
||||||
/// Replies to
|
/// Replies to
|
||||||
replies_to: Entity<HashSet<EventId>>,
|
replies_to: Entity<HashSet<EventId>>,
|
||||||
|
|
||||||
@@ -181,7 +177,6 @@ impl ChatPanel {
|
|||||||
// Define all functions that will run after the current cycle
|
// Define all functions that will run after the current cycle
|
||||||
cx.defer_in(window, |this, window, cx| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
this.connect(cx);
|
this.connect(cx);
|
||||||
this.handle_notifications(cx);
|
|
||||||
this.subscribe_room_events(window, cx);
|
this.subscribe_room_events(window, cx);
|
||||||
this.get_messages(window, cx);
|
this.get_messages(window, cx);
|
||||||
});
|
});
|
||||||
@@ -203,7 +198,6 @@ impl ChatPanel {
|
|||||||
decrypted_files: HashMap::new(),
|
decrypted_files: HashMap::new(),
|
||||||
rendered_texts_by_id: BTreeMap::new(),
|
rendered_texts_by_id: BTreeMap::new(),
|
||||||
reports_by_id,
|
reports_by_id,
|
||||||
sent_ids: Arc::new(Mutex::new(Vec::new())),
|
|
||||||
uploading: false,
|
uploading: false,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
@@ -218,78 +212,6 @@ impl ChatPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle nostr notifications
|
|
||||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let sent_ids = self.sent_ids.clone();
|
|
||||||
let reports = self.reports_by_id.clone();
|
|
||||||
|
|
||||||
let (tx, rx) = flume::bounded::<Arc<SendStatus>>(256);
|
|
||||||
|
|
||||||
self.tasks.push(cx.background_spawn(async move {
|
|
||||||
let mut notifications = client.notifications();
|
|
||||||
|
|
||||||
while let Some(notification) = notifications.next().await {
|
|
||||||
if let ClientNotification::Message { message, relay_url } = notification
|
|
||||||
&& let RelayMessage::Ok {
|
|
||||||
event_id,
|
|
||||||
status,
|
|
||||||
message,
|
|
||||||
} = *message
|
|
||||||
{
|
|
||||||
let sent_ids = sent_ids.lock().await;
|
|
||||||
|
|
||||||
if sent_ids.contains(&event_id) {
|
|
||||||
let status = if status {
|
|
||||||
SendStatus::ok(event_id, relay_url)
|
|
||||||
} else {
|
|
||||||
SendStatus::failed(event_id, relay_url, message.into())
|
|
||||||
};
|
|
||||||
tx.send_async(Arc::new(status)).await.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
while let Ok(status) = rx.recv_async().await {
|
|
||||||
{
|
|
||||||
let mut map = reports.write().unwrap();
|
|
||||||
let status_id = match &*status {
|
|
||||||
SendStatus::Ok { id, .. } => *id,
|
|
||||||
SendStatus::Failed { id, .. } => *id,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the matching report and update it (exit early on first match)
|
|
||||||
'outer: for reports_list in map.values_mut() {
|
|
||||||
for report in reports_list.iter_mut() {
|
|
||||||
let Some(output) = report.output.as_mut() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if *output.id() != status_id {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match &*status {
|
|
||||||
SendStatus::Ok { relay, .. } => {
|
|
||||||
output.success.insert(relay.clone(), EventSendStatus::Sent);
|
|
||||||
}
|
|
||||||
SendStatus::Failed { relay, message, .. } => {
|
|
||||||
output.failed.insert(relay.clone(), message.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break 'outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.update(cx, |_, cx| cx.notify()).ok();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribe to room events
|
/// Subscribe to room events
|
||||||
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(room) = self.room.upgrade() else {
|
let Some(room) = self.room.upgrade() else {
|
||||||
@@ -513,7 +435,6 @@ impl ChatPanel {
|
|||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let id = rumor.id.expect("rumor must have an id");
|
let id = rumor.id.expect("rumor must have an id");
|
||||||
let sent_ids = self.sent_ids.clone();
|
|
||||||
|
|
||||||
// Insert optimistic message and clear input
|
// Insert optimistic message and clear input
|
||||||
if rumor.kind != Kind::Reaction {
|
if rumor.kind != Kind::Reaction {
|
||||||
@@ -528,13 +449,10 @@ impl ChatPanel {
|
|||||||
|
|
||||||
// Spawn a single task to await the send and update reports
|
// Spawn a single task to await the send and update reports
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
let outputs = send_task.await;
|
let reports = send_task.await;
|
||||||
|
|
||||||
let mut sent_ids = sent_ids.lock().await;
|
|
||||||
sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id));
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.insert_reports(id, outputs, cx);
|
this.insert_reports(id, reports, cx);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1391,9 +1309,7 @@ impl ChatPanel {
|
|||||||
fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement {
|
fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement {
|
||||||
let reports = self.sent_reports(id);
|
let reports = self.sent_reports(id);
|
||||||
|
|
||||||
let pending = reports
|
let pending = reports.as_ref().is_some_and(|reports| reports.is_empty());
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|reports| reports.is_empty() || reports.iter().any(|r| r.pending()));
|
|
||||||
|
|
||||||
let success = reports
|
let success = reports
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1462,7 +1378,7 @@ impl ChatPanel {
|
|||||||
.child(name.clone()),
|
.child(name.clone()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.when_some(report.error.clone(), |this, error| {
|
.when_some(report.error().cloned(), |this, error| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
@@ -1477,7 +1393,7 @@ impl ChatPanel {
|
|||||||
.child(div().flex_1().w_full().text_center().child(error)),
|
.child(div().flex_1().w_full().text_center().child(error)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when_some(report.output.clone(), |this, output| {
|
.when_some(report.output().cloned(), |this, output| {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use concord::derive::{GroupKey, channel_rekey_group_key, epoch_key_commitment};
|
|||||||
use concord::state::{CommunityState, HeldKey};
|
use concord::state::{CommunityState, HeldKey};
|
||||||
use concord::{ChannelId, CommunityId, Epoch};
|
use concord::{ChannelId, CommunityId, Epoch};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::UniversalSigner;
|
|
||||||
|
|
||||||
use crate::sync::{self, PlaneKind};
|
use crate::sync::{self, PlaneKind};
|
||||||
|
|
||||||
@@ -175,13 +174,16 @@ pub struct ChannelAdoption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read every rekey wrap the local database holds and adopt what is admissible.
|
/// Read every rekey wrap the local database holds and adopt what is admissible.
|
||||||
pub async fn adopt(
|
pub async fn adopt<S>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
state: &CommunityState,
|
state: &CommunityState,
|
||||||
roles: &CommunityRoles,
|
roles: &CommunityRoles,
|
||||||
signer: &UniversalSigner,
|
signer: &S,
|
||||||
me: PublicKey,
|
me: PublicKey,
|
||||||
) -> Result<Adoptions> {
|
) -> Result<Adoptions>
|
||||||
|
where
|
||||||
|
S: AsyncNip44 + ?Sized,
|
||||||
|
{
|
||||||
let watches = watches(state)?;
|
let watches = watches(state)?;
|
||||||
|
|
||||||
let mut authors: BTreeSet<PublicKey> = BTreeSet::new();
|
let mut authors: BTreeSet<PublicKey> = BTreeSet::new();
|
||||||
@@ -362,15 +364,18 @@ impl Rewrite {
|
|||||||
|
|
||||||
/// Publish this client's own rotation of one scope to its next epoch.
|
/// Publish this client's own rotation of one scope to its next epoch.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn rotate(
|
pub async fn rotate<S>(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
state: &CommunityState,
|
state: &CommunityState,
|
||||||
roles: &CommunityRoles,
|
roles: &CommunityRoles,
|
||||||
signer: &UniversalSigner,
|
signer: &S,
|
||||||
me: PublicKey,
|
me: PublicKey,
|
||||||
rewrite: &Rewrite,
|
rewrite: &Rewrite,
|
||||||
at: Timestamp,
|
at: Timestamp,
|
||||||
) -> Result<Epoch> {
|
) -> Result<Epoch>
|
||||||
|
where
|
||||||
|
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
|
||||||
|
{
|
||||||
if rewrite.excluded.contains(&me) {
|
if rewrite.excluded.contains(&me) {
|
||||||
bail!("a rotation cannot cut off the member who publishes it");
|
bail!("a rotation cannot cut off the member who publishes it");
|
||||||
}
|
}
|
||||||
@@ -611,18 +616,21 @@ struct Delivery {
|
|||||||
|
|
||||||
/// Walk a scope's rotations forward, one epoch at a time, off the key held.
|
/// Walk a scope's rotations forward, one epoch at a time, off the key held.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn walk(
|
async fn walk<S>(
|
||||||
scope: RekeyScope,
|
scope: RekeyScope,
|
||||||
permissions: &[u64],
|
permissions: &[u64],
|
||||||
mut held_epoch: Epoch,
|
mut held_epoch: Epoch,
|
||||||
mut held_key: [u8; 32],
|
mut held_key: [u8; 32],
|
||||||
state: &CommunityState,
|
state: &CommunityState,
|
||||||
roles: &CommunityRoles,
|
roles: &CommunityRoles,
|
||||||
signer: &UniversalSigner,
|
signer: &S,
|
||||||
me: PublicKey,
|
me: PublicKey,
|
||||||
rotations: &[Rotation],
|
rotations: &[Rotation],
|
||||||
published: &BTreeMap<RotationKey, u64>,
|
published: &BTreeMap<RotationKey, u64>,
|
||||||
) -> Result<Step> {
|
) -> Result<Step>
|
||||||
|
where
|
||||||
|
S: AsyncNip44 + ?Sized,
|
||||||
|
{
|
||||||
let mut step = Step::default();
|
let mut step = Step::default();
|
||||||
let mut stepped: Vec<HeldKey> = Vec::new();
|
let mut stepped: Vec<HeldKey> = Vec::new();
|
||||||
let ceiling = held_epoch.0 + REKEY_LOOKAHEAD;
|
let ceiling = held_epoch.0 + REKEY_LOOKAHEAD;
|
||||||
@@ -794,6 +802,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use concord::state::{ChannelKeyRef, HeldRoot};
|
use concord::state::{ChannelKeyRef, HeldRoot};
|
||||||
use nostr_memory::MemoryDatabase;
|
use nostr_memory::MemoryDatabase;
|
||||||
|
use state::UniversalSigner;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use concord::state::{CommunityState, HeldKey, HeldRoot, list_entry};
|
|||||||
use concord::{ChannelId, CommunityId, Epoch, GroupKey, decode_hex_32};
|
use concord::{ChannelId, CommunityId, Epoch, GroupKey, decode_hex_32};
|
||||||
use gpui::AsyncApp;
|
use gpui::AsyncApp;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::UniversalSigner;
|
|
||||||
|
|
||||||
use crate::cache::{self, Observed};
|
use crate::cache::{self, Observed};
|
||||||
use crate::history::{CURSOR_OVERLAP, Window};
|
use crate::history::{CURSOR_OVERLAP, Window};
|
||||||
@@ -348,11 +347,10 @@ pub async fn subscribe_list(client: &Client, self_pk: PublicKey) -> Result<()> {
|
|||||||
///
|
///
|
||||||
/// A held membership is dropped only when the List carries a tombstone at least
|
/// A held membership is dropped only when the List carries a tombstone at least
|
||||||
/// as new as it, because absence from the List is never a fact (§8).
|
/// as new as it, because absence from the List is never a fact (§8).
|
||||||
pub async fn load(
|
pub async fn load<S>(client: &Client, signer: &S, self_pk: PublicKey) -> Result<Vec<CommunityState>>
|
||||||
client: &Client,
|
where
|
||||||
signer: &UniversalSigner,
|
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
|
||||||
self_pk: PublicKey,
|
{
|
||||||
) -> Result<Vec<CommunityState>> {
|
|
||||||
let list = match load_list(client, signer, self_pk).await? {
|
let list = match load_list(client, signer, self_pk).await? {
|
||||||
Some(list) => list,
|
Some(list) => list,
|
||||||
None => return cache::load_states(client).await,
|
None => return cache::load_states(client).await,
|
||||||
@@ -735,6 +733,7 @@ mod tests {
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use nostr_memory::MemoryDatabase;
|
use nostr_memory::MemoryDatabase;
|
||||||
|
use state::UniversalSigner;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|||||||
@@ -759,7 +759,10 @@ fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt and store device keys in the local database.
|
/// Encrypt and store device keys in the local database.
|
||||||
async fn set_keys(client: &Client, signer: &UniversalSigner, secret: &str) -> Result<(), Error> {
|
async fn set_keys<S>(client: &Client, signer: &S, secret: &str) -> Result<(), Error>
|
||||||
|
where
|
||||||
|
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
|
||||||
|
{
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
let content = signer.nip44_encrypt_async(&public_key, secret).await?;
|
let content = signer.nip44_encrypt_async(&public_key, secret).await?;
|
||||||
|
|
||||||
@@ -776,7 +779,10 @@ async fn set_keys(client: &Client, signer: &UniversalSigner, secret: &str) -> Re
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get device keys from the local database.
|
/// Get device keys from the local database.
|
||||||
async fn get_keys(client: &Client, signer: &UniversalSigner) -> Result<Keys, Error> {
|
async fn get_keys<S>(client: &Client, signer: &S) -> Result<Keys, Error>
|
||||||
|
where
|
||||||
|
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
|
||||||
|
{
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
|
|||||||
Reference in New Issue
Block a user