Files
coop/crates/registry/src/room.rs
T
reyaandGitHub 2ea2519e8b feat: resend failed messages (#147)
* .

* .

* fix

* fix

* update

* fix

* .

* .
2025-09-12 17:07:57 +07:00

597 lines
18 KiB
Rust

use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::time::Duration;
use anyhow::Error;
use common::display::ReadableProfile;
use common::event::EventUtils;
use global::constants::SEND_RETRY;
use global::{css, nostr_client};
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use crate::Registry;
#[derive(Debug, Clone)]
pub struct SendReport {
pub receiver: PublicKey,
pub tags: Option<Vec<Tag>>,
pub status: Option<Output<EventId>>,
pub error: Option<SharedString>,
pub relays_not_found: bool,
}
impl SendReport {
pub fn new(receiver: PublicKey) -> Self {
Self {
receiver,
status: None,
error: None,
tags: None,
relays_not_found: false,
}
}
pub fn not_found(mut self) -> Self {
self.relays_not_found = true;
self
}
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
self.error = Some(error.into());
self.relays_not_found = false;
self
}
pub fn status(mut self, output: Output<EventId>) -> Self {
self.status = Some(output);
self.relays_not_found = false;
self
}
pub fn tags(mut self, tags: &Vec<Tag>) -> Self {
self.tags = Some(tags.to_owned());
self
}
pub fn is_relay_error(&self) -> bool {
self.error.is_some() || self.relays_not_found
}
pub fn is_sent_success(&self) -> bool {
if let Some(output) = self.status.as_ref() {
!output.success.is_empty()
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub enum RoomSignal {
NewMessage((EventId, Box<Event>)),
Refresh,
}
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum RoomKind {
Ongoing,
#[default]
Request,
}
#[derive(Debug)]
pub struct Room {
pub id: u64,
pub created_at: Timestamp,
/// Subject of the room
pub subject: Option<String>,
/// Picture of the room
pub picture: Option<String>,
/// All members of the room
pub members: Vec<PublicKey>,
/// Kind
pub kind: RoomKind,
}
impl Ord for Room {
fn cmp(&self, other: &Self) -> Ordering {
self.created_at.cmp(&other.created_at)
}
}
impl PartialOrd for Room {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Room {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
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 {}
impl Room {
pub fn new(event: &Event) -> Self {
let id = event.uniq_id();
let created_at = event.created_at;
// 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())
} 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())
} else {
None
};
Self {
id,
created_at,
subject,
picture,
members,
kind: RoomKind::default(),
}
}
/// Sets the kind of the room and returns the modified room
///
/// This is a builder-style method that allows chaining room modifications.
///
/// # Arguments
///
/// * `kind` - The RoomKind to set for this room
///
/// # Returns
///
/// The modified Room instance with the new kind
pub fn kind(mut self, kind: RoomKind) -> Self {
self.kind = kind;
self
}
/// Sets the rearrange_by field of the room and returns the modified room
///
/// This is a builder-style method that allows chaining room modifications.
///
/// # Arguments
///
/// * `rearrange_by` - The PublicKey to set for rearranging the member list
///
/// # Returns
///
/// 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.iter().partition(|&key| key != &rearrange_by);
self.members = not_match;
self.members.extend(matches);
self
}
/// Set the room kind to ongoing
///
/// # Arguments
///
/// * `cx` - The context to notify about the update
pub fn set_ongoing(&mut self, cx: &mut Context<Self>) {
if self.kind != RoomKind::Ongoing {
self.kind = RoomKind::Ongoing;
cx.notify();
}
}
/// Checks if the room is a group chat
///
/// # Returns
///
/// true if the room has more than 2 members, false otherwise
pub fn is_group(&self) -> bool {
self.members.len() > 2
}
/// Updates the creation timestamp of the room
///
/// # Arguments
///
/// * `created_at` - The new Timestamp to set
/// * `cx` - The context to notify about the update
pub fn created_at(&mut self, created_at: impl Into<Timestamp>, cx: &mut Context<Self>) {
self.created_at = created_at.into();
cx.notify();
}
/// Updates the subject of the room
///
/// # Arguments
///
/// * `subject` - The new subject to set
/// * `cx` - The context to notify about the update
pub fn subject(&mut self, subject: String, cx: &mut Context<Self>) {
self.subject = Some(subject);
cx.notify();
}
/// Updates the picture of the room
///
/// # Arguments
///
/// * `picture` - The new subject to set
/// * `cx` - The context to notify about the update
pub fn picture(&mut self, picture: String, cx: &mut Context<Self>) {
self.picture = Some(picture);
cx.notify();
}
/// Gets the display name for the room
///
/// If the room has a subject set, that will be used as the display name.
/// Otherwise, it will generate a name based on the room members.
///
/// # Arguments
///
/// * `cx` - The application context
///
/// # Returns
///
/// A string containing the display name
pub fn display_name(&self, cx: &App) -> String {
if let Some(subject) = self.subject.clone() {
subject
} else {
self.merge_name(cx)
}
}
/// Gets the display image for the room
///
/// The image is determined by:
/// - The room's picture if set
/// - The first member's avatar for 1:1 chats
/// - A default group image for group chats
///
/// # Arguments
///
/// * `proxy` - Whether to use the proxy for the avatar URL
/// * `cx` - The application context
///
/// # Returns
///
/// 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() {
self.first_member(cx).avatar_url(proxy)
} else {
"brand/group.png".into()
}
}
/// Get the first member of the room.
///
/// First member is always different from the current user.
pub(crate) fn first_member(&self, cx: &App) -> Profile {
let registry = Registry::read_global(cx);
registry.get_person(&self.members[0], cx)
}
/// Merge the names of the first two members of the room.
pub(crate) fn merge_name(&self, cx: &App) -> String {
let registry = Registry::read_global(cx);
if self.is_group() {
let profiles = self
.members
.iter()
.map(|pk| registry.get_person(pk, cx))
.collect::<Vec<_>>();
let mut name = profiles
.iter()
.take(2)
.map(|p| p.display_name())
.collect::<Vec<_>>()
.join(", ");
if profiles.len() > 2 {
name = format!("{}, +{}", name, profiles.len() - 2);
}
name
} else {
self.first_member(cx).display_name()
}
}
/// Loads all messages for this room from the database
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<Event>, Error> containing all messages for this room
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<Event>, Error>> {
let members = self.members.clone();
cx.background_spawn(async move {
let client = nostr_client();
let public_key = members[members.len() - 1];
let sent = Filter::new()
.kind(Kind::PrivateDirectMessage)
.author(public_key)
.pubkeys(members.clone());
let recv = Filter::new()
.kind(Kind::PrivateDirectMessage)
.authors(members)
.pubkey(public_key);
let sent_events = client.database().query(sent).await?;
let recv_events = client.database().query(recv).await?;
let events: Vec<Event> = sent_events.merge(recv_events).into_iter().collect();
Ok(events)
})
}
/// Emits a new message signal to the current room
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.
pub fn emit_refresh(&mut self, cx: &mut Context<Self>) {
cx.emit(RoomSignal::Refresh);
}
/// Creates a temporary message for optimistic updates
///
/// The event must not been published to relays.
pub fn create_temp_message(
&self,
receiver: PublicKey,
content: &str,
replies: &[EventId],
) -> UnsignedEvent {
let builder = EventBuilder::private_msg_rumor(receiver, content);
let mut tags = vec![];
// Add event reference if it's present (replying to another event)
if replies.len() == 1 {
tags.push(Tag::event(replies[0]))
} else {
for id in replies.iter() {
tags.push(Tag::from_standardized(TagStandard::Quote {
event_id: id.to_owned(),
relay_url: None,
public_key: None,
}))
}
}
let mut event = builder.tags(tags).build(receiver);
// Ensure event ID is set
event.ensure_id();
event
}
/// Create a task to sends a message to all members in the background
pub fn send_in_background(
&self,
content: &str,
replies: Vec<EventId>,
backup: bool,
cx: &App,
) -> Task<Result<Vec<SendReport>, Error>> {
let content = content.to_owned();
let subject = self.subject.clone();
let picture = self.picture.clone();
let mut public_keys = self.members.clone();
cx.background_spawn(async move {
let css = css();
let client = nostr_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let mut tags: Vec<Tag> = public_keys
.iter()
.filter_map(|&this| {
if this != public_key {
Some(Tag::public_key(this))
} else {
None
}
})
.collect();
// Add event reference if it's present (replying to another event)
if replies.len() == 1 {
tags.push(Tag::event(replies[0]))
} else {
for id in replies.iter() {
tags.push(Tag::from_standardized(TagStandard::Quote {
event_id: id.to_owned(),
relay_url: None,
public_key: None,
}))
}
}
// Add subject tag if it's present
if let Some(subject) = subject {
tags.push(Tag::from_standardized(TagStandard::Subject(
subject.to_string(),
)));
}
// Add picture tag if it's present
if let Some(picture) = picture {
tags.push(Tag::custom(TagKind::custom("picture"), vec![picture]));
}
// 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 pubkey in public_keys.into_iter() {
match client
.send_private_msg(pubkey, &content, tags.clone())
.await
{
Ok(output) => {
let id = output.id().to_owned();
let auth_required = output.failed.iter().any(|m| m.1.starts_with("auth-"));
let report = SendReport::new(pubkey).status(output).tags(&tags);
if auth_required {
// Wait for authenticated and resent event successfully
for attempt in 0..=SEND_RETRY {
// Check if event was successfully resent
if let Some(output) = css
.resent_ids
.read()
.await
.iter()
.find(|e| e.id() == &id)
.cloned()
{
let output = SendReport::new(pubkey).status(output).tags(&tags);
reports.push(output);
break;
}
// Check if retry limit exceeded
if attempt == SEND_RETRY {
reports.push(report);
break;
}
smol::Timer::after(Duration::from_millis(1200)).await;
}
} else {
reports.push(report);
}
}
Err(e) => {
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
reports.push(SendReport::new(pubkey).not_found().tags(&tags));
} else {
reports.push(SendReport::new(pubkey).error(e.to_string()).tags(&tags));
}
}
}
}
// 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(public_key, &content, tags.clone())
.await
{
Ok(output) => {
reports.push(SendReport::new(public_key).status(output).tags(&tags));
}
Err(e) => {
if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e {
reports.push(SendReport::new(public_key).not_found());
} else {
reports
.push(SendReport::new(public_key).error(e.to_string()).tags(&tags));
}
}
}
}
Ok(reports)
})
}
/// Create a task to resend a failed message
pub fn resend(
&self,
reports: Vec<SendReport>,
message: String,
backup: bool,
cx: &App,
) -> Task<Result<Vec<SendReport>, Error>> {
cx.background_spawn(async move {
let client = nostr_client();
let mut resend_reports = vec![];
let mut resend_tag = vec![];
for report in reports.into_iter() {
if let Some(output) = report.status {
let id = output.id();
let urls: Vec<&RelayUrl> = output.failed.keys().collect();
if let Some(event) = client.database().event_by_id(id).await? {
for url in urls.into_iter() {
let relay = client.pool().relay(url).await?;
let id = relay.send_event(&event).await?;
let resent: Output<EventId> = Output {
val: id,
success: HashSet::from([url.to_owned()]),
failed: HashMap::new(),
};
resend_reports.push(SendReport::new(report.receiver).status(resent));
}
if let Some(tags) = report.tags {
resend_tag.extend(tags);
}
}
}
}
// Only send a backup message to current user if sent successfully to others
if backup && !resend_reports.is_empty() {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let output = client
.send_private_msg(public_key, message, resend_tag)
.await?;
resend_reports.push(SendReport::new(public_key).status(output));
}
Ok(resend_reports)
})
}
}