diff --git a/src/db/person_relay.rs b/src/db/person_relay.rs index 190d18d7..851e85a1 100644 --- a/src/db/person_relay.rs +++ b/src/db/person_relay.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::globals::GLOBALS; -use nostr_types::PublicKeyHex; +use nostr_types::{PublicKeyHex, Url}; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; @@ -216,7 +216,6 @@ impl DbPersonRelay { Ok(()) } - pub async fn upsert_last_suggested_bytag( person: String, relay: String, @@ -330,6 +329,35 @@ impl DbPersonRelay { output } + #[allow(dead_code)] + pub async fn get_best_relay(pubkey: PublicKeyHex) -> Result, Error> { + // This is the ranking we are using. There might be reasons + // for ranking differently: + // nip23 > kind3 > nip05 > kind2 > fetched > bytag + + let sql = "SELECT relay FROM person_relay WHERE person=? \ + ORDER BY last_suggested_nip23 DESC, last_suggested_kind3 DESC, \ + last_suggested_nip05 DESC, last_suggested_kind2 DESC, \ + last_fetched DESC, last_suggested_bytag DESC"; + + let maybe_relay_result: Result, Error> = spawn_blocking(move || { + let maybe_db = GLOBALS.db.blocking_lock(); + let db = maybe_db.as_ref().unwrap(); + let mut stmt = db.prepare(sql)?; + stmt.raw_bind_parameter(1, &pubkey.0)?; + let mut rows = stmt.raw_query(); + let mut maybe_relay: Option = None; + if let Some(row) = rows.next()? { + let s: String = row.get(0)?; + maybe_relay = Some(Url::new(&s)); + } + Ok(maybe_relay) + }) + .await?; + + maybe_relay_result + } + /* pub async fn delete(criteria: &str) -> Result<(), Error> { let sql = format!("DELETE FROM person_relay WHERE {}", criteria); diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index 95090275..6fe83e1c 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -473,7 +473,7 @@ impl Overlord { self.pull_following(false).await?; } ToOverlordMessage::PushFollow => { - tracing::error!("Push Follow Unimplemented"); + self.push_following().await?; } ToOverlordMessage::SaveRelays => { let dirty_relays: Vec = GLOBALS @@ -919,6 +919,41 @@ impl Overlord { Ok(()) } + async fn push_following(&mut self) -> Result<(), Error> { + let event = GLOBALS.people.generate_contact_list_event().await?; + + // Push to all of the relays we post to + let relays: Vec = GLOBALS + .relays + .read() + .await + .iter() + .filter_map(|(_, r)| if r.post { Some(r.to_owned()) } else { None }) + .collect(); + + for relay in relays { + // Start a minion for it, if there is none + if !GLOBALS + .relays_watching + .read() + .await + .contains(&Url::new(&relay.url)) + { + self.start_minion(relay.url.clone()).await?; + } + + // Send it the event to pull our followers + tracing::debug!("Pushing ContactList to {}", &relay.url); + + let _ = self.to_minions.send(ToMinionMessage { + target: relay.url.clone(), + payload: ToMinionPayload::PostEvent(Box::new(event.clone())), + }); + } + + Ok(()) + } + async fn set_thread_feed(&mut self, id: Id, referenced_by: Id) -> Result<(), Error> { // Cancel current thread subscriptions, if any let _ = self.to_minions.send(ToMinionMessage { diff --git a/src/people.rs b/src/people.rs index 7d15c138..512ca1dd 100644 --- a/src/people.rs +++ b/src/people.rs @@ -1,4 +1,4 @@ -use crate::db::DbPerson; +use crate::db::{DbPerson, DbPersonRelay}; use crate::error::Error; use crate::globals::GLOBALS; use crate::AVATAR_SIZE; @@ -6,7 +6,7 @@ use dashmap::{DashMap, DashSet}; use eframe::egui::ColorImage; use egui_extras::image::FitTo; use image::imageops::FilterType; -use nostr_types::{Metadata, PublicKeyHex, Unixtime, Url}; +use nostr_types::{Event, EventKind, Metadata, PreEvent, PublicKeyHex, Tag, Unixtime, Url}; use std::cmp::Ordering; use std::time::Duration; use tokio::task; @@ -438,6 +438,43 @@ impl People { Ok(()) } + #[allow(dead_code)] + pub 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 best relay + let maybeurl = DbPersonRelay::get_best_relay(pubkey.clone()).await?; + + p_tags.push(Tag::Pubkey { + pubkey: pubkey.clone(), + recommended_relay_url: maybeurl, + petname: None, + }); + } + + let public_key = match GLOBALS.signer.read().await.public_key() { + Some(pk) => pk, + None => return Err(Error::NoPrivateKey), // not even a public key + }; + + // NOTICE - some clients are stuffing relay following data into the content + // of `ContactList`s. We don't have a set of relays that we read from, so + // we could only do half of that even if we wanted to, and I'm not sure only + // putting in write relays is of any use. + let pre_event = PreEvent { + pubkey: public_key, + created_at: Unixtime::now().unwrap(), + kind: EventKind::ContactList, + tags: p_tags, + content: "".to_owned(), + ots: None, + }; + + GLOBALS.signer.read().await.sign_preevent(pre_event, None) + } + pub fn follow(&self, pubkeyhex: &PublicKeyHex, follow: bool) { // We can't do it now, but we spawn a task to do it soon let pubkeyhex = pubkeyhex.to_owned(); diff --git a/src/ui/people/mod.rs b/src/ui/people/mod.rs index cfd1a82f..f51aebb2 100644 --- a/src/ui/people/mod.rs +++ b/src/ui/people/mod.rs @@ -44,11 +44,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra if ui.button("↓ PULL ↓\nMerge (Add)").clicked() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PullFollowMerge); } - /* not yet implemented + if ui.button("↑ PUSH ↑\n").clicked() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushFollow); } - */ }); ui.add_space(10.0);