diff --git a/gossip-bin/src/ui/people/followed.rs b/gossip-bin/src/ui/people/followed.rs index 2a47b674..d4525b2b 100644 --- a/gossip-bin/src/ui/people/followed.rs +++ b/gossip-bin/src/ui/people/followed.rs @@ -3,12 +3,14 @@ use crate::AVATAR_SIZE_F32; use eframe::egui; use egui::{Context, Image, RichText, Sense, Ui, Vec2}; use gossip_lib::comms::ToOverlordMessage; -use gossip_lib::Person; -use gossip_lib::GLOBALS; +use gossip_lib::{Person, PersonList, GLOBALS}; use std::sync::atomic::Ordering; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - let followed_pubkeys = GLOBALS.people.get_followed_pubkeys(); + let followed_pubkeys = GLOBALS + .storage + .get_people_in_list(PersonList::Followed, None) + .unwrap_or(vec![]); let mut people: Vec = Vec::new(); for pk in &followed_pubkeys { if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { @@ -23,16 +25,15 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.add_space(12.0); - let last_contact_list_size = GLOBALS + let latest_event_data = GLOBALS .people - .last_contact_list_size - .load(Ordering::Relaxed); - let last_contact_list_asof = GLOBALS - .people - .last_contact_list_asof - .load(Ordering::Relaxed); + .latest_person_list_event_data + .get(&PersonList::Followed) + .map(|v| v.value().clone()) + .unwrap_or(Default::default()); + let mut asof = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_contact_list_asof) { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" )) { @@ -42,8 +43,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.label( RichText::new(format!( - "REMOTE: {} (size={})", - asof, last_contact_list_size + "REMOTE: {} (len={})", + asof, latest_event_data.public_len )) .size(15.0), ) @@ -63,7 +64,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra { let _ = GLOBALS .to_overlord - .send(ToOverlordMessage::UpdateFollowing { merge: false }); + .send(ToOverlordMessage::UpdatePersonList { + person_list: PersonList::Followed, + merge: false, + }); } if ui .button("↓ Merge ↓") @@ -74,7 +78,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra { let _ = GLOBALS .to_overlord - .send(ToOverlordMessage::UpdateFollowing { merge: true }); + .send(ToOverlordMessage::UpdatePersonList { + person_list: PersonList::Followed, + merge: true, + }); } if GLOBALS.signer.is_ready() { @@ -83,7 +90,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra .on_hover_text("This publishes your Contact List") .clicked() { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushFollow); + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::PushPersonList(PersonList::Followed)); } } @@ -118,8 +127,12 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.add_space(10.0); - let last_contact_list_edit = match GLOBALS.storage.read_last_contact_list_edit() { - Ok(date) => date, + let last_contact_list_edit = match GLOBALS + .storage + .get_person_list_last_edit_time(PersonList::Followed) + { + Ok(Some(date)) => date, + Ok(None) => 0, Err(e) => { tracing::error!("{}", e); 0 diff --git a/gossip-bin/src/ui/people/muted.rs b/gossip-bin/src/ui/people/muted.rs index 17f3d0f6..9fa9bbe3 100644 --- a/gossip-bin/src/ui/people/muted.rs +++ b/gossip-bin/src/ui/people/muted.rs @@ -3,12 +3,14 @@ use crate::AVATAR_SIZE_F32; use eframe::egui; use egui::{Context, Image, RichText, Sense, Ui, Vec2}; use gossip_lib::comms::ToOverlordMessage; -use gossip_lib::Person; -use gossip_lib::GLOBALS; +use gossip_lib::{Person, PersonList, GLOBALS}; use std::sync::atomic::Ordering; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - let muted_pubkeys = GLOBALS.people.get_muted_pubkeys(); + let muted_pubkeys = GLOBALS + .storage + .get_people_in_list(PersonList::Muted, None) + .unwrap_or(vec![]); let mut people: Vec = Vec::new(); for pk in &muted_pubkeys { @@ -20,14 +22,19 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra people.push(person); } } - people.sort_unstable(); + people.sort(); ui.add_space(12.0); - let last_mute_list_size = GLOBALS.people.last_mute_list_size.load(Ordering::Relaxed); - let last_mute_list_asof = GLOBALS.people.last_mute_list_asof.load(Ordering::Relaxed); + let latest_event_data = GLOBALS + .people + .latest_person_list_event_data + .get(&PersonList::Muted) + .map(|v| v.value().clone()) + .unwrap_or(Default::default()); + let mut asof = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_mute_list_asof) { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" )) { @@ -35,7 +42,18 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra } } - ui.label(RichText::new(format!("REMOTE: {} (size={})", asof, last_mute_list_size)).size(15.0)) + let txt = if let Some(private_len) = latest_event_data.private_len { + format!( + "REMOTE: {} (public_len={} private_len={})", + asof, latest_event_data.public_len, private_len + ) + } else { + format!( + "REMOTE: {} (public_len={})", + asof, latest_event_data.public_len + ) + }; + ui.label(RichText::new(txt).size(15.0)) .on_hover_text("This is the data in the latest MuteList event fetched from relays"); ui.add_space(10.0); @@ -50,7 +68,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra { let _ = GLOBALS .to_overlord - .send(ToOverlordMessage::UpdateMuteList { merge: false }); + .send(ToOverlordMessage::UpdatePersonList { + person_list: PersonList::Muted, + merge: false, + }); } if ui .button("↓ Merge ↓") @@ -59,7 +80,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra { let _ = GLOBALS .to_overlord - .send(ToOverlordMessage::UpdateMuteList { merge: true }); + .send(ToOverlordMessage::UpdatePersonList { + person_list: PersonList::Muted, + merge: true, + }); } if GLOBALS.signer.is_ready() { @@ -68,7 +92,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra .on_hover_text("This publishes your Mute List") .clicked() { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushMuteList); + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::PushPersonList(PersonList::Muted)); } } @@ -91,8 +117,12 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.add_space(10.0); - let last_mute_list_edit = match GLOBALS.storage.read_last_mute_list_edit() { - Ok(date) => date, + let last_mute_list_edit = match GLOBALS + .storage + .get_person_list_last_edit_time(PersonList::Muted) + { + Ok(Some(date)) => date, + Ok(None) => 0, Err(e) => { tracing::error!("{}", e); 0 diff --git a/gossip-bin/src/ui/wizard/follow_people.rs b/gossip-bin/src/ui/wizard/follow_people.rs index 6ae0f0a4..8e7b95e5 100644 --- a/gossip-bin/src/ui/wizard/follow_people.rs +++ b/gossip-bin/src/ui/wizard/follow_people.rs @@ -17,7 +17,10 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr if app.wizard_state.contacts_sought { let _ = GLOBALS .to_overlord - .send(ToOverlordMessage::UpdateFollowing { merge: false }); + .send(ToOverlordMessage::UpdatePersonList { + person_list: PersonList::Followed, + merge: false, + }); app.wizard_state.contacts_sought = false; } @@ -139,7 +142,9 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr label = label.color(app.theme.accent_color()); } if ui.button(label).clicked() { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushFollow); + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::PushPersonList(PersonList::Followed)); let _ = GLOBALS.storage.write_wizard_complete(true, None); app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); diff --git a/gossip-bin/src/ui/wizard/wizard_state.rs b/gossip-bin/src/ui/wizard/wizard_state.rs index 9a46c664..4aaa67ac 100644 --- a/gossip-bin/src/ui/wizard/wizard_state.rs +++ b/gossip-bin/src/ui/wizard/wizard_state.rs @@ -1,5 +1,4 @@ -use gossip_lib::Relay; -use gossip_lib::GLOBALS; +use gossip_lib::{PersonList, Relay, GLOBALS}; use nostr_types::{Event, EventKind, PublicKey, RelayUrl}; use std::collections::HashSet; @@ -80,7 +79,10 @@ impl WizardState { .unwrap_or(Vec::new()); } - self.followed = GLOBALS.people.get_followed_pubkeys(); + self.followed = GLOBALS + .storage + .get_people_in_list(PersonList::Followed, None) + .unwrap_or(vec![]); if self.need_discovery_relays() { let purplepages = RelayUrl::try_from_str("wss://purplepag.es/").unwrap(); diff --git a/gossip-lib/src/comms.rs b/gossip-lib/src/comms.rs index b450bc19..ddc9a69c 100644 --- a/gossip-lib/src/comms.rs +++ b/gossip-lib/src/comms.rs @@ -1,4 +1,5 @@ use crate::dm_channel::DmChannel; +use crate::people::PersonList; use nostr_types::{ Event, EventAddr, Id, IdHex, Metadata, MilliSatoshi, Profile, PublicKey, RelayUrl, Tag, UncheckedUrl, @@ -102,15 +103,12 @@ pub enum ToOverlordMessage { /// Calls [prune_database](crate::Overlord::prune_database) PruneDatabase, - /// Calls [push_follow](crate::Overlord::push_follow) - PushFollow, + /// Calls [push_person_list](crate::Overlord::push_person_list) + PushPersonList(PersonList), /// Calls [push_metadata](crate::Overlord::push_metadata) PushMetadata(Metadata), - /// Calls [push_mute_list](crate::Overlord::push_mute_list) - PushMuteList, - /// Calls [rank_relay](crate::Overlord::rank_relay) RankRelay(RelayUrl, u8), @@ -152,17 +150,17 @@ pub enum ToOverlordMessage { /// Calls [unlock_key](crate::Overlord::unlock_key) UnlockKey(String), - /// Calls [update_following](crate::Overlord::update_following) - UpdateFollowing { merge: bool }, - /// Calls [update_metadata](crate::Overlord::update_metadata) UpdateMetadata(PublicKey), /// Calls [update_metadata_in_bulk](crate::Overlord::update_metadata_in_bulk) UpdateMetadataInBulk(Vec), - /// Calls [update_mute_list](crate::Overlord::update_mute_list) - UpdateMuteList { merge: bool }, + /// Calls [update_person_list](crate::Overlord::update_person_list) + UpdatePersonList { + person_list: PersonList, + merge: bool, + }, /// Calls [visible_notes_changed](crate::Overlord::visible_notes_changed) VisibleNotesChanged(Vec), diff --git a/gossip-lib/src/error.rs b/gossip-lib/src/error.rs index 6a77a0cf..48b081aa 100644 --- a/gossip-lib/src/error.rs +++ b/gossip-lib/src/error.rs @@ -17,6 +17,7 @@ pub enum ErrorKind { MpscSend(tokio::sync::mpsc::error::SendError), Nip05KeyNotFound, Nostr(nostr_types::Error), + NoPublicKey, NoPrivateKey, NoRelay, NoSlotsRemaining, @@ -88,6 +89,7 @@ impl std::fmt::Display for Error { MpscSend(e) => write!(f, "Error sending mpsc: {e}"), Nip05KeyNotFound => write!(f, "NIP-05 public key not found"), Nostr(e) => write!(f, "Nostr: {e}"), + NoPublicKey => write!(f, "No public key identity available."), NoPrivateKey => write!(f, "No private key available."), NoRelay => write!(f, "Could not determine a relay to use."), NoSlotsRemaining => write!(f, "No custom list slots remaining."), diff --git a/gossip-lib/src/feed.rs b/gossip-lib/src/feed.rs index 8c2d1d9e..ab700b0c 100644 --- a/gossip-lib/src/feed.rs +++ b/gossip-lib/src/feed.rs @@ -486,6 +486,10 @@ pub fn enabled_event_kinds() -> Vec { && ((*k != EventKind::DmChat) || direct_messages) && ((*k != EventKind::GiftWrap) || direct_messages) && ((*k != EventKind::Zap) || enable_zap_receipts) + && (*k != EventKind::ChannelMessage) // not yet implemented + && (*k != EventKind::LiveChatMessage) // not yet implemented + && (*k != EventKind::CommunityPost) // not yet implemented + && (*k != EventKind::DraftLongFormContent) // not yet implemented }) .collect() } diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 3e869f53..fe4e0036 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -7,7 +7,7 @@ use crate::comms::{ use crate::dm_channel::DmChannel; use crate::error::{Error, ErrorKind}; use crate::globals::{ZapState, GLOBALS}; -use crate::people::Person; +use crate::people::{Person, PersonList}; use crate::person_relay::PersonRelay; use crate::relay::Relay; use crate::tags::{ @@ -15,6 +15,7 @@ use crate::tags::{ add_subject_to_tags_if_missing, }; use gossip_relay_picker::{Direction, RelayAssignment}; +use heed::RwTxn; use http::StatusCode; use minion::Minion; use nostr_types::{ @@ -639,15 +640,12 @@ impl Overlord { ToOverlordMessage::PruneDatabase => { Self::prune_database()?; } - ToOverlordMessage::PushFollow => { - self.push_follow().await?; + ToOverlordMessage::PushPersonList(person_list) => { + self.push_person_list(person_list).await?; } ToOverlordMessage::PushMetadata(metadata) => { self.push_metadata(metadata).await?; } - ToOverlordMessage::PushMuteList => { - self.push_mute_list().await?; - } ToOverlordMessage::RankRelay(relay_url, rank) => { Self::rank_relay(relay_url, rank)?; } @@ -690,17 +688,14 @@ impl Overlord { ToOverlordMessage::UnlockKey(password) => { Self::unlock_key(password)?; } - ToOverlordMessage::UpdateFollowing { merge } => { - self.update_following(merge).await?; - } ToOverlordMessage::UpdateMetadata(pubkey) => { self.update_metadata(pubkey).await?; } ToOverlordMessage::UpdateMetadataInBulk(pubkeys) => { self.update_metadata_in_bulk(pubkeys).await?; } - ToOverlordMessage::UpdateMuteList { merge } => { - self.update_mute_list(merge).await?; + ToOverlordMessage::UpdatePersonList { person_list, merge } => { + self.update_person_list(person_list, merge).await?; } ToOverlordMessage::VisibleNotesChanged(visible) => { self.visible_notes_changed(visible).await?; @@ -1599,9 +1594,9 @@ impl Overlord { Ok(()) } - /// Publish the user's following list - pub async fn push_follow(&mut self) -> Result<(), Error> { - let event = GLOBALS.people.generate_contact_list_event().await?; + /// Publish the user's specified PersonList + pub async fn push_person_list(&mut self, list: PersonList) -> Result<(), Error> { + let event = GLOBALS.people.generate_person_list_event(list).await?; // process event locally crate::process::process_new_event(&event, None, None, false, false).await?; @@ -1613,7 +1608,7 @@ impl Overlord { for relay in relays { // Send it the event to pull our followers - tracing::debug!("Pushing ContactList to {}", &relay.url); + tracing::debug!("Pushing PersonList={} to {}", list.name(), &relay.url); self.engage_minion( relay.url.clone(), @@ -1673,38 +1668,6 @@ impl Overlord { Ok(()) } - /// Publish the user's mute list - pub async fn push_mute_list(&mut self) -> Result<(), Error> { - let event = GLOBALS.people.generate_mute_list_event().await?; - - // process event locally - crate::process::process_new_event(&event, None, None, false, false).await?; - - // Push to all of the relays we post to - let relays: Vec = GLOBALS - .storage - .filter_relays(|r| r.has_usage_bits(Relay::WRITE) && r.rank != 0)?; - - for relay in relays { - // Send it the event to pull our followers - tracing::debug!("Pushing MuteList to {}", &relay.url); - - self.engage_minion( - relay.url.clone(), - vec![RelayJob { - reason: RelayConnectionReason::PostMuteList, - payload: ToMinionPayload { - job_id: rand::random::(), - detail: ToMinionPayloadDetail::PostEvent(Box::new(event.clone())), - }, - }], - ) - .await?; - } - - Ok(()) - } - /// Rank a relay from 0 to 9. The default rank is 3. A rank of 0 means the relay will not be used. /// This represent a user's judgement, and is factored into how suitable a relay is for various /// purposes. @@ -2245,127 +2208,6 @@ impl Overlord { Ok(()) } - /// Update the local following list from the last ContactList event received. - pub async fn update_following(&mut self, merge: bool) -> Result<(), Error> { - // Load the latest contact list from the database - let our_contact_list = { - let pubkey = match GLOBALS.signer.public_key() { - Some(pk) => pk, - None => return Ok(()), // we cannot do anything without an identity setup first - }; - - if let Some(event) = GLOBALS - .storage - .get_replaceable_event(pubkey, EventKind::ContactList)? - { - event.clone() - } else { - return Ok(()); // we have no contact list to update from - } - }; - - let mut pubkeys: Vec = Vec::new(); - - let now = Unixtime::now().unwrap(); - - let mut txn = GLOBALS.storage.get_write_txn()?; - - // 'p' tags represent the author's contacts - for tag in &our_contact_list.tags { - if let Tag::Pubkey { - pubkey, - recommended_relay_url, - petname, - .. - } = tag - { - if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { - // Save the pubkey for actual following them (outside of the loop in a batch) - pubkeys.push(pubkey.to_owned()); - - // If there is a URL - if let Some(url) = recommended_relay_url - .as_ref() - .and_then(|rru| RelayUrl::try_from_unchecked_url(rru).ok()) - { - // Save relay if missing - GLOBALS - .storage - .write_relay_if_missing(&url, Some(&mut txn))?; - - // create or update person_relay last_suggested_kind3 - let mut pr = match GLOBALS.storage.read_person_relay(pubkey, &url)? { - Some(pr) => pr, - None => PersonRelay::new(pubkey, url.clone()), - }; - pr.last_suggested_kind3 = Some(now.0 as u64); - GLOBALS.storage.write_person_relay(&pr, Some(&mut txn))?; - } - - // Handle petname - if merge && petname.is_none() { - // In this case, we leave any existing petname, so no need to load the - // person record. But we need to ensure the person exists - GLOBALS - .storage - .write_person_if_missing(&pubkey, Some(&mut txn))?; - } else { - // In every other case we have to load the person and compare - let mut person_needs_save = false; - let mut person = match GLOBALS.storage.read_person(&pubkey)? { - Some(person) => person, - None => { - person_needs_save = true; - Person::new(pubkey.to_owned()) - } - }; - - if *petname != person.petname { - if petname.is_some() { - person_needs_save = true; - person.petname = petname.clone(); - } else if !merge { - // In overwrite mode, clear to None - person_needs_save = true; - person.petname = None; - } - } - - if person_needs_save { - GLOBALS.storage.write_person(&person, Some(&mut txn))?; - } - } - } - } - } - - txn.commit()?; - - // Follow all those pubkeys publicly, and unfollow everbody else if merge=false - GLOBALS.people.follow_all(&pubkeys, merge, true)?; - - // Update last_contact_list_edit - let last_edit = if merge { - Unixtime::now().unwrap() // now, since superior to the last event - } else { - our_contact_list.created_at - }; - GLOBALS - .storage - .write_last_contact_list_edit(last_edit.0, None)?; - - // Pick relays again - { - // Refresh person-relay scores - GLOBALS.relay_picker.refresh_person_relay_scores().await?; - - // Then pick - self.pick_relays().await; - } - - Ok(()) - } - /// Subscribe, fetch, and update metadata for the person pub async fn update_metadata(&mut self, pubkey: PublicKey) -> Result<(), Error> { let best_relays = GLOBALS.storage.get_best_relays(pubkey, Direction::Write)?; @@ -2427,49 +2269,163 @@ impl Overlord { Ok(()) } - /// Update the local mute list from the last ContactList event received. - pub async fn update_mute_list(&mut self, merge: bool) -> Result<(), Error> { - // Load the latest MuteList from the database - let our_mute_list = { - let pubkey = match GLOBALS.signer.public_key() { - Some(pk) => pk, - None => return Ok(()), // we cannot do anything without an identity setup first - }; + /// Update the local mute list from the last MuteList event received. + pub async fn update_person_list(&mut self, list: PersonList, merge: bool) -> Result<(), Error> { + // we cannot do anything without an identity setup first + let my_pubkey = match GLOBALS.storage.read_setting_public_key() { + Some(pk) => pk, + None => return Err(ErrorKind::NoPublicKey.into()), + }; + // Load the latest PersonList event from the database + let event = { if let Some(event) = GLOBALS .storage - .get_replaceable_event(pubkey, EventKind::MuteList)? + .get_replaceable_event(my_pubkey, list.event_kind())? { event.clone() } else { - return Ok(()); // we have no mute list to update from + return Ok(()); // we have no event to update from, so we are done } }; - let mut pubkeys: Vec = Vec::new(); + let now = Unixtime::now().unwrap(); - // 'p' tags represent the author's mutes - for tag in &our_mute_list.tags { - if let Tag::Pubkey { pubkey, .. } = tag { + let mut txn = GLOBALS.storage.get_write_txn()?; + + let mut entries: Vec<(PublicKey, bool)> = Vec::new(); + + // Public entries + for tag in &event.tags { + if let Tag::Pubkey { + pubkey, + recommended_relay_url, + petname, + .. + } = tag + { if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { // Save the pubkey - pubkeys.push(pubkey.to_owned()); + entries.push((pubkey.to_owned(), true)); + + // Deal with recommended_relay_urls and petnames + if list == PersonList::Followed { + Self::integrate_rru_and_petname( + &pubkey, + recommended_relay_url, + petname, + now, + merge, + &mut txn, + )?; + } } } } - // Mute all those pubkeys publicly, and unmute everbody else if merge=false - GLOBALS.people.mute_all(&pubkeys, merge, true)?; + // Private entries + if list != PersonList::Followed { + let decrypted_content = GLOBALS.signer.decrypt_nip04(&my_pubkey, &event.content)?; + + let tags: Vec = serde_json::from_slice(&decrypted_content)?; + + for tag in &tags { + if let Tag::Pubkey { pubkey, .. } = tag { + if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { + // Save the pubkey + entries.push((pubkey.to_owned(), false)); + } + } + } + } + + if !merge { + GLOBALS.storage.clear_person_list(list, Some(&mut txn))?; + } + + for (pubkey, public) in &entries { + GLOBALS + .storage + .add_person_to_list(pubkey, list, *public, Some(&mut txn))?; + GLOBALS.ui_people_to_invalidate.write().push(*pubkey); + } + + let last_edit = if merge { now } else { event.created_at }; - // Update last_must_list_edit - let last_edit = if merge { - Unixtime::now().unwrap() // now, since superior to the last event - } else { - our_mute_list.created_at - }; GLOBALS .storage - .write_last_mute_list_edit(last_edit.0, None)?; + .set_person_list_last_edit_time(list, last_edit.0, Some(&mut txn))?; + + txn.commit()?; + + // Pick relays again + if list.subscribe() { + // Refresh person-relay scores + GLOBALS.relay_picker.refresh_person_relay_scores().await?; + + // Then pick + self.pick_relays().await; + } + + Ok(()) + } + + fn integrate_rru_and_petname( + pubkey: &PublicKey, + recommended_relay_url: &Option, + petname: &Option, + now: Unixtime, + merge: bool, + txn: &mut RwTxn, + ) -> Result<(), Error> { + // If there is a URL + if let Some(url) = recommended_relay_url + .as_ref() + .and_then(|rru| RelayUrl::try_from_unchecked_url(rru).ok()) + { + // Save relay if missing + GLOBALS.storage.write_relay_if_missing(&url, Some(txn))?; + + // create or update person_relay last_suggested_kind3 + let mut pr = match GLOBALS.storage.read_person_relay(*pubkey, &url)? { + Some(pr) => pr, + None => PersonRelay::new(*pubkey, url.clone()), + }; + pr.last_suggested_kind3 = Some(now.0 as u64); + GLOBALS.storage.write_person_relay(&pr, Some(txn))?; + } + + // Handle petname + if merge && petname.is_none() { + // In this case, we leave any existing petname, so no need to load the + // person record. But we need to ensure the person exists + GLOBALS.storage.write_person_if_missing(pubkey, Some(txn))?; + } else { + // In every other case we have to load the person and compare + let mut person_needs_save = false; + let mut person = match GLOBALS.storage.read_person(pubkey)? { + Some(person) => person, + None => { + person_needs_save = true; + Person::new(pubkey.to_owned()) + } + }; + + if *petname != person.petname { + if petname.is_some() { + person_needs_save = true; + person.petname = petname.clone(); + } else if !merge { + // In overwrite mode, clear to None + person_needs_save = true; + person.petname = None; + } + } + + if person_needs_save { + GLOBALS.storage.write_person(&person, Some(txn))?; + } + } Ok(()) } diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 5857cf06..5c7c8eaf 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -5,10 +5,11 @@ use dashmap::{DashMap, DashSet}; use gossip_relay_picker::Direction; use image::RgbaImage; use nostr_types::{ - Event, EventKind, Metadata, PreEvent, PublicKey, RelayUrl, Tag, UncheckedUrl, Unixtime, Url, + ContentEncryptionAlgorithm, Event, EventKind, Metadata, PreEvent, PublicKey, RelayUrl, Tag, + UncheckedUrl, Unixtime, Url, }; use serde::{Deserialize, Serialize}; -use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; +use std::sync::atomic::Ordering; use std::time::Duration; use tokio::sync::RwLock; use tokio::task; @@ -19,6 +20,30 @@ pub type Person = crate::storage::types::Person2; /// PersonList type, aliased to the latest version pub type PersonList = crate::storage::types::PersonList1; +/// Person List Compare Data +#[derive(Debug, Clone)] +pub struct PersonListEventData { + /// The timestamp of the latest event + pub when: Unixtime, + + /// The number of public entries in the latest event + pub public_len: usize, + + /// The number of private entires in the latest event, or None if it + /// couldn't be computed (not logged in, Following event, or none found) + pub private_len: Option, +} + +impl Default for PersonListEventData { + fn default() -> PersonListEventData { + PersonListEventData { + when: Unixtime(0), + public_len: 0, + private_len: None, + } + } +} + /// Handles people and remembers what needs to be done for each, such as fetching /// metadata or avatars. pub struct People { @@ -45,17 +70,8 @@ pub struct People { // per gossip run (this set only grows) tried_metadata: DashSet, - // Date of the last self-owned contact list we have an event for - pub last_contact_list_asof: AtomicI64, - - // Size of the last self-owned contact list we have an event for - pub last_contact_list_size: AtomicUsize, - - // Date of the last self-owned mute list we have an event for - pub last_mute_list_asof: AtomicI64, - - // Size of the last self-owned mute list we have an event for - pub last_mute_list_size: AtomicUsize, + /// Latest person list event data for each PersonList + pub latest_person_list_event_data: DashMap, } impl Default for People { @@ -74,67 +90,13 @@ impl People { recheck_nip05: DashSet::new(), need_metadata: DashSet::new(), tried_metadata: DashSet::new(), - last_contact_list_asof: AtomicI64::new(0), - last_contact_list_size: AtomicUsize::new(0), - last_mute_list_asof: AtomicI64::new(0), - last_mute_list_size: AtomicUsize::new(0), + latest_person_list_event_data: DashMap::new(), } } // Start the periodic task management pub(crate) fn start() { - if let Some(pk) = GLOBALS.signer.public_key() { - // Load our contact list from the database in order to populate - // last_contact_list_asof and last_contact_list_size - if let Ok(Some(event)) = GLOBALS - .storage - .get_replaceable_event(pk, EventKind::ContactList) - { - if event.created_at.0 - > GLOBALS - .people - .last_contact_list_asof - .load(Ordering::Relaxed) - { - GLOBALS - .people - .last_contact_list_asof - .store(event.created_at.0, Ordering::Relaxed); - let size = event - .tags - .iter() - .filter(|t| matches!(t, Tag::Pubkey { .. })) - .count(); - GLOBALS - .people - .last_contact_list_size - .store(size, Ordering::Relaxed); - } - } - - // Load our mute list from the database in order to populate - // last_mute_list_asof and last_mute_list_size - if let Ok(Some(event)) = GLOBALS - .storage - .get_replaceable_event(pk, EventKind::MuteList) - { - if event.created_at.0 > GLOBALS.people.last_mute_list_asof.load(Ordering::Relaxed) { - GLOBALS - .people - .last_mute_list_asof - .store(event.created_at.0, Ordering::Relaxed); - let size = event - .tags - .iter() - .filter(|t| matches!(t, Tag::Pubkey { .. })) - .count(); - GLOBALS - .people - .last_mute_list_size - .store(size, Ordering::Relaxed); - } - } - } + GLOBALS.people.update_latest_person_list_event_data(); task::spawn(async { loop { @@ -155,6 +117,52 @@ impl People { }); } + /// Search local events for the latest PersonList event for each kind of PersonList, + /// and determine their timestamps and lengths, storing result in People. + pub fn update_latest_person_list_event_data(&self) { + // Get public key, or give up + let pk = match GLOBALS.storage.read_setting_public_key() { + Some(pk) => pk, + None => return, + }; + + for (person_list, _) in PersonList::all_lists() { + if let Ok(Some(event)) = GLOBALS + .storage + .get_replaceable_event(pk, person_list.event_kind()) + { + self.latest_person_list_event_data.insert( + person_list, + PersonListEventData { + when: event.created_at, + public_len: event + .tags + .iter() + .filter(|t| matches!(t, Tag::Pubkey { .. })) + .count(), + private_len: { + let mut private_len: Option = None; + if !matches!(person_list, PersonList::Followed) + && GLOBALS.signer.is_ready() + { + if let Ok(bytes) = GLOBALS.signer.decrypt_nip04(&pk, &event.content) + { + if let Ok(vectags) = serde_json::from_slice::>(&bytes) + { + private_len = Some(vectags.len()); + } + } + } + private_len + }, + }, + ); + } else { + self.latest_person_list_event_data.remove(&person_list); + } + } + } + /// Get all the pubkeys that the user subscribes to in any list pub fn get_subscribed_pubkeys(&self) -> Vec { // We subscribe to all people in all lists. @@ -168,50 +176,13 @@ impl People { } } - /// Get all the pubkeys in the Followed list - pub fn get_followed_pubkeys(&self) -> Vec { - // We subscribe to all people in all lists. - // This is no longer synonomous with the ContactList list - match GLOBALS + /// Is the person in the list? (returns false on error) + #[inline] + pub fn is_person_in_list(&self, pubkey: &PublicKey, list: PersonList) -> bool { + GLOBALS .storage - .get_people_in_list(PersonList::Followed, None) - { - Ok(people) => people, - Err(e) => { - tracing::error!("{}", e); - vec![] - } - } - } - - /// Get all the pubkeys that the user mutes - pub fn get_muted_pubkeys(&self) -> Vec { - match GLOBALS.storage.get_people_in_list(PersonList::Muted, None) { - Ok(people) => people, - Err(e) => { - tracing::error!("{}", e); - vec![] - } - } - } - - /// Is the given pubkey followed? - pub fn is_followed(&self, pubkey: &PublicKey) -> bool { - match GLOBALS - .storage - .is_person_in_list(pubkey, PersonList::Followed) - { - Ok(answer) => answer, - _ => false, - } - } - - /// Is the given pubkey muted? - pub fn is_muted(&self, pubkey: &PublicKey) -> bool { - match GLOBALS.storage.is_person_in_list(pubkey, PersonList::Muted) { - Ok(answer) => answer, - _ => false, - } + .is_person_in_list(pubkey, list) + .unwrap_or(false) } /// Get all the pubkeys that need relay lists (from the given set) @@ -609,92 +580,103 @@ impl People { .collect()) } - pub(crate) async fn generate_contact_list_event(&self) -> Result { - let mut p_tags: Vec = Vec::new(); - - let pubkeys = self.get_followed_pubkeys(); - - for pubkey in &pubkeys { - // Get their petname - let mut petname: Option = None; - if let Some(person) = GLOBALS.storage.read_person(pubkey)? { - petname = person.petname.clone(); - } - - // Get their best relay - let relays = GLOBALS.storage.get_best_relays(*pubkey, Direction::Write)?; - let maybeurl = relays.get(0); - p_tags.push(Tag::Pubkey { - pubkey: (*pubkey).into(), - recommended_relay_url: maybeurl.map(|(u, _)| u.to_unchecked_url()), - petname, - trailing: Vec::new(), - }); + pub(crate) async fn generate_person_list_event( + &self, + person_list: PersonList, + ) -> Result { + if !GLOBALS.signer.is_ready() { + return Err((ErrorKind::NoPrivateKey, file!(), line!()).into()); } - let public_key = match GLOBALS.signer.public_key() { - Some(pk) => pk, - None => return Err((ErrorKind::NoPrivateKey, file!(), line!()).into()), // not even a public key - }; + let my_pubkey = GLOBALS.signer.public_key().unwrap(); - // Get the content from our latest ContactList. - // We don't use the data, but we shouldn't clobber it. - - let content = match GLOBALS + // Read the person list in two parts + let public_people = GLOBALS .storage - .get_replaceable_event(public_key, EventKind::ContactList)? - { - Some(c) => c.content, - None => "".to_owned(), + .get_people_in_list(person_list, Some(true))?; + let private_people = GLOBALS + .storage + .get_people_in_list(person_list, Some(false))?; + + // Determine the event kind + let kind = match person_list { + PersonList::Followed => EventKind::ContactList, + PersonList::Muted => EventKind::MuteList, + PersonList::Custom(_) => EventKind::CategorizedPeopleList, }; - let pre_event = PreEvent { - pubkey: public_key, - created_at: Unixtime::now().unwrap(), - kind: EventKind::ContactList, - tags: p_tags, - content, - }; + // Build public p-tags + let mut tags: Vec = Vec::new(); + for pubkey in public_people.iter() { + // Only include petnames in the ContactList (which is only public people) + let petname = if kind == EventKind::ContactList { + if let Some(person) = GLOBALS.storage.read_person(pubkey)? { + person.petname.clone() + } else { + None + } + } else { + None + }; - GLOBALS.signer.sign_preevent(pre_event, None, None) - } + // Only include recommended relay urls in public entries, and not in the mute list + let recommended_relay_url = if kind != EventKind::MuteList { + let relays = GLOBALS.storage.get_best_relays(*pubkey, Direction::Write)?; + relays.get(0).map(|(u, _)| u.to_unchecked_url()) + } else { + None + }; - pub(crate) async fn generate_mute_list_event(&self) -> Result { - let mut p_tags: Vec = Vec::new(); - - let muted_pubkeys = self.get_muted_pubkeys(); - - for muted_pubkey in &muted_pubkeys { - p_tags.push(Tag::Pubkey { - pubkey: (*muted_pubkey).into(), - recommended_relay_url: None, - petname: None, + tags.push(Tag::Pubkey { + pubkey: pubkey.into(), + recommended_relay_url, + petname, trailing: vec![], }); } - let public_key = match GLOBALS.signer.public_key() { - Some(pk) => pk, - None => return Err((ErrorKind::NoPrivateKey, file!(), line!()).into()), // not even a public key - }; + // Add d-tag if using CategorizedPeopleList + if matches!(person_list, PersonList::Custom(_)) { + tags.push(Tag::Identifier { + d: person_list.name(), + trailing: vec![], + }); + } - // Get the content from our latest MuteList. - // We don't use the data, but we shouldn't clobber it (it is for private mutes - // that we have not implemented yet) - - let content = match GLOBALS - .storage - .get_replaceable_event(public_key, EventKind::MuteList)? - { - Some(c) => c.content, - None => "".to_owned(), + let content = { + if kind == EventKind::ContactList { + match GLOBALS + .storage + .get_replaceable_event(my_pubkey, EventKind::ContactList)? + { + Some(c) => c.content, + None => "".to_owned(), + } + } else { + // Build private p-tags (except for ContactList) + let mut private_p_tags: Vec = Vec::new(); + for pubkey in private_people.iter() { + private_p_tags.push(Tag::Pubkey { + pubkey: pubkey.into(), + recommended_relay_url: None, + petname: None, + trailing: vec![], + }); + } + let private_tags_string = serde_json::to_string(&private_p_tags)?; + GLOBALS.signer.encrypt( + &my_pubkey, + &private_tags_string, + ContentEncryptionAlgorithm::Nip04, + )? + } }; let pre_event = PreEvent { - pubkey: public_key, + pubkey: my_pubkey, created_at: Unixtime::now().unwrap(), - kind: EventKind::MuteList, - tags: p_tags, + kind, + tags, content, }; @@ -721,55 +703,17 @@ impl People { } GLOBALS.ui_people_to_invalidate.write().push(*pubkey); - GLOBALS - .storage - .write_last_contact_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?; + GLOBALS.storage.set_person_list_last_edit_time( + PersonList::Followed, + Unixtime::now().unwrap().0, + Some(&mut txn), + )?; txn.commit()?; Ok(()) } - /// Follow all these public keys. - /// This does not publish any events. - pub(crate) fn follow_all( - &self, - pubkeys: &[PublicKey], - public: bool, - merge: bool, - ) -> Result<(), Error> { - let mut txn = GLOBALS.storage.get_write_txn()?; - - if !merge { - GLOBALS - .storage - .clear_person_list(PersonList::Followed, Some(&mut txn))?; - } - - for pubkey in pubkeys { - GLOBALS.storage.add_person_to_list( - pubkey, - PersonList::Followed, - public, - Some(&mut txn), - )?; - GLOBALS.ui_people_to_invalidate.write().push(*pubkey); - } - - GLOBALS - .storage - .write_last_contact_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?; - - txn.commit()?; - - // Add the people to the relay_picker for picking - for pubkey in pubkeys.iter() { - GLOBALS.relay_picker.add_someone(pubkey.to_owned())?; - } - - Ok(()) - } - /// Empty the following list. /// This does not publish any events. pub(crate) fn follow_none(&self) -> Result<(), Error> { @@ -778,9 +722,11 @@ impl People { GLOBALS .storage .clear_person_list(PersonList::Followed, Some(&mut txn))?; - GLOBALS - .storage - .write_last_contact_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?; + GLOBALS.storage.set_person_list_last_edit_time( + PersonList::Followed, + Unixtime::now().unwrap().0, + Some(&mut txn), + )?; txn.commit()?; @@ -797,9 +743,11 @@ impl People { GLOBALS .storage .clear_person_list(PersonList::Muted, Some(&mut txn))?; - GLOBALS - .storage - .write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?; + GLOBALS.storage.set_person_list_last_edit_time( + PersonList::Muted, + Unixtime::now().unwrap().0, + Some(&mut txn), + )?; txn.commit()?; @@ -831,9 +779,11 @@ impl People { .remove_person_from_list(pubkey, PersonList::Muted, Some(&mut txn))?; } - GLOBALS - .storage - .write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?; + GLOBALS.storage.set_person_list_last_edit_time( + PersonList::Muted, + Unixtime::now().unwrap().0, + Some(&mut txn), + )?; txn.commit()?; @@ -842,39 +792,6 @@ impl People { Ok(()) } - pub(crate) fn mute_all( - &self, - pubkeys: &[PublicKey], - merge: bool, - public: bool, - ) -> Result<(), Error> { - let mut txn = GLOBALS.storage.get_write_txn()?; - - if !merge { - GLOBALS - .storage - .clear_person_list(PersonList::Muted, Some(&mut txn))?; - } - - for pubkey in pubkeys { - GLOBALS.storage.add_person_to_list( - pubkey, - PersonList::Muted, - public, - Some(&mut txn), - )?; - GLOBALS.ui_people_to_invalidate.write().push(*pubkey); - } - - GLOBALS - .storage - .write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?; - - txn.commit()?; - - Ok(()) - } - // Returns true if the date passed in is newer than what we already had pub(crate) async fn update_relay_list_stamps( &self, diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index 1f4f0278..21143baf 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -2,6 +2,7 @@ use crate::comms::ToOverlordMessage; use crate::error::Error; use crate::filter::EventFilterAction; use crate::globals::GLOBALS; +use crate::people::PersonList; use crate::person_relay::PersonRelay; use async_recursion::async_recursion; use nostr_types::{ @@ -63,7 +64,11 @@ pub async fn process_new_event( } // Spam filter (displayable and author is not followed) - if event.effective_kind().is_feed_displayable() && !GLOBALS.people.is_followed(&event.pubkey) { + if event.effective_kind().is_feed_displayable() + && !GLOBALS + .people + .is_person_in_list(&event.pubkey, PersonList::Followed) + { let author = GLOBALS.storage.read_person(&event.pubkey)?; match crate::filter::filter(event.clone(), author) { EventFilterAction::Allow => {} @@ -212,62 +217,32 @@ pub async fn process_new_event( if event.kind == EventKind::ContactList { if let Some(pubkey) = GLOBALS.signer.public_key() { if event.pubkey == pubkey { - // We do not process our own contact list automatically. - // Instead we only process it on user command. - // See Overlord::update_following() - // - // But we do update people.last_contact_list_asof and _size - if event.created_at.0 - > GLOBALS - .people - .last_contact_list_asof - .load(Ordering::Relaxed) - { - GLOBALS - .people - .last_contact_list_asof - .store(event.created_at.0, Ordering::Relaxed); - let size = event - .tags - .iter() - .filter(|t| matches!(t, Tag::Pubkey { .. })) - .count(); - GLOBALS - .people - .last_contact_list_size - .store(size, Ordering::Relaxed); - } - return Ok(()); + // Update this data for the UI. We don't actually process the latest event + // until the user gives the go ahead. + GLOBALS.people.update_latest_person_list_event_data(); } else { process_somebody_elses_contact_list(event).await?; } } else { process_somebody_elses_contact_list(event).await?; } - } else if event.kind == EventKind::MuteList { + } else if event.kind == EventKind::MuteList || event.kind == EventKind::CategorizedPeopleList { if let Some(pubkey) = GLOBALS.signer.public_key() { if event.pubkey == pubkey { - // We do not process our own mute list automatically. - // Instead we only process it on user command. - // See Overlord::update_muted() - // - // But we do update people.last_mute_list_asof and _size - if event.created_at.0 > GLOBALS.people.last_mute_list_asof.load(Ordering::Relaxed) { - GLOBALS - .people - .last_mute_list_asof - .store(event.created_at.0, Ordering::Relaxed); - let size = event - .tags - .iter() - .filter(|t| matches!(t, Tag::Pubkey { .. })) - .count(); - GLOBALS - .people - .last_mute_list_size - .store(size, Ordering::Relaxed); + // Update this data for the UI. We don't actually process the latest event + // until the user gives the go ahead. + GLOBALS.people.update_latest_person_list_event_data(); + } + } + + // Allocate a slot for this person list + if event.kind == EventKind::CategorizedPeopleList { + // get d-tag + for tag in event.tags.iter() { + if let Tag::Identifier { d, .. } = tag { + // This will allocate if missing, and will be ok if it exists + PersonList::allocate(d, None)?; } - return Ok(()); } } } else if event.kind == EventKind::RelayList { diff --git a/gossip-lib/src/storage/migrations/deprecated.rs b/gossip-lib/src/storage/migrations/deprecated.rs new file mode 100644 index 00000000..0d78f3a5 --- /dev/null +++ b/gossip-lib/src/storage/migrations/deprecated.rs @@ -0,0 +1,61 @@ +use super::Storage; +use crate::error::Error; +use heed::RwTxn; +use nostr_types::Unixtime; + +impl Storage { + /// Write the user's last ContactList edit time + /// DEPRECATED - use set_person_list_last_edit_time instead + pub(in crate::storage) fn write_last_contact_list_edit<'a>( + &'a self, + when: i64, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + let bytes = when.to_be_bytes(); + + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + self.general + .put(txn, b"last_contact_list_edit", bytes.as_slice())?; + Ok(()) + }; + + match rw_txn { + Some(txn) => f(txn)?, + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) + } + + /// Read the user's last ContactList edit time + /// DEPRECATED - use get_person_list_last_edit_time instead + pub(in crate::storage) fn read_last_contact_list_edit(&self) -> Result { + let txn = self.env.read_txn()?; + + match self.general.get(&txn, b"last_contact_list_edit")? { + None => { + let now = Unixtime::now().unwrap(); + Ok(now.0) + } + Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())), + } + } + + /// Read the user's last MuteList edit time + /// DEPRECATED - use get_person_list_last_edit_time instead + pub(in crate::storage) fn read_last_mute_list_edit(&self) -> Result { + let txn = self.env.read_txn()?; + + match self.general.get(&txn, b"last_mute_list_edit")? { + None => { + let now = Unixtime::now().unwrap(); + Ok(now.0) + } + Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())), + } + } +} diff --git a/gossip-lib/src/storage/migrations/mod.rs b/gossip-lib/src/storage/migrations/mod.rs index bdce53e8..d4446823 100644 --- a/gossip-lib/src/storage/migrations/mod.rs +++ b/gossip-lib/src/storage/migrations/mod.rs @@ -1,3 +1,5 @@ +mod deprecated; + use super::types::{ Person2, PersonList1, PersonRelay1, Settings1, Settings2, Theme1, ThemeVariant1, }; @@ -10,7 +12,7 @@ use speedy::{Readable, Writable}; use std::collections::HashMap; impl Storage { - const MAX_MIGRATION_LEVEL: u32 = 14; + const MAX_MIGRATION_LEVEL: u32 = 15; pub(super) fn migrate(&self, mut level: u32) -> Result<(), Error> { if level > Self::MAX_MIGRATION_LEVEL { @@ -137,6 +139,10 @@ impl Storage { tracing::info!("{prefix}: removing a retired setting..."); self.remove_setting_custom_person_list_names(txn)?; } + 14 => { + tracing::info!("{prefix}: moving person list last edit times..."); + self.move_person_list_last_edit_times(txn)?; + } _ => panic!("Unreachable migration level"), }; @@ -603,4 +609,15 @@ impl Storage { self.general.delete(txn, b"custom_person_list_names")?; Ok(()) } + + pub fn move_person_list_last_edit_times<'a>( + &'a self, + txn: &mut RwTxn<'a>, + ) -> Result<(), Error> { + let mut edit_times: HashMap = HashMap::new(); + edit_times.insert(PersonList1::Followed, self.read_last_contact_list_edit()?); + edit_times.insert(PersonList1::Muted, self.read_last_mute_list_edit()?); + self.write_person_lists_last_edit_times(edit_times, Some(txn))?; + Ok(()) + } } diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index df9bcd38..9ca7ea83 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -538,17 +538,17 @@ impl Storage { } } - /// Write the user's last ContactList edit time - pub fn write_last_contact_list_edit<'a>( + /// Write the user's last PersonList edit times + pub fn write_person_lists_last_edit_times<'a>( &'a self, - when: i64, + times: HashMap, rw_txn: Option<&mut RwTxn<'a>>, ) -> Result<(), Error> { - let bytes = when.to_be_bytes(); + let bytes = times.write_to_vec()?; let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { self.general - .put(txn, b"last_contact_list_edit", bytes.as_slice())?; + .put(txn, b"person_lists_last_edit_times", bytes.as_slice())?; Ok(()) }; @@ -565,57 +565,32 @@ impl Storage { } /// Read the user's last ContactList edit time - pub fn read_last_contact_list_edit(&self) -> Result { + pub fn read_person_lists_last_edit_times(&self) -> Result, Error> { let txn = self.env.read_txn()?; - match self.general.get(&txn, b"last_contact_list_edit")? { - None => { - let now = Unixtime::now().unwrap(); - self.write_last_contact_list_edit(now.0, None)?; - Ok(now.0) - } - Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())), + match self.general.get(&txn, b"person_lists_last_edit_times")? { + None => Ok(HashMap::new()), + Some(bytes) => Ok(HashMap::::read_from_buffer(bytes)?), } } - /// Write the user's last MuteList edit time - pub fn write_last_mute_list_edit<'a>( + /// Set a person list last edit time + pub fn set_person_list_last_edit_time<'a>( &'a self, - when: i64, + list: PersonList, + time: i64, rw_txn: Option<&mut RwTxn<'a>>, ) -> Result<(), Error> { - let bytes = when.to_be_bytes(); - - let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { - self.general - .put(txn, b"last_mute_list_edit", bytes.as_slice())?; - Ok(()) - }; - - match rw_txn { - Some(txn) => f(txn)?, - None => { - let mut txn = self.env.write_txn()?; - f(&mut txn)?; - txn.commit()?; - } - }; - + let mut lists = self.read_person_lists_last_edit_times()?; + let _ = lists.insert(list, time); + self.write_person_lists_last_edit_times(lists, rw_txn)?; Ok(()) } - /// Read the user's last MuteList edit time - pub fn read_last_mute_list_edit(&self) -> Result { - let txn = self.env.read_txn()?; - - match self.general.get(&txn, b"last_mute_list_edit")? { - None => { - let now = Unixtime::now().unwrap(); - self.write_last_mute_list_edit(now.0, None)?; - Ok(now.0) - } - Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())), - } + /// Get a person list last edit time + pub fn get_person_list_last_edit_time(&self, list: PersonList) -> Result, Error> { + let lists = self.read_person_lists_last_edit_times()?; + Ok(lists.get(&list).copied()) } /// Write a flag, whether the user is only following people with no account (or not) diff --git a/gossip-lib/src/storage/types/person_list1.rs b/gossip-lib/src/storage/types/person_list1.rs index 82ce4c55..4f3e661d 100644 --- a/gossip-lib/src/storage/types/person_list1.rs +++ b/gossip-lib/src/storage/types/person_list1.rs @@ -1,6 +1,7 @@ use crate::error::{Error, ErrorKind}; use crate::globals::GLOBALS; use heed::RwTxn; +use nostr_types::EventKind; use speedy::{Readable, Writable}; /// Lists people can be added to @@ -51,14 +52,34 @@ impl PersonList1 { let mut output: Vec<(PersonList1, String)> = vec![]; let map = GLOBALS.storage.read_setting_custom_person_list_map(); for (k, v) in map.iter() { - output.push((PersonList1::Custom(*k), v.clone())); + match k { + 0 => output.push((PersonList1::Muted, v.clone())), + 1 => output.push((PersonList1::Followed, v.clone())), + _ => output.push((PersonList1::Custom(*k), v.clone())), + } } output } /// Allocate a new PersonList1 with the given name pub fn allocate(name: &str, txn: Option<&mut RwTxn<'_>>) -> Result { + // Do not allocate for well-known names + if name == "Followed" { + return Ok(PersonList1::Followed); + } else if name == "Muted" { + return Ok(PersonList1::Muted); + } + let mut map = GLOBALS.storage.read_setting_custom_person_list_map(); + + // Check if it already exists to prevent duplicates + for (k, v) in map.iter() { + if v == name { + return Ok(PersonList1::Custom(*k)); + } + } + + // Find a slot and allocate for i in 2..255 { if map.contains_key(&i) { continue; @@ -69,6 +90,7 @@ impl PersonList1 { .write_setting_custom_person_list_map(&map, txn)?; return Ok(PersonList1::Custom(i)); } + Err(ErrorKind::NoSlotsRemaining.into()) } @@ -105,6 +127,15 @@ impl PersonList1 { } } + /// Get the event kind matching this PersonList1 + pub fn event_kind(&self) -> EventKind { + match *self { + PersonList1::Followed => EventKind::ContactList, + PersonList1::Muted => EventKind::MuteList, + PersonList1::Custom(_) => EventKind::CategorizedPeopleList, + } + } + /// Should we subscribe to events from people in this list? pub fn subscribe(&self) -> bool { !matches!(*self, PersonList1::Muted)