chore: fix crash when failing to parse message (#202)

* clean up

* .

* fix rich text component

* clean up
This commit is contained in:
reya
2025-11-03 19:04:16 +07:00
committed by GitHub
parent 4ebe590f8a
commit a4067d2c00
12 changed files with 200 additions and 366 deletions

View File

@@ -197,7 +197,11 @@ impl AutoUpdater {
});
}
Err(e) => {
log::warn!("{e}")
_ = this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Idle, cx);
});
log::warn!("{e}");
}
}
}),

View File

@@ -371,6 +371,7 @@ impl Room {
continue;
};
// Construct a filter for messaging relays
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(member)
@@ -379,6 +380,7 @@ impl Room {
// Subscribe to get members messaging relays
client.subscribe(filter, Some(opts)).await?;
// Construct a filter for encryption keys announcement
let filter = Filter::new()
.kind(Kind::Custom(10044))
.author(member)
@@ -392,42 +394,6 @@ impl Room {
})
}
pub fn verify_connections(&self, cx: &App) -> Task<Result<HashMap<PublicKey, bool>, Error>> {
let members = self.members();
cx.background_spawn(async move {
let client = app_state().client();
let mut result = HashMap::default();
for member in members.into_iter() {
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(member)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first() {
let urls: Vec<&RelayUrl> = nip17::extract_relay_list(event).collect();
if urls.is_empty() {
result.insert(member, false);
continue;
}
for url in urls {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
result.insert(member, true);
} else {
result.insert(member, false);
}
}
Ok(result)
})
}
/// 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();

View File

@@ -30,5 +30,4 @@ serde_json.workspace = true
indexset = "0.12.3"
emojis = "0.6.4"
once_cell = "1.19.0"
linkify = "0.10.0"
regex = "1"

View File

@@ -22,7 +22,7 @@ use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use smol::fs;
use states::{app_state, SignerKind, QUERY_TIMEOUT};
use states::{app_state, SignerKind};
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -45,9 +45,6 @@ mod emoji;
mod subject;
mod text;
const NIP17_WARN: &str = "has not set up Messaging Relays, they cannot receive your message.";
const EMPTY_WARN: &str = "Something is wrong. Coop cannot display this message";
pub fn init(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<ChatPanel> {
cx.new(|cx| ChatPanel::new(room, window, cx))
}
@@ -77,7 +74,7 @@ pub struct ChatPanel {
image_cache: Entity<RetainAllImageCache>,
_subscriptions: SmallVec<[Subscription; 3]>,
_tasks: SmallVec<[Task<()>; 3]>,
_tasks: SmallVec<[Task<()>; 2]>,
}
impl ChatPanel {
@@ -99,21 +96,11 @@ impl ChatPanel {
let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.));
let connect = room.read(cx).connect(cx);
let verify_connections = room.read(cx).verify_connections(cx);
let get_messages = room.read(cx).get_messages(cx);
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
tasks.push(
// Get messaging relays and encryption keys announcement for all members
cx.background_spawn(async move {
if let Err(e) = connect.await {
log::error!("Failed to initialize room: {e}");
}
}),
);
tasks.push(
// Load all messages belonging to this room
cx.spawn_in(window, async move |this, cx| {
@@ -134,35 +121,11 @@ impl ChatPanel {
);
tasks.push(
// Connect and verify all members messaging relays
cx.spawn_in(window, async move |this, cx| {
// Wait for 5 seconds before connecting and verifying
cx.background_executor()
.timer(Duration::from_secs(QUERY_TIMEOUT))
.await;
let result = verify_connections.await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(data) => {
let persons = PersonRegistry::global(cx);
for (public_key, status) in data.into_iter() {
if !status {
let profile = persons.read(cx).get_person(&public_key, cx);
let name = profile.display_name();
this.insert_warning(format!("{NIP17_WARN} {name}"), cx);
}
}
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
// Get messaging relays and encryption keys announcement for each member
cx.background_spawn(async move {
if let Err(e) = connect.await {
log::error!("Failed to initialize room: {}", e);
}
}),
);
@@ -663,6 +626,33 @@ impl ChatPanel {
}
fn render_message(
&mut self,
ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
if let Some(message) = self.messages.get_index(ix) {
match message {
Message::User(rendered) => {
let text = self
.rendered_texts_by_id
.entry(rendered.id)
.or_insert_with(|| RenderedText::new(&rendered.content, cx))
.element(ix.into(), window, cx);
self.render_text_message(ix, rendered, text, cx)
}
Message::Warning(content, _timestamp) => {
self.render_warning(ix, SharedString::from(content), cx)
}
Message::System(_timestamp) => self.render_announcement(ix, cx),
}
} else {
self.render_warning(ix, SharedString::from("Message not found"), cx)
}
}
fn render_text_message(
&self,
ix: usize,
message: &RenderedMessage,
@@ -1358,26 +1348,9 @@ impl Render for ChatPanel {
.child(
list(
self.list_state.clone(),
cx.processor(move |this, ix: usize, window, cx| {
if let Some(message) = this.messages.get_index(ix) {
match message {
Message::User(rendered) => {
let text = this
.rendered_texts_by_id
.entry(rendered.id)
.or_insert_with(|| RenderedText::new(&rendered.content, cx))
.element(ix.into(), window, cx);
this.render_message(ix, rendered, text, cx)
}
Message::Warning(content, _timestamp) => {
this.render_warning(ix, SharedString::from(content), cx)
}
Message::System(_timestamp) => this.render_announcement(ix, cx),
}
} else {
this.render_warning(ix, SharedString::from(EMPTY_WARN), cx)
}
cx.processor(|this, ix, window, cx| {
// Get and render message by index
this.render_message(ix, window, cx)
}),
)
.flex_1(),

View File

@@ -3,10 +3,9 @@ use std::sync::Arc;
use common::display::RenderedProfile;
use gpui::{
AnyElement, AnyView, App, ElementId, HighlightStyle, InteractiveText, IntoElement,
SharedString, StyledText, UnderlineStyle, Window,
AnyElement, App, ElementId, HighlightStyle, InteractiveText, IntoElement, SharedString,
StyledText, UnderlineStyle, Window,
};
use linkify::{LinkFinder, LinkKind};
use nostr_sdk::prelude::*;
use once_cell::sync::Lazy;
use person::PersonRegistry;
@@ -16,7 +15,7 @@ use theme::ActiveTheme;
use crate::actions::OpenPublicKey;
static URL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^(?:[a-zA-Z]+://)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(/.*)?$").unwrap()
Regex::new(r"(?i)(?:^|\s)(?:https?://)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d+)?(?:/[^\s]*)?(?:\s|$)").unwrap()
});
static NOSTR_URI_REGEX: Lazy<Regex> =
@@ -24,43 +23,16 @@ static NOSTR_URI_REGEX: Lazy<Regex> =
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Highlight {
Link(HighlightStyle),
Link,
Nostr,
}
impl Highlight {
fn link() -> Self {
Self::Link(HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
})
}
fn nostr() -> Self {
Self::Nostr
}
}
impl From<HighlightStyle> for Highlight {
fn from(style: HighlightStyle) -> Self {
Self::Link(style)
}
}
type CustomRangeTooltipFn =
Option<Arc<dyn Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView>>>;
#[derive(Default)]
pub struct RenderedText {
pub text: SharedString,
pub highlights: Vec<(Range<usize>, Highlight)>,
pub link_ranges: Vec<Range<usize>>,
pub link_urls: Arc<[String]>,
pub custom_ranges: Vec<Range<usize>>,
custom_ranges_tooltip_fn: CustomRangeTooltipFn,
}
impl RenderedText {
@@ -86,19 +58,9 @@ impl RenderedText {
link_urls: link_urls.into(),
link_ranges,
highlights,
custom_ranges: Vec::new(),
custom_ranges_tooltip_fn: None,
}
}
#[allow(dead_code)]
pub fn set_tooltip_builder_for_custom_ranges<F>(&mut self, f: F)
where
F: Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView> + 'static,
{
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
}
pub fn element(&self, id: ElementId, window: &Window, cx: &App) -> AnyElement {
let link_color = cx.theme().text_accent;
@@ -110,17 +72,11 @@ impl RenderedText {
(
range.clone(),
match highlight {
Highlight::Link(highlight) => {
// Check if this is a link highlight by seeing if it has an underline
if highlight.underline.is_some() {
// It's a link, so apply the link color
let mut link_style = *highlight;
link_style.color = Some(link_color);
link_style
} else {
*highlight
}
}
Highlight::Link => HighlightStyle {
color: Some(link_color),
underline: Some(UnderlineStyle::default()),
..Default::default()
},
Highlight::Nostr => HighlightStyle {
color: Some(link_color),
..Default::default()
@@ -135,49 +91,22 @@ impl RenderedText {
move |ix, window, cx| {
let token = link_urls[ix].as_str();
if token.starts_with("nostr:") {
let clean_url = token.replace("nostr:", "");
let Ok(public_key) = PublicKey::parse(&clean_url) else {
log::error!("Failed to parse public key from: {clean_url}");
return;
};
window.dispatch_action(Box::new(OpenPublicKey(public_key)), cx);
} else if is_url(token) {
if !token.starts_with("http") {
cx.open_url(&format!("https://{token}"));
} else {
cx.open_url(token);
if let Some(clean_url) = token.strip_prefix("nostr:") {
if let Ok(public_key) = PublicKey::parse(clean_url) {
window.dispatch_action(Box::new(OpenPublicKey(public_key)), cx);
}
} else if is_url(token) {
let url = if token.starts_with("http") {
token.to_string()
} else {
format!("https://{token}")
};
cx.open_url(&url);
} else {
log::warn!("Unrecognized token {token}")
}
}
})
.tooltip({
let link_ranges = self.link_ranges.clone();
let link_urls = self.link_urls.clone();
let custom_tooltip_ranges = self.custom_ranges.clone();
let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone();
move |idx, window, cx| {
for (ix, range) in link_ranges.iter().enumerate() {
if range.contains(&idx) {
let url = &link_urls[ix];
if url.starts_with("http") {
// return Some(LinkPreview::new(url, cx));
}
// You can add custom tooltip handling for mentions here
}
}
for range in &custom_tooltip_ranges {
if range.contains(&idx) {
if let Some(f) = &custom_tooltip_fn {
return f(idx, range.clone(), window, cx);
}
}
}
None
}
})
.into_any_element()
}
}
@@ -193,20 +122,15 @@ fn render_plain_text_mut(
// Copy the content directly
text.push_str(content);
// Initialize the link finder
let mut finder = LinkFinder::new();
finder.url_must_have_scheme(false);
finder.kinds(&[LinkKind::Url]);
// Collect all URLs
let mut url_matches: Vec<(Range<usize>, String)> = Vec::new();
for link in finder.links(content) {
let start = link.start();
let end = link.end();
let range = start..end;
for link in URL_REGEX.find_iter(content) {
let range = link.start()..link.end();
let url = link.as_str().to_string();
log::info!("Found URL: {}", url);
url_matches.push((range, url));
}
@@ -214,9 +138,7 @@ fn render_plain_text_mut(
let mut nostr_matches: Vec<(Range<usize>, String)> = Vec::new();
for nostr_match in NOSTR_URI_REGEX.find_iter(content) {
let start = nostr_match.start();
let end = nostr_match.end();
let range = start..end;
let range = nostr_match.start()..nostr_match.end();
let nostr_uri = nostr_match.as_str().to_string();
// Check if this nostr URI overlaps with any already processed URL
@@ -240,12 +162,9 @@ fn render_plain_text_mut(
for (range, entity) in all_matches {
// Handle URL token
if is_url(&entity) {
// Add underline highlight
highlights.push((range.clone(), Highlight::link()));
// Make it clickable
highlights.push((range.clone(), Highlight::Link));
link_ranges.push(range);
link_urls.push(entity);
continue;
};
@@ -306,75 +225,6 @@ fn render_plain_text_mut(
}
}
}
fn render_pubkey(
public_key: PublicKey,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
cx: &App,
) {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let display_name = format!("@{}", profile.display_name());
// Replace token with display name
text.replace_range(range.clone(), &display_name);
// Adjust ranges
let new_length = display_name.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
// New range for the replacement
let new_range = range.start..(range.start + new_length);
// Add highlight for the profile name
highlights.push((new_range.clone(), Highlight::nostr()));
// Make it clickable
link_ranges.push(new_range);
link_urls.push(format!("nostr:{}", profile.public_key().to_hex()));
// Adjust subsequent ranges if needed
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
fn render_bech32(
bech32: String,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
) {
let njump_url = format!("https://njump.me/{bech32}");
// Create a shortened display format for the URL
let shortened_entity = format_shortened_entity(&bech32);
let display_text = format!("https://njump.me/{shortened_entity}");
// Replace the original entity with the shortened display version
text.replace_range(range.clone(), &display_text);
// Adjust the ranges
let new_length = display_text.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
// New range for the replacement
let new_range = range.start..(range.start + new_length);
// Add underline highlight
highlights.push((new_range.clone(), Highlight::link()));
// Make it clickable
link_ranges.push(new_range);
link_urls.push(njump_url);
// Adjust subsequent ranges if needed
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
}
/// Check if a string is a URL
@@ -396,6 +246,61 @@ fn format_shortened_entity(entity: &str) -> String {
}
}
fn render_pubkey(
public_key: PublicKey,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
cx: &App,
) {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let display_name = format!("@{}", profile.display_name());
text.replace_range(range.clone(), &display_name);
let new_length = display_name.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
let new_range = range.start..(range.start + new_length);
highlights.push((new_range.clone(), Highlight::Nostr));
link_ranges.push(new_range);
link_urls.push(format!("nostr:{}", profile.public_key().to_hex()));
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
fn render_bech32(
bech32: String,
text: &mut String,
range: &Range<usize>,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
) {
let njump_url = format!("https://njump.me/{bech32}");
let shortened_entity = format_shortened_entity(&bech32);
let display_text = format!("https://njump.me/{shortened_entity}");
text.replace_range(range.clone(), &display_text);
let new_length = display_text.len();
let length_diff = new_length as isize - (range.end - range.start) as isize;
let new_range = range.start..(range.start + new_length);
highlights.push((new_range.clone(), Highlight::Link));
link_ranges.push(new_range);
link_urls.push(njump_url);
if length_diff != 0 {
adjust_ranges(highlights, link_ranges, range.end, length_diff);
}
}
// Helper function to adjust ranges when text length changes
fn adjust_ranges(
highlights: &mut [(Range<usize>, Highlight)],

View File

@@ -6,7 +6,6 @@ use gpui::{Image, ImageFormat, SharedString, SharedUri};
use nostr_sdk::prelude::*;
use qrcode::render::svg;
use qrcode::QrCode;
use states::IMAGE_RESIZE_SERVICE;
const NOW: &str = "now";
const SECONDS_IN_MINUTE: i64 = 60;
@@ -14,6 +13,7 @@ const MINUTES_IN_HOUR: i64 = 60;
const HOURS_IN_DAY: i64 = 24;
const DAYS_IN_MONTH: i64 = 30;
const FALLBACK_IMG: &str = "https://image.nostr.build/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png";
const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
pub trait RenderedProfile {
fn avatar(&self, proxy: bool) -> SharedUri;

View File

@@ -1167,42 +1167,41 @@ impl ChatSpace {
let file_keystore = KeyStore::global(cx).read(cx).is_using_file_keystore();
let proxy = AppSettings::get_proxy_user_avatars(cx);
let auth_requests = self.auth_requests.read(cx).len();
let auto_update = AutoUpdater::global(cx);
h_flex()
.gap_1()
.map(
|this| match AutoUpdater::global(cx).read(cx).status.as_ref() {
AutoUpdateStatus::Checking => this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Checking for Coop updates...")),
),
AutoUpdateStatus::Installing => this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Installing updates...")),
),
AutoUpdateStatus::Errored { msg } => this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(msg.as_ref())),
),
AutoUpdateStatus::Updated => this.child(
div()
.id("restart")
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Updated. Click to restart"))
.on_click(|_ev, _window, cx| {
cx.restart();
}),
),
_ => this.child(div()),
},
)
.map(|this| match auto_update.read(cx).status.as_ref() {
AutoUpdateStatus::Checking => this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Checking for Coop updates...")),
),
AutoUpdateStatus::Installing => this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Installing updates...")),
),
AutoUpdateStatus::Errored { msg } => this.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(msg.as_ref())),
),
AutoUpdateStatus::Updated => this.child(
div()
.id("restart")
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Updated. Click to restart"))
.on_click(|_ev, _window, cx| {
cx.restart();
}),
),
_ => this.child(div()),
})
.when(file_keystore, |this| {
this.child(
Button::new("keystore-warning")

View File

@@ -6,7 +6,7 @@ use gpui::{
TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
WindowOptions,
};
use states::{app_state, APP_ID, CLIENT_NAME};
use states::{app_state, APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS};
use ui::Root;
use crate::actions::{load_embedded_fonts, quit, Quit};
@@ -21,14 +21,34 @@ fn main() {
// Initialize logging
tracing_subscriber::fmt::init();
// Initialize the coop simple storage
let _app_state = app_state();
// Initialize the Application
let app = Application::new()
.with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
// Initialize app state
let app_state = app_state();
// Connect to relays
app.background_executor()
.spawn(async move {
let client = app_state.client();
// Get all bootstrapping relays
let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS);
urls.extend(SEARCH_RELAYS);
// Add relay to the relay pool
for url in urls.into_iter() {
client.add_relay(url).await.ok();
}
// Establish connection to relays
client.connect().await;
})
.detach();
// Run application
app.run(move |cx| {
// Load embedded fonts in assets/fonts

View File

@@ -6,7 +6,7 @@ use anyhow::{anyhow, Error};
use chat::room::{Room, RoomKind};
use chat::{ChatEvent, ChatRegistry};
use common::debounced_delay::DebouncedDelay;
use common::display::{RenderedProfile, RenderedTimestamp, TextUtils};
use common::display::{RenderedTimestamp, TextUtils};
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
@@ -628,8 +628,8 @@ impl Sidebar {
items.push(
RoomListItem::new(ix)
.room_id(room_id)
.name(member.display_name())
.avatar(member.avatar(proxy))
.name(this.display_name(cx))
.avatar(this.display_image(proxy, cx))
.public_key(member.public_key())
.kind(this.kind)
.created_at(this.created_at.to_ago())

View File

@@ -42,9 +42,3 @@ pub const METADATA_BATCH_TIMEOUT: u64 = 300;
/// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;
/// Image Resize Service
pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
/// Default NIP96 Media Server.
pub const NIP96_SERVER: &str = "https://nostrmedia.com";

View File

@@ -12,7 +12,7 @@ use nostr_sdk::prelude::*;
use smol::lock::RwLock;
use crate::constants::{
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, QUERY_TIMEOUT, SEARCH_RELAYS,
BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT, QUERY_TIMEOUT,
};
use crate::paths::config_dir;
use crate::state::ingester::Ingester;
@@ -210,19 +210,6 @@ impl AppState {
/// Handles events from the nostr client
pub async fn handle_notifications(&self) -> Result<(), Error> {
// Get all bootstrapping relays
let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS);
urls.extend(SEARCH_RELAYS);
// Add relay to the relay pool
for url in urls.into_iter() {
self.client.add_relay(url).await?;
}
// Establish connection to relays
self.client.connect().await;
let mut processed_events: HashSet<EventId> = HashSet::new();
let mut challenges: HashSet<Cow<'_, str>> = HashSet::new();
let mut notifications = self.client.notifications();
@@ -345,9 +332,7 @@ impl AppState {
self.signal.send(SignalKind::NewProfile(profile)).await;
}
Kind::GiftWrap => {
if let Err(e) = self.extract_rumor(&event).await {
log::error!("Failed to extract rumor: {e}");
}
self.extract_rumor(&event).await.ok();
}
_ => {}
}
@@ -997,6 +982,8 @@ impl AppState {
.subscribe_with_id_to(&urls, id, filter, None)
.await?;
log::info!("Subscribed to gift wrap events");
Ok(())
}
@@ -1027,15 +1014,15 @@ impl AppState {
}
/// Stores an unwrapped event in local database with reference to original
async fn set_rumor(&self, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
async fn set_rumor(&self, gift_wrap: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
let rumor_id = rumor.id.context("Rumor is missing an event id")?;
let author = rumor.pubkey;
let conversation = self.conversation_id(rumor).to_string();
let conversation = self.conversation_id(rumor);
let mut tags = rumor.tags.clone().to_vec();
// Add a unique identifier
tags.push(Tag::identifier(id));
tags.push(Tag::identifier(gift_wrap));
// Add a reference to the rumor's author
tags.push(Tag::custom(
@@ -1046,7 +1033,7 @@ impl AppState {
// Add a conversation id
tags.push(Tag::custom(
TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)),
[conversation],
[conversation.to_string()],
));
// Add a reference to the rumor's id
@@ -1063,21 +1050,23 @@ impl AppState {
// Convert rumor to json
let content = rumor.as_json();
// Construct the event
let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tags(tags)
.sign(&Keys::generate())
.await?;
// Save the event to the database
self.client.database().save_event(&event).await?;
Ok(())
}
/// Retrieves a previously unwrapped event from local database
async fn get_rumor(&self, id: EventId) -> Result<UnsignedEvent, Error> {
async fn get_rumor(&self, gift_wrap: EventId) -> Result<UnsignedEvent, Error> {
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifier(id)
.identifier(gift_wrap)
.limit(1);
if let Some(event) = self.client.database().query(filter).await?.first_owned() {
@@ -1118,20 +1107,15 @@ impl AppState {
// Helper method to try unwrapping with different signers
async fn try_unwrap_gift_wrap(&self, gift_wrap: &Event) -> Result<UnwrappedGift, Error> {
// Try to unwrap with the encryption key first
// Try to unwrap with the encryption key if available
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
if let Some(signer) = self.device.read().await.encryption.as_ref() {
match UnwrappedGift::from_gift_wrap(signer, gift_wrap).await {
Ok(unwrapped) => {
return Ok(unwrapped);
}
Err(e) => {
log::warn!("Failed to unwrap with the encryption key: {e}")
}
if let Ok(unwrapped) = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await {
return Ok(unwrapped);
}
}
// Try to unwrap with the user's signer
// Fallback to unwrap with the user's signer
let signer = self.client.signer().await?;
let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?;