NIP-35 support

This commit is contained in:
Mike Dilger 2022-12-27 09:05:01 +13:00
parent f6341fed47
commit 5653bb781a
6 changed files with 211 additions and 39 deletions

View File

@ -164,6 +164,34 @@ impl DbPerson {
Ok(()) Ok(())
} }
pub async fn upsert_valid_nip05(
pubkey: PublicKeyHex,
dns_id: String,
dns_id_last_checked: u64,
) -> Result<(), Error> {
let sql = "INSERT INTO person (pubkey, dns_id, dns_id_valid, dns_id_last_checked, followed) \
values (?, ?, 1, ?, 1) \
ON CONFLICT(pubkey) DO UPDATE SET dns_id=?, dns_id_valid=1, dns_id_last_checked=?, followed=1";
spawn_blocking(move || {
let maybe_db = GLOBALS.db.blocking_lock();
let db = maybe_db.as_ref().unwrap();
let mut stmt = db.prepare(sql)?;
stmt.execute((
&pubkey.0,
&dns_id,
&dns_id_last_checked,
&dns_id,
&dns_id_last_checked,
))?;
Ok::<(), Error>(())
})
.await??;
Ok(())
}
#[allow(dead_code)] #[allow(dead_code)]
pub async fn delete(criteria: &str) -> Result<(), Error> { pub async fn delete(criteria: &str) -> Result<(), Error> {
let sql = format!("DELETE FROM person WHERE {}", criteria); let sql = format!("DELETE FROM person WHERE {}", criteria);

View File

@ -213,6 +213,33 @@ impl DbPersonRelay {
Ok(()) Ok(())
} }
pub async fn upsert_last_suggested_nip35(
person: PublicKeyHex,
relay: String,
last_suggested_nip35: u64,
) -> Result<(), Error> {
let sql = "INSERT INTO person_relay (person, relay, last_suggested_nip35) \
VALUES (?, ?, ?) \
ON CONFLICT(person, relay) DO UPDATE SET last_suggested_nip35=?";
spawn_blocking(move || {
let maybe_db = GLOBALS.db.blocking_lock();
let db = maybe_db.as_ref().unwrap();
let mut stmt = db.prepare(sql)?;
stmt.execute((
&person.0,
&relay,
&last_suggested_nip35,
&last_suggested_nip35,
))?;
Ok::<(), Error>(())
})
.await??;
Ok(())
}
#[allow(dead_code)] #[allow(dead_code)]
pub async fn delete(criteria: &str) -> Result<(), Error> { pub async fn delete(criteria: &str) -> Result<(), Error> {
let sql = format!("DELETE FROM person_relay WHERE {}", criteria); let sql = format!("DELETE FROM person_relay WHERE {}", criteria);

View File

@ -21,6 +21,12 @@ pub enum Error {
#[error("Error sending mpsc: {0}")] #[error("Error sending mpsc: {0}")]
MpscSend(#[from] tokio::sync::mpsc::error::SendError<BusMessage>), MpscSend(#[from] tokio::sync::mpsc::error::SendError<BusMessage>),
#[error("NIP-05 public key not found")]
Nip05NotFound,
#[error("NIP-35 relays not found")]
Nip35NotFound,
#[error("Nostr: {0}")] #[error("Nostr: {0}")]
Nostr(#[from] nostr_types::Error), Nostr(#[from] nostr_types::Error),
@ -30,12 +36,18 @@ pub enum Error {
#[error("I/O Error: {0}")] #[error("I/O Error: {0}")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("Invalid DNS ID (nip-05 / nip-35), should be user@domain")]
InvalidDnsId,
#[error("Invalid URI: {0}")] #[error("Invalid URI: {0}")]
InvalidUri(#[from] http::uri::InvalidUri), InvalidUri(#[from] http::uri::InvalidUri),
#[error("Bad integer: {0}")] #[error("Bad integer: {0}")]
ParseInt(#[from] std::num::ParseIntError), ParseInt(#[from] std::num::ParseIntError),
#[error("HTTP (reqwest) error: {0}")]
ReqwestHttpError(#[from] reqwest::Error),
#[error("SerdeJson Error: {0}")] #[error("SerdeJson Error: {0}")]
SerdeJson(#[from] serde_json::Error), SerdeJson(#[from] serde_json::Error),

View File

@ -7,7 +7,7 @@ use crate::error::Error;
use crate::globals::{Globals, GLOBALS}; use crate::globals::{Globals, GLOBALS};
use crate::settings::Settings; use crate::settings::Settings;
use minion::Minion; use minion::Minion;
use nostr_types::{Event, PrivateKey, PublicKey, PublicKeyHex, Unixtime, Url}; use nostr_types::{Event, Nip05, PrivateKey, PublicKey, PublicKeyHex, Unixtime, Url};
use relay_picker::{BestRelay, RelayPicker}; use relay_picker::{BestRelay, RelayPicker};
use std::collections::HashMap; use std::collections::HashMap;
use tokio::sync::broadcast::Sender; use tokio::sync::broadcast::Sender;
@ -349,6 +349,14 @@ impl Overlord {
"get_missing_events" => { "get_missing_events" => {
self.get_missing_events().await?; self.get_missing_events().await?;
} }
"follow_nip35" => {
let dns_id: String = serde_json::from_str(&bus_message.json_payload)?;
let _ = tokio::spawn(async move {
if let Err(e) = Overlord::get_and_follow_nip35(dns_id).await {
error!("{}", e);
}
});
}
_ => {} _ => {}
}, },
_ => {} _ => {}
@ -399,4 +407,61 @@ impl Overlord {
Ok(()) Ok(())
} }
async fn get_and_follow_nip35(nip35: String) -> Result<(), Error> {
let mut parts: Vec<&str> = nip35.split('@').collect();
if parts.len() != 2 {
return Err(Error::InvalidDnsId);
}
let domain = parts.pop().unwrap();
let user = parts.pop().unwrap();
let nip05_future = reqwest::Client::new()
.get(format!(
"https://{}/.well-known/nostr.json?name={}",
domain, user
))
.header("Host", domain)
.send();
let timeout_future = tokio::time::timeout(std::time::Duration::new(15, 0), nip05_future);
let response = timeout_future.await??;
let nip05 = response.json::<Nip05>().await?;
Overlord::follow_nip35(nip05, user.to_string(), domain.to_string()).await?;
Ok(())
}
async fn follow_nip35(nip05: Nip05, user: String, domain: String) -> Result<(), Error> {
let dns_id = format!("{}@{}", user, domain);
let pubkey = match nip05.names.get(&user) {
Some(pk) => pk,
None => return Err(Error::Nip05NotFound),
};
let relays = match nip05.relays.get(pubkey) {
Some(relays) => relays,
None => return Err(Error::Nip35NotFound),
};
// Save person
DbPerson::upsert_valid_nip05((*pubkey).into(), dns_id, Unixtime::now().unwrap().0 as u64)
.await?;
for relay in relays.iter() {
// Save relay
let db_relay = DbRelay::new(relay.to_string())?;
DbRelay::insert(db_relay).await?;
DbPersonRelay::upsert_last_suggested_nip35(
(*pubkey).into(),
relay.0.clone(),
Unixtime::now().unwrap().0 as u64,
)
.await?;
}
info!("Followed {}@{} at {} relays", user, domain, relays.len());
Ok(())
}
} }

View File

@ -47,7 +47,8 @@ pub fn run() -> Result<(), Error> {
#[derive(PartialEq)] #[derive(PartialEq)]
enum Page { enum Page {
Feed, Feed,
People, PeopleFollow,
PeopleList,
You, You,
Relays, Relays,
Settings, Settings,
@ -62,6 +63,7 @@ struct GossipUi {
placeholder_avatar: TextureHandle, placeholder_avatar: TextureHandle,
draft: String, draft: String,
settings: Settings, settings: Settings,
nip35follow: String,
} }
impl GossipUi { impl GossipUi {
@ -113,6 +115,7 @@ impl GossipUi {
placeholder_avatar: placeholder_avatar_texture_handle, placeholder_avatar: placeholder_avatar_texture_handle,
draft: "".to_owned(), draft: "".to_owned(),
settings, settings,
nip35follow: "".to_owned(),
} }
} }
} }
@ -132,7 +135,7 @@ impl eframe::App for GossipUi {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.selectable_value(&mut self.page, Page::Feed, "Feed"); ui.selectable_value(&mut self.page, Page::Feed, "Feed");
ui.separator(); ui.separator();
ui.selectable_value(&mut self.page, Page::People, "People"); ui.selectable_value(&mut self.page, Page::PeopleList, "People");
ui.separator(); ui.separator();
ui.selectable_value(&mut self.page, Page::You, "You"); ui.selectable_value(&mut self.page, Page::You, "You");
ui.separator(); ui.separator();
@ -149,7 +152,8 @@ impl eframe::App for GossipUi {
egui::CentralPanel::default().show(ctx, |ui| match self.page { egui::CentralPanel::default().show(ctx, |ui| match self.page {
Page::Feed => feed::update(self, ctx, frame, ui), Page::Feed => feed::update(self, ctx, frame, ui),
Page::People => people::update(self, ctx, frame, ui), Page::PeopleList => people::update(self, ctx, frame, ui),
Page::PeopleFollow => people::update(self, ctx, frame, ui),
Page::You => you::update(self, ctx, frame, ui), Page::You => you::update(self, ctx, frame, ui),
Page::Relays => relays::update(self, ctx, frame, ui), Page::Relays => relays::update(self, ctx, frame, ui),
Page::Settings => settings::update(self, ctx, frame, ui, darkmode), Page::Settings => settings::update(self, ctx, frame, ui, darkmode),

View File

@ -1,9 +1,44 @@
use super::GossipUi; use super::{GossipUi, Page};
use crate::comms::BusMessage;
use crate::globals::GLOBALS; use crate::globals::GLOBALS;
use eframe::egui; use eframe::egui;
use egui::{Context, RichText, ScrollArea, TextStyle, Ui, Vec2}; use egui::{Context, RichText, ScrollArea, TextStyle, TopBottomPanel, Ui, Vec2};
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
TopBottomPanel::top("people_menu").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut app.page, Page::PeopleList, "Followed");
ui.separator();
ui.selectable_value(&mut app.page, Page::PeopleFollow, "Follow Someone New");
ui.separator();
});
});
if app.page == Page::PeopleFollow {
ui.add_space(24.0);
ui.add_space(8.0);
ui.heading("Follow someone");
ui.add_space(18.0);
ui.separator();
ui.horizontal(|ui| {
ui.label("Enter user@domain");
ui.text_edit_singleline(&mut app.nip35follow);
if ui.button("follow").clicked() {
let tx = GLOBALS.to_overlord.clone();
let _ = tx.send(BusMessage {
target: "overlord".to_string(),
kind: "follow_nip35".to_string(),
json_payload: serde_json::to_string(&app.nip35follow).unwrap(),
});
app.nip35follow = "".to_owned();
}
});
} else if app.page == Page::PeopleList {
ui.add_space(24.0);
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
ui.add_space(8.0); ui.add_space(8.0);
ui.heading("People Followed"); ui.heading("People Followed");
ui.add_space(18.0); ui.add_space(18.0);
@ -50,3 +85,4 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
} }
}); });
} }
}