Mute List made to operate like Contact List does

This commit is contained in:
Mike Dilger 2023-09-13 16:29:06 +12:00
parent 27cd06f6a7
commit f225fe8bf4
8 changed files with 401 additions and 13 deletions

View File

@ -11,6 +11,7 @@ pub enum ToOverlordMessage {
AdvertiseRelayList,
ChangePassphrase(String, String),
ClearFollowing,
ClearMuteList,
DelegationReset,
DeletePost(Id),
DeletePriv,
@ -35,6 +36,7 @@ pub enum ToOverlordMessage {
PruneDatabase,
PushFollow,
PushMetadata(Metadata),
PushMuteList,
ReengageMinion(RelayUrl, Vec<RelayJob>),
RefreshFollowedMetadata,
ClearAllUsageOnRelay(RelayUrl),
@ -47,6 +49,7 @@ pub enum ToOverlordMessage {
Shutdown,
UnlockKey(String),
UpdateFollowing(bool),
UpdateMuteList(bool),
UpdateMetadata(PublicKey),
UpdateMetadataInBulk(Vec<PublicKey>),
VisibleNotesChanged(Vec<Id>),
@ -105,6 +108,7 @@ pub enum RelayConnectionReason {
PostContacts,
PostLike,
PostMetadata,
PostMuteList,
ReadThread,
}
@ -131,6 +135,7 @@ impl RelayConnectionReason {
PostLike => "Posting a reaction to an event",
FetchContacts => "Fetching our contact list",
PostContacts => "Posting our contact list",
PostMuteList => "Posting our mute list",
PostMetadata => "Posting our metadata",
ReadThread => "Reading ancestors to build a thread",
}
@ -151,6 +156,7 @@ impl RelayConnectionReason {
PostLike => false,
FetchContacts => false,
PostContacts => false,
PostMuteList => false,
PostMetadata => false,
ReadThread => true,
}

View File

@ -624,6 +624,7 @@ impl Minion {
EventKind::Metadata,
//EventKind::RecommendRelay,
EventKind::ContactList,
EventKind::MuteList,
EventKind::RelayList,
],
// these are all replaceable, no since required

View File

@ -532,6 +532,9 @@ impl Overlord {
ToOverlordMessage::ClearFollowing => {
self.clear_following().await?;
}
ToOverlordMessage::ClearMuteList => {
self.clear_mute_list().await?;
}
ToOverlordMessage::DelegationReset => {
Self::delegation_reset().await?;
}
@ -754,6 +757,9 @@ impl Overlord {
ToOverlordMessage::PushMetadata(metadata) => {
self.push_metadata(metadata).await?;
}
ToOverlordMessage::PushMuteList => {
self.push_mute_list().await?;
}
ToOverlordMessage::RankRelay(relay_url, rank) => {
if let Some(mut relay) = GLOBALS.storage.read_relay(&relay_url)? {
relay.rank = rank as u64;
@ -856,6 +862,9 @@ impl Overlord {
.await?;
}
}
ToOverlordMessage::UpdateMuteList(merge) => {
self.update_mute_list(merge).await?;
}
ToOverlordMessage::VisibleNotesChanged(visible) => {
let visible: Vec<IdHex> = visible.iter().map(|i| (*i).into()).collect();
@ -1396,6 +1405,42 @@ impl Overlord {
Ok(())
}
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<Relay> = 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::<u64>(),
detail: ToMinionPayloadDetail::PostEvent(Box::new(event.clone())),
},
}],
)
.await?;
}
Ok(())
}
async fn clear_mute_list(&mut self) -> Result<(), Error> {
GLOBALS.people.clear_mute_list()?;
Ok(())
}
async fn push_metadata(&mut self, metadata: Metadata) -> Result<(), Error> {
let public_key = match GLOBALS.signer.public_key() {
Some(pk) => pk,
@ -1963,6 +2008,53 @@ impl Overlord {
Ok(())
}
// This updates the actual mute list from the last MuteList received
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
};
if let Some(event) = GLOBALS
.storage
.get_replaceable_event(pubkey, EventKind::MuteList)?
{
event.clone()
} else {
return Ok(()); // we have no mute list to update from
}
};
let mut pubkeys: Vec<PublicKey> = Vec::new();
// 'p' tags represent the author's mutes
for tag in &our_mute_list.tags {
if let Tag::Pubkey { pubkey, .. } = tag {
if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) {
// Save the pubkey
pubkeys.push(pubkey.to_owned());
}
}
}
// Mute all those pubkeys, and unmute everbody else if merge=false
GLOBALS.people.mute_all(&pubkeys, merge)?;
// 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)?;
Ok(())
}
async fn search(mut text: String) -> Result<(), Error> {
if text.len() < 2 {
GLOBALS

View File

@ -77,6 +77,12 @@ pub struct People {
// 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,
}
impl People {
@ -91,14 +97,16 @@ impl People {
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),
}
}
// Start the periodic task management
pub fn start() {
// Load our contact list from the database in order to populate
// last_contact_list_asof and last_contact_list_size
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)
@ -124,6 +132,29 @@ impl People {
.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);
}
}
}
task::spawn(async {
@ -602,6 +633,49 @@ impl People {
GLOBALS.signer.sign_preevent(pre_event, None, None)
}
pub async fn generate_mute_list_event(&self) -> Result<Event, Error> {
let mut p_tags: Vec<Tag> = 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,
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
};
// 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 pre_event = PreEvent {
pubkey: public_key,
created_at: Unixtime::now().unwrap(),
kind: EventKind::MuteList,
tags: p_tags,
content,
ots: None,
};
GLOBALS.signer.sign_preevent(pre_event, None, None)
}
pub fn follow(&self, pubkey: &PublicKey, follow: bool) -> Result<(), Error> {
let mut txn = GLOBALS.storage.get_write_txn()?;
@ -674,7 +748,26 @@ impl People {
Ok(())
}
pub fn clear_mute_list(&self) -> Result<(), Error> {
let mut txn = GLOBALS.storage.get_write_txn()?;
GLOBALS
.storage
.clear_person_list(PersonList::Muted, Some(&mut txn))?;
GLOBALS
.storage
.write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
txn.commit()?;
GLOBALS.ui_invalidate_all.store(false, Ordering::Relaxed);
Ok(())
}
pub fn mute(&self, pubkey: &PublicKey, mute: bool) -> Result<(), Error> {
let mut txn = GLOBALS.storage.get_write_txn()?;
if mute {
if let Some(pk) = GLOBALS.signer.public_key() {
if pk == *pubkey {
@ -684,17 +777,49 @@ impl People {
GLOBALS
.storage
.add_person_to_list(pubkey, PersonList::Muted, None)?;
.add_person_to_list(pubkey, PersonList::Muted, Some(&mut txn))?;
} else {
GLOBALS
.storage
.remove_person_from_list(pubkey, PersonList::Muted, None)?;
.remove_person_from_list(pubkey, PersonList::Muted, Some(&mut txn))?;
}
GLOBALS
.storage
.write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
txn.commit()?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
Ok(())
}
pub fn mute_all(&self, pubkeys: &[PublicKey], merge: 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, 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 async fn update_relay_list_stamps(
&self,

View File

@ -234,14 +234,36 @@ pub async fn process_new_event(
} else {
process_somebody_elses_contact_list(event).await?;
}
}
if event.kind == EventKind::RelayList {
} else if event.kind == EventKind::MuteList {
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);
}
return Ok(());
}
}
} else if event.kind == EventKind::RelayList {
GLOBALS.storage.process_relay_list(event)?;
}
// If the content is a repost, seek the event it reposts
if event.kind == EventKind::Repost {
} else if event.kind == EventKind::Repost {
// If the content is a repost, seek the event it reposts
for (id, optrelay) in event.mentions().iter() {
if let Some(rurl) = optrelay {
let _ = GLOBALS

View File

@ -544,6 +544,44 @@ impl Storage {
}
}
pub fn write_last_mute_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_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()?;
}
};
Ok(())
}
pub fn read_last_mute_list_edit(&self) -> Result<i64, Error> {
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())),
}
}
// Settings ----------------------------------------------------------
// This defines functions for read_{setting} and write_{setting} for each

