From c3334d91cf0e57a4f26dd8bcdc4b793a92912f56 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Tue, 6 Aug 2024 08:24:12 +0700 Subject: [PATCH] feat: add relay manager --- src-tauri/resources/relays.txt | 4 + src-tauri/src/commands/account.rs | 50 +--------- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/relay.rs | 82 ++++++++++++++++ src-tauri/src/main.rs | 50 ++++++++-- src-tauri/tauri.conf.json | 3 + src/commands.ts | 48 ++++++--- src/routes.gen.ts | 62 ++++++++---- src/routes/$account.relays.lazy.tsx | 20 ++-- src/routes/$account.relays.tsx | 14 +++ src/routes/bootstrap-relays.lazy.tsx | 142 +++++++++++++++++++++++++++ src/routes/bootstrap-relays.tsx | 14 +++ src/routes/index.lazy.tsx | 13 ++- 13 files changed, 396 insertions(+), 107 deletions(-) create mode 100644 src-tauri/resources/relays.txt create mode 100644 src-tauri/src/commands/relay.rs create mode 100644 src/routes/$account.relays.tsx create mode 100644 src/routes/bootstrap-relays.lazy.tsx create mode 100644 src/routes/bootstrap-relays.tsx diff --git a/src-tauri/resources/relays.txt b/src-tauri/resources/relays.txt new file mode 100644 index 0000000..d9119ab --- /dev/null +++ b/src-tauri/resources/relays.txt @@ -0,0 +1,4 @@ +wss://purplepag.es/, +wss://directory.yabu.me/, +wss://user.kindpag.es/, +wss://relay.nos.social/, diff --git a/src-tauri/src/commands/account.rs b/src-tauri/src/commands/account.rs index 0badc68..72fa1da 100644 --- a/src-tauri/src/commands/account.rs +++ b/src-tauri/src/commands/account.rs @@ -5,7 +5,7 @@ use serde::Serialize; use std::{collections::HashSet, str::FromStr, time::Duration}; use tauri::{Emitter, Manager, State}; -use crate::{Nostr, BOOTSTRAP_RELAYS}; +use crate::Nostr; #[derive(Clone, Serialize)] pub struct EventPayload { @@ -32,8 +32,7 @@ pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result { if let Some(event) = events.first() { Ok(Metadata::from_json(&event.content).unwrap_or(Metadata::new()).as_json()) @@ -171,51 +170,6 @@ pub async fn get_contact_list(state: State<'_, Nostr>) -> Result, St } } -#[tauri::command] -#[specta::specta] -pub async fn get_inbox(id: String, state: State<'_, Nostr>) -> Result, String> { - let client = &state.client; - let public_key = PublicKey::parse(id).map_err(|e| e.to_string())?; - let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1); - - match client.get_events_from(BOOTSTRAP_RELAYS, vec![inbox], None).await { - Ok(events) => { - if let Some(event) = events.into_iter().next() { - let urls = event - .tags() - .iter() - .filter_map(|tag| { - if let Some(TagStandard::Relay(relay)) = tag.as_standardized() { - Some(relay.to_string()) - } else { - None - } - }) - .collect::>(); - - Ok(urls) - } else { - Ok(Vec::new()) - } - } - Err(e) => Err(e.to_string()), - } -} - -#[tauri::command] -#[specta::specta] -pub async fn set_inbox(relays: Vec, state: State<'_, Nostr>) -> Result<(), String> { - let client = &state.client; - - let tags = relays.into_iter().map(|t| Tag::custom(TagKind::Relay, vec![t])).collect::>(); - let event = EventBuilder::new(Kind::Custom(10050), "", tags); - - match client.send_event_builder(event).await { - Ok(_) => Ok(()), - Err(e) => Err(e.to_string()), - } -} - #[tauri::command] #[specta::specta] pub async fn login( diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index d0f897b..e0068a5 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,2 +1,3 @@ pub mod account; pub mod chat; +pub mod relay; diff --git a/src-tauri/src/commands/relay.rs b/src-tauri/src/commands/relay.rs new file mode 100644 index 0000000..816a480 --- /dev/null +++ b/src-tauri/src/commands/relay.rs @@ -0,0 +1,82 @@ +use nostr_sdk::prelude::*; +use std::{ + fs::OpenOptions, + io::{self, BufRead, Write}, +}; +use tauri::{Manager, State}; + +use crate::Nostr; + +#[tauri::command] +#[specta::specta] +pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result, String> { + let relays_path = app + .path() + .resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource) + .map_err(|e| e.to_string())?; + + let file = std::fs::File::open(relays_path).map_err(|e| e.to_string())?; + let reader = io::BufReader::new(file); + + reader.lines().collect::, io::Error>>().map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub fn set_bootstrap_relays(relays: String, app: tauri::AppHandle) -> Result<(), String> { + let relays_path = app + .path() + .resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource) + .map_err(|e| e.to_string())?; + let mut file = OpenOptions::new().write(true).open(relays_path).map_err(|e| e.to_string())?; + + file.write_all(relays.as_bytes()).map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub async fn get_inbox_relays( + user_id: String, + state: State<'_, Nostr>, +) -> Result, String> { + let client = &state.client; + let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?; + let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1); + + match client.get_events_of(vec![inbox], None).await { + Ok(events) => { + if let Some(event) = events.into_iter().next() { + let urls = event + .tags() + .iter() + .filter_map(|tag| { + if let Some(TagStandard::Relay(relay)) = tag.as_standardized() { + Some(relay.to_string()) + } else { + None + } + }) + .collect::>(); + + Ok(urls) + } else { + Ok(Vec::new()) + } + } + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +#[specta::specta] +pub async fn set_inbox_relays(relays: Vec, state: State<'_, Nostr>) -> Result<(), String> { + let client = &state.client; + + let tags = relays.into_iter().map(|t| Tag::custom(TagKind::Relay, vec![t])).collect::>(); + let event = EventBuilder::new(Kind::Custom(10050), "", tags); + + match client.send_event_builder(event).await { + Ok(_) => Ok(()), + Err(e) => Err(e.to_string()), + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5434a02..c367742 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,12 +4,18 @@ #[cfg(target_os = "macos")] use border::WebviewWindowExt as WebviewWindowExtAlt; use nostr_sdk::prelude::*; -use std::{collections::HashMap, fs, time::Duration}; +use std::{ + collections::HashMap, + fs, + io::{self, BufRead}, + str::FromStr, + time::Duration, +}; use tauri::{async_runtime::Mutex, Manager}; #[cfg(not(target_os = "linux"))] use tauri_plugin_decorum::WebviewWindowExt; -use commands::{account::*, chat::*}; +use commands::{account::*, chat::*, relay::*}; mod commands; @@ -18,12 +24,13 @@ pub struct Nostr { inbox_relays: Mutex>>, } -// TODO: Allow user config bootstrap relays. -pub const BOOTSTRAP_RELAYS: [&str; 2] = ["wss://relay.damus.io/", "wss://relay.nostr.net/"]; - fn main() { let invoke_handler = { let builder = tauri_specta::ts::builder().commands(tauri_specta::collect_commands![ + get_bootstrap_relays, + set_bootstrap_relays, + get_inbox_relays, + set_inbox_relays, login, delete_account, create_account, @@ -34,8 +41,6 @@ fn main() { get_contact_list, get_chats, get_chat_messages, - get_inbox, - set_inbox, connect_inbox, disconnect_inbox, send_message, @@ -102,8 +107,35 @@ fn main() { let client = ClientBuilder::default().opts(opts).database(database).build(); - // Add bootstrap relay - let _ = client.add_relays(BOOTSTRAP_RELAYS).await; + // Add bootstrap relays + if let Ok(path) = handle + .path() + .resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource) + { + let file = std::fs::File::open(&path).unwrap(); + let lines = io::BufReader::new(file).lines(); + + // Add bootstrap relays to relay pool + for line in lines.map_while(Result::ok) { + if let Some((relay, option)) = line.split_once(',') { + match RelayMetadata::from_str(option) { + Ok(meta) => { + println!("Connecting to relay...: {} - {}", relay, meta); + let opts = if meta == RelayMetadata::Read { + RelayOptions::new().read(true).write(false) + } else { + RelayOptions::new().write(true).read(false) + }; + let _ = client.add_relay_with_opts(relay, opts).await; + } + Err(_) => { + println!("Connecting to relay...: {}", relay); + let _ = client.add_relay(relay).await; + } + } + } + } + } // Connect client.connect().await; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cdbc4d6..a5d4462 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -38,6 +38,9 @@ "targets": "all", "active": true, "category": "SocialNetworking", + "resources": [ + "resources/*" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/commands.ts b/src/commands.ts index 5f6c7f3..6a8e742 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -4,6 +4,38 @@ /** user-defined commands **/ export const commands = { +async getBootstrapRelays() : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("get_bootstrap_relays") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setBootstrapRelays(relays: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("set_bootstrap_relays", { relays }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async getInboxRelays(userId: string) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("get_inbox_relays", { userId }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, +async setInboxRelays(relays: string[]) : Promise> { +try { + return { status: "ok", data: await TAURI_INVOKE("set_inbox_relays", { relays }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async login(account: string, password: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("login", { account, password }) }; @@ -79,22 +111,6 @@ try { else return { status: "error", error: e as any }; } }, -async getInbox(id: string) : Promise> { -try { - return { status: "ok", data: await TAURI_INVOKE("get_inbox", { id }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async setInbox(relays: string[]) : Promise> { -try { - return { status: "ok", data: await TAURI_INVOKE("set_inbox", { relays }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, async connectInbox(id: string) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("connect_inbox", { id }) }; diff --git a/src/routes.gen.ts b/src/routes.gen.ts index d8d4fa1..984950f 100644 --- a/src/routes.gen.ts +++ b/src/routes.gen.ts @@ -13,7 +13,9 @@ import { createFileRoute } from '@tanstack/react-router' // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as BootstrapRelaysImport } from './routes/bootstrap-relays' import { Route as IndexImport } from './routes/index' +import { Route as AccountRelaysImport } from './routes/$account.relays' import { Route as AccountContactsImport } from './routes/$account.contacts' import { Route as AccountChatsIdImport } from './routes/$account.chats.$id' @@ -23,7 +25,6 @@ const NostrConnectLazyImport = createFileRoute('/nostr-connect')() const NewLazyImport = createFileRoute('/new')() const ImportKeyLazyImport = createFileRoute('/import-key')() const CreateAccountLazyImport = createFileRoute('/create-account')() -const AccountRelaysLazyImport = createFileRoute('/$account/relays')() const AccountChatsLazyImport = createFileRoute('/$account/chats')() const AccountChatsNewLazyImport = createFileRoute('/$account/chats/new')() @@ -51,18 +52,18 @@ const CreateAccountLazyRoute = CreateAccountLazyImport.update({ import('./routes/create-account.lazy').then((d) => d.Route), ) +const BootstrapRelaysRoute = BootstrapRelaysImport.update({ + path: '/bootstrap-relays', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/bootstrap-relays.lazy').then((d) => d.Route), +) + const IndexRoute = IndexImport.update({ path: '/', getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) -const AccountRelaysLazyRoute = AccountRelaysLazyImport.update({ - path: '/$account/relays', - getParentRoute: () => rootRoute, -} as any).lazy(() => - import('./routes/$account.relays.lazy').then((d) => d.Route), -) - const AccountChatsLazyRoute = AccountChatsLazyImport.update({ path: '/$account/chats', getParentRoute: () => rootRoute, @@ -70,6 +71,13 @@ const AccountChatsLazyRoute = AccountChatsLazyImport.update({ import('./routes/$account.chats.lazy').then((d) => d.Route), ) +const AccountRelaysRoute = AccountRelaysImport.update({ + path: '/$account/relays', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/$account.relays.lazy').then((d) => d.Route), +) + const AccountContactsRoute = AccountContactsImport.update({ path: '/$account/contacts', getParentRoute: () => rootRoute, @@ -102,6 +110,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } + '/bootstrap-relays': { + id: '/bootstrap-relays' + path: '/bootstrap-relays' + fullPath: '/bootstrap-relays' + preLoaderRoute: typeof BootstrapRelaysImport + parentRoute: typeof rootRoute + } '/create-account': { id: '/create-account' path: '/create-account' @@ -137,6 +152,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AccountContactsImport parentRoute: typeof rootRoute } + '/$account/relays': { + id: '/$account/relays' + path: '/$account/relays' + fullPath: '/$account/relays' + preLoaderRoute: typeof AccountRelaysImport + parentRoute: typeof rootRoute + } '/$account/chats': { id: '/$account/chats' path: '/$account/chats' @@ -144,13 +166,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AccountChatsLazyImport parentRoute: typeof rootRoute } - '/$account/relays': { - id: '/$account/relays' - path: '/$account/relays' - fullPath: '/$account/relays' - preLoaderRoute: typeof AccountRelaysLazyImport - parentRoute: typeof rootRoute - } '/$account/chats/$id': { id: '/$account/chats/$id' path: '/$id' @@ -172,16 +187,17 @@ declare module '@tanstack/react-router' { export const routeTree = rootRoute.addChildren({ IndexRoute, + BootstrapRelaysRoute, CreateAccountLazyRoute, ImportKeyLazyRoute, NewLazyRoute, NostrConnectLazyRoute, AccountContactsRoute, + AccountRelaysRoute, AccountChatsLazyRoute: AccountChatsLazyRoute.addChildren({ AccountChatsIdRoute, AccountChatsNewLazyRoute, }), - AccountRelaysLazyRoute, }) /* prettier-ignore-end */ @@ -193,18 +209,22 @@ export const routeTree = rootRoute.addChildren({ "filePath": "__root.tsx", "children": [ "/", + "/bootstrap-relays", "/create-account", "/import-key", "/new", "/nostr-connect", "/$account/contacts", - "/$account/chats", - "/$account/relays" + "/$account/relays", + "/$account/chats" ] }, "/": { "filePath": "index.tsx" }, + "/bootstrap-relays": { + "filePath": "bootstrap-relays.tsx" + }, "/create-account": { "filePath": "create-account.lazy.tsx" }, @@ -220,6 +240,9 @@ export const routeTree = rootRoute.addChildren({ "/$account/contacts": { "filePath": "$account.contacts.tsx" }, + "/$account/relays": { + "filePath": "$account.relays.tsx" + }, "/$account/chats": { "filePath": "$account.chats.lazy.tsx", "children": [ @@ -227,9 +250,6 @@ export const routeTree = rootRoute.addChildren({ "/$account/chats/new" ] }, - "/$account/relays": { - "filePath": "$account.relays.lazy.tsx" - }, "/$account/chats/$id": { "filePath": "$account.chats.$id.tsx", "parent": "/$account/chats" diff --git a/src/routes/$account.relays.lazy.tsx b/src/routes/$account.relays.lazy.tsx index 9c282c2..84f475f 100644 --- a/src/routes/$account.relays.lazy.tsx +++ b/src/routes/$account.relays.lazy.tsx @@ -12,6 +12,7 @@ export const Route = createLazyFileRoute("/$account/relays")({ function Screen() { const navigate = Route.useNavigate(); + const inboxRelays = Route.useLoaderData(); const { account } = Route.useParams(); const [newRelay, setNewRelay] = useState(""); @@ -43,6 +44,10 @@ function Screen() { } }; + const remove = (relay: string) => { + setRelays((prev) => prev.filter((item) => item !== relay)); + }; + const submit = async () => { startTransition(async () => { if (!relays.length) { @@ -50,7 +55,7 @@ function Screen() { return; } - const res = await commands.setInbox(relays); + const res = await commands.setInboxRelays(relays); if (res.status === "ok") { navigate({ @@ -69,16 +74,8 @@ function Screen() { }; useEffect(() => { - async function getRelays() { - const res = await commands.getInbox(account); - - if (res.status === "ok") { - setRelays((prev) => [...prev, ...res.data]); - } - } - - getRelays(); - }, []); + setRelays(inboxRelays); + }, [inboxRelays]); return (
@@ -140,6 +137,7 @@ function Screen() {
+
+
+ {relays.map((relay) => ( +
+
{relay}
+
+ +
+
+ ))} +
+ +
+ +
+
+ + + ); +} diff --git a/src/routes/bootstrap-relays.tsx b/src/routes/bootstrap-relays.tsx new file mode 100644 index 0000000..681b8e4 --- /dev/null +++ b/src/routes/bootstrap-relays.tsx @@ -0,0 +1,14 @@ +import { commands } from "@/commands"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/bootstrap-relays")({ + loader: async () => { + const res = await commands.getBootstrapRelays(); + + if (res.status === "ok") { + return res.data.map((item) => item.replace(",", "")); + } else { + throw new Error(res.error); + } + }, +}); diff --git a/src/routes/index.lazy.tsx b/src/routes/index.lazy.tsx index f05e383..bb478f5 100644 --- a/src/routes/index.lazy.tsx +++ b/src/routes/index.lazy.tsx @@ -3,7 +3,7 @@ import { npub } from "@/commons"; import { Frame } from "@/components/frame"; import { Spinner } from "@/components/spinner"; import { User } from "@/components/user"; -import { ArrowRight, DotsThree, Plus } from "@phosphor-icons/react"; +import { ArrowRight, DotsThree, GearSix, Plus } from "@phosphor-icons/react"; import { Link, createLazyFileRoute } from "@tanstack/react-router"; import { Menu, MenuItem } from "@tauri-apps/api/menu"; import { message } from "@tauri-apps/plugin-dialog"; @@ -103,7 +103,7 @@ function Screen() { return (
@@ -190,6 +190,15 @@ function Screen() {
+
+ + + Manage Relays + +
); }