View File

@ -387,6 +387,7 @@ struct GossipUi {
follow_pubkey: String,
follow_pubkey_at_relay: String,
follow_clear_needs_confirm: bool,
mute_clear_needs_confirm: bool,
password: String,
password2: String,
password3: String,
@ -598,6 +599,7 @@ impl GossipUi {
follow_pubkey: "".to_owned(),
follow_pubkey_at_relay: "".to_owned(),
follow_clear_needs_confirm: false,
mute_clear_needs_confirm: false,
password: "".to_owned(),
password2: "".to_owned(),
password3: "".to_owned(),

View File

@ -1,4 +1,5 @@
use super::{GossipUi, Page};
use crate::comms::ToOverlordMessage;
use crate::globals::GLOBALS;
use crate::people::Person;
use crate::AVATAR_SIZE_F32;
@ -7,8 +8,6 @@ use egui::{Context, Image, RichText, ScrollArea, Sense, Ui, Vec2};
use std::sync::atomic::Ordering;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.add_space(30.0);
let muted_pubkeys = GLOBALS.people.get_muted_pubkeys();
let mut people: Vec<Person> = Vec::new();
for pk in &muted_pubkeys {
@ -18,6 +17,109 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
}
people.sort_unstable();
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 mut asof = "unknown".to_owned();
if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_mute_list_asof) {
if let Ok(formatted) = stamp.format(time::macros::format_description!(
"[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]"
)) {
asof = formatted;
}
}
ui.label(RichText::new(format!("REMOTE: {} (size={})", asof, last_mute_list_size)).size(15.0))
.on_hover_text("This is the data in the latest MuteList event fetched from relays");
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.add_space(30.0);
if ui
.button("↓ Overwrite ↓")
.on_hover_text("This pulls down your Mute List, erasing anything that is already here")
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::UpdateMuteList(false));
}
if ui
.button("↓ Merge ↓")
.on_hover_text("This pulls down your Mute List, merging it into what is already here")
.clicked()
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::UpdateMuteList(true));
}
if GLOBALS.signer.is_ready() {
if ui
.button("↑ Publish ↑")
.on_hover_text("This publishes your Mute List")
.clicked()
{
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushMuteList);
}
}
if GLOBALS.signer.is_ready() {
if app.mute_clear_needs_confirm {
if ui.button("CANCEL").clicked() {
app.mute_clear_needs_confirm = false;
}
if ui.button("YES, CLEAR ALL").clicked() {
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::ClearMuteList);
app.mute_clear_needs_confirm = false;
}
} else {
if ui.button("Clear All").clicked() {
app.mute_clear_needs_confirm = true;
}
}
}
});
ui.add_space(10.0);
let last_mute_list_edit = match GLOBALS.storage.read_last_mute_list_edit() {
Ok(date) => date,
Err(e) => {
tracing::error!("{}", e);
0
}
};
let mut ledit = "unknown".to_owned();
if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_mute_list_edit) {
if let Ok(formatted) = stamp.format(time::macros::format_description!(
"[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]"
)) {
ledit = formatted;
}
}
ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len())).size(15.0))
.on_hover_text("This is the local (and effective) mute list");
if !GLOBALS.signer.is_ready() {
ui.add_space(10.0);
ui.horizontal_wrapped(|ui| {
ui.label("You need to ");
if ui.link("setup your identity").clicked() {
app.set_page(Page::YourKeys);
}
ui.label(" to push.");
});
}
ui.add_space(10.0);
ui.separator();
ui.add_space(10.0);
ui.heading(format!("People who are Muted ({})", people.len()));
ui.add_space(10.0);