What is Nostr?
Kind 1617
git patch: add nostr_url nip05 whilst retaining structure
Author Public Key
npub1elta7cneng3w8p9y4dw633qzdjr4kyvaparuyuttyrx6e8xp7xnq32cume
Published at
2024-12-09 22:07:57
Kind type
1617
Event JSON
{ "id": "ff1845c0d52fc6e177767aa2b111b3130c06e2faf57d614080043a135ecb8079", "pubkey": "cfd7df62799a22e384a4ab5da8c4026c875b119d0f47c2716b20cdac9cc1f1a6", "created_at": 1733782077, "kind": 1617, "tags": [ [ "a", "30617:a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d:ngit" ], [ "r", "26689f97810fc656c7134c76e2a37d33b2e40ce7" ], [ "r", "b73f311b6eb52f3221b0f0bff9bc31dd17483f3a" ], [ "alt", "git patch: add nostr_url nip05 whilst retaining structure" ], [ "t", "root" ], [ "branch-name", "nip05-lez" ], [ "p", "a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d" ], [ "commit", "b73f311b6eb52f3221b0f0bff9bc31dd17483f3a" ], [ "parent-commit", "f0d0e1ba1cba11d3a98a5ab0c7f1dc72b6bc4e17" ], [ "commit-pgp-sig", "" ], [ "description", "add nostr_url nip05 whilst retaining structure\n\nFollowing the discussion:\nnostr:nevent1qvzqqqqqqypzpn7hma38nx3zuwz2f26a4rzqymy8tvge6r68cfckkgxd4jwvrudxqyt8wumn8ghj7ur4wfcxcetjv4kxz7fwvdhk6tcppemhxue69uhkummn9ekx7mp0qythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qyt8wumn8ghj7ur4wfcxcetjv4kxz7fwvdhk6tcppemhxue69uhkummn9ekx7mp0qythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qqsf26xu3q0x4mzfz2nu76pcj42qthh30hwwrclzdhj3n60qvwe54dcrghvk2\nI've pulled this together to demonstrate how this feature could be\nadded to `NostrUrlDecoded` without dramatically changing its\nstructure or adding a similar struct which omits the `PublicKey`.\n\nit replaces `NostrUrlDecoded::from_str` with\n`NostrUrlDecoded::parse_and_resolve`.\n\nIn this WIP I have began to add nip05 to repo_ref so that\n`RepoRef::to_nostr_git_url` can render the nip05 when originally\nused. Unfortunately, there is more work to as the type of\n`trusted_maintainer` needs to change to `NostrUrlDecoded`.\n\nThere may still be instances where the variable name\n`repo_coordinate` is used to create repo_refs when it is exclusively\nrefering to trusted maintainers and not other repo maintainers.\nthese should use the type `NostrUrlDecoded` too, to ensure\n`RepoRef::to_nostr_git_url` always includes nip05 when originally\ntaken from the nostr_url.\n" ], [ "author", "DanConwayDev", "DanConwayDev@protonmail.com", "1733736848", "0" ], [ "committer", "Laszlo Megyer", "lez@github.com", "1733746277", "60" ] ], "content": "From b73f311b6eb52f3221b0f0bff9bc31dd17483f3a Mon Sep 17 00:00:00 2001\nFrom: DanConwayDev \u003cDanConwayDev@protonmail.com\u003e\nDate: Mon, 9 Dec 2024 09:34:08 +0000\nSubject: [PATCH 1/2] add nostr_url nip05 whilst retaining structure\n\nFollowing the discussion:\nnostr:nevent1qvzqqqqqqypzpn7hma38nx3zuwz2f26a4rzqymy8tvge6r68cfckkgxd4jwvrudxqyt8wumn8ghj7ur4wfcxcetjv4kxz7fwvdhk6tcppemhxue69uhkummn9ekx7mp0qythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qyt8wumn8ghj7ur4wfcxcetjv4kxz7fwvdhk6tcppemhxue69uhkummn9ekx7mp0qythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qqsf26xu3q0x4mzfz2nu76pcj42qthh30hwwrclzdhj3n60qvwe54dcrghvk2\nI've pulled this together to demonstrate how this feature could be\nadded to `NostrUrlDecoded` without dramatically changing its\nstructure or adding a similar struct which omits the `PublicKey`.\n\nit replaces `NostrUrlDecoded::from_str` with\n`NostrUrlDecoded::parse_and_resolve`.\n\nIn this WIP I have began to add nip05 to repo_ref so that\n`RepoRef::to_nostr_git_url` can render the nip05 when originally\nused. Unfortunately, there is more work to as the type of\n`trusted_maintainer` needs to change to `NostrUrlDecoded`.\n\nThere may still be instances where the variable name\n`repo_coordinate` is used to create repo_refs when it is exclusively\nrefering to trusted maintainers and not other repo maintainers.\nthese should use the type `NostrUrlDecoded` too, to ensure\n`RepoRef::to_nostr_git_url` always includes nip05 when originally\ntaken from the nostr_url.\n---\n src/bin/git_remote_nostr/main.rs | 12 ++++++------\n src/bin/ngit/sub_commands/init.rs | 11 +++++++----\n src/lib/git/nostr_url.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------\n src/lib/repo_ref.rs | 19 +++++++++++++++----\n 4 files changed, 100 insertions(+), 23 deletions(-)\n\ndiff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs\nindex 8e12d68..84327d7 100644\n--- a/src/bin/git_remote_nostr/main.rs\n+++ b/src/bin/git_remote_nostr/main.rs\n@@ -9,7 +9,6 @@ use std::{\n collections::HashSet,\n env, io,\n path::{Path, PathBuf},\n- str::FromStr,\n };\n \n use anyhow::{bail, Context, Result};\n@@ -28,7 +27,7 @@ mod utils;\n \n #[tokio::main]\n async fn main() -\u003e Result\u003c()\u003e {\n- let Some((decoded_nostr_url, git_repo)) = process_args()? else {\n+ let Some((decoded_nostr_url, git_repo)) = process_args().await? else {\n return Ok(());\n };\n \n@@ -109,7 +108,7 @@ async fn main() -\u003e Result\u003c()\u003e {\n }\n }\n \n-fn process_args() -\u003e Result\u003cOption\u003c(NostrUrlDecoded, Repo)\u003e\u003e {\n+async fn process_args() -\u003e Result\u003cOption\u003c(NostrUrlDecoded, Repo)\u003e\u003e {\n let args = env::args();\n let args = args.skip(1).take(2).collect::\u003cVec\u003c_\u003e\u003e();\n \n@@ -135,13 +134,14 @@ fn process_args() -\u003e Result\u003cOption\u003c(NostrUrlDecoded, Repo)\u003e\u003e {\n return Ok(None);\n };\n \n- let decoded_nostr_url =\n- NostrUrlDecoded::from_str(nostr_remote_url).context(\"invalid nostr url\")?;\n-\n let git_repo = Repo::from_path(\u0026PathBuf::from(\n std::env::var(\"GIT_DIR\").context(\"git should set GIT_DIR when remote helper is called\")?,\n ))?;\n \n+ let decoded_nostr_url = NostrUrlDecoded::parse_and_resolve(nostr_remote_url, \u0026Some(\u0026git_repo))\n+ .await\n+ .context(\"invalid nostr url\")?;\n+\n Ok(Some((decoded_nostr_url, git_repo)))\n }\n \ndiff --git a/src/bin/ngit/sub_commands/init.rs b/src/bin/ngit/sub_commands/init.rs\nindex 6fc1ec4..dbad9b6 100644\n--- a/src/bin/ngit/sub_commands/init.rs\n+++ b/src/bin/ngit/sub_commands/init.rs\n@@ -1,4 +1,4 @@\n-use std::{collections::HashMap, str::FromStr};\n+use std::collections::HashMap;\n \n use anyhow::{Context, Result};\n use console::Style;\n@@ -408,6 +408,7 @@ pub async fn launch(cli_args: \u0026Cli, args: \u0026SubCommandArgs) -\u003e Result\u003c()\u003e {\n trusted_maintainer: user_ref.public_key,\n maintainers: maintainers.clone(),\n events: HashMap::new(),\n+ nip05: None,\n };\n let repo_event = repo_ref.to_event(\u0026signer).await?;\n \n@@ -442,7 +443,7 @@ pub async fn launch(cli_args: \u0026Cli, args: \u0026SubCommandArgs) -\u003e Result\u003c()\u003e {\n .map(std::string::ToString::to_string)\n .collect::\u003cVec\u003cString\u003e\u003e();\n \n- prompt_to_set_nostr_url_as_origin(\u0026repo_ref, \u0026git_repo)?;\n+ prompt_to_set_nostr_url_as_origin(\u0026repo_ref, \u0026git_repo).await?;\n \n // TODO: if no state event exists and there is currently a remote called\n // \"origin\", automtically push rather than waiting for the next commit\n@@ -483,7 +484,7 @@ pub async fn launch(cli_args: \u0026Cli, args: \u0026SubCommandArgs) -\u003e Result\u003c()\u003e {\n Ok(())\n }\n \n-fn prompt_to_set_nostr_url_as_origin(repo_ref: \u0026RepoRef, git_repo: \u0026Repo) -\u003e Result\u003c()\u003e {\n+async fn prompt_to_set_nostr_url_as_origin(repo_ref: \u0026RepoRef, git_repo: \u0026Repo) -\u003e Result\u003c()\u003e {\n println!(\n \"starting from your next commit, when you `git push` to a remote that uses your nostr url, it will store your repository state on nostr and update the state of the git server(s) you just listed.\"\n );\n@@ -493,7 +494,9 @@ fn prompt_to_set_nostr_url_as_origin(repo_ref: \u0026RepoRef, git_repo: \u0026Repo) -\u003e Res\n \n if let Ok(origin_remote) = git_repo.git_repo.find_remote(\"origin\") {\n if let Some(origin_url) = origin_remote.url() {\n- if let Ok(nostr_url) = NostrUrlDecoded::from_str(origin_url) {\n+ if let Ok(nostr_url) =\n+ NostrUrlDecoded::parse_and_resolve(origin_url, \u0026Some(git_repo)).await\n+ {\n if nostr_url.coordinate.identifier == repo_ref.identifier {\n if nostr_url.coordinate.public_key == repo_ref.trusted_maintainer {\n return Ok(());\ndiff --git a/src/lib/git/nostr_url.rs b/src/lib/git/nostr_url.rs\nindex c26bb2e..ac57538 100644\n--- a/src/lib/git/nostr_url.rs\n+++ b/src/lib/git/nostr_url.rs\n@@ -2,9 +2,11 @@ use core::fmt;\n use std::str::FromStr;\n \n use anyhow::{anyhow, bail, Context, Error, Result};\n-use nostr::nips::nip01::Coordinate;\n+use nostr::nips::{nip01::Coordinate, nip05};\n use nostr_sdk::{PublicKey, RelayUrl, ToBech32, Url};\n \n+use super::{get_git_config_item, save_git_config_item, Repo};\n+\n #[derive(Debug, PartialEq, Default, Clone)]\n pub enum ServerProtocol {\n Ssh,\n@@ -59,6 +61,7 @@ pub struct NostrUrlDecoded {\n pub coordinate: Coordinate,\n pub protocol: Option\u003cServerProtocol\u003e,\n pub user: Option\u003cString\u003e,\n+ pub nip05: Option\u003cString\u003e,\n }\n \n impl fmt::Display for NostrUrlDecoded {\n@@ -89,10 +92,8 @@ impl fmt::Display for NostrUrlDecoded {\n \n static INCORRECT_NOSTR_URL_FORMAT_ERROR: \u0026str = \"incorrect nostr git url format. try nostr://naddr123 or nostr://npub123/my-repo or nostr://ssh/npub123/relay.damus.io/my-repo\";\n \n-impl std::str::FromStr for NostrUrlDecoded {\n- type Err = anyhow::Error;\n-\n- fn from_str(url: \u0026str) -\u003e Result\u003cSelf\u003e {\n+impl NostrUrlDecoded {\n+ pub async fn parse_and_resolve(url: \u0026str, git_repo: \u0026Option\u003c\u0026Repo\u003e) -\u003e Result\u003cSelf\u003e {\n let mut protocol = None;\n let mut user = None;\n let mut relays = vec![];\n@@ -154,6 +155,7 @@ impl std::str::FromStr for NostrUrlDecoded {\n }\n // extract naddr npub/\u003coptional-relays\u003e/identifer\n let part = parts.first().context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;\n+ let mut nip05 = None;\n // naddr used\n let coordinate = if let Ok(coordinate) = Coordinate::parse(part) {\n if coordinate.kind.eq(\u0026nostr_sdk::Kind::GitRepoAnnouncement) {\n@@ -161,8 +163,9 @@ impl std::str::FromStr for NostrUrlDecoded {\n } else {\n bail!(\"naddr doesnt point to a git repository announcement\");\n }\n- // npub/\u003coptional-relays\u003e/identifer used\n- } else if let Ok(public_key) = PublicKey::parse(part) {\n+ // \u003cnpub|nip05_address\u003e/\u003coptional-relays\u003e/identifer used\n+ } else {\n+ let npub_or_nip05 = part.to_owned();\n parts.remove(0);\n let identifier = parts\n .pop()\n@@ -179,14 +182,41 @@ impl std::str::FromStr for NostrUrlDecoded {\n RelayUrl::parse(\u0026decoded).context(\"could not parse relays in nostr git url\")?;\n relays.push(url);\n }\n+ let public_key = match PublicKey::parse(npub_or_nip05) {\n+ Ok(public_key) =\u003e public_key,\n+ Err(_) =\u003e {\n+ nip05 = Some(npub_or_nip05.to_string());\n+ if let Ok(public_key) =\n+ resolve_nip05_from_git_config_cache(npub_or_nip05, git_repo)\n+ {\n+ public_key\n+ } else {\n+ // TODO eprint loading message\n+ let res = nip05::profile(npub_or_nip05, None)\n+ .await\n+ .context(INCORRECT_NOSTR_URL_FORMAT_ERROR)?;\n+ // TODO clear loading message\n+ nip05 = Some(npub_or_nip05.to_string());\n+ let _ = save_nip05_to_git_config_cache(\n+ npub_or_nip05,\n+ \u0026res.public_key,\n+ git_repo,\n+ );\n+ if relays.is_empty() {\n+ for r in res.relays {\n+ relays.push(r);\n+ }\n+ }\n+ res.public_key\n+ }\n+ }\n+ };\n Coordinate {\n identifier,\n public_key,\n kind: nostr_sdk::Kind::GitRepoAnnouncement,\n relays,\n }\n- } else {\n- bail!(INCORRECT_NOSTR_URL_FORMAT_ERROR);\n };\n \n Ok(Self {\n@@ -194,10 +224,43 @@ impl std::str::FromStr for NostrUrlDecoded {\n coordinate,\n protocol,\n user,\n+ nip05,\n })\n }\n }\n \n+fn resolve_nip05_from_git_config_cache(nip05: \u0026str, git_repo: \u0026Option\u003c\u0026Repo\u003e) -\u003e Result\u003cPublicKey\u003e {\n+ let stored_value = get_git_config_item(\n+ git_repo,\n+ \u0026format!(\"nostr.nip05.{}\", urlencoding::encode(nip05)),\n+ )?\n+ .context(\"not in cache\")?;\n+ PublicKey::parse(stored_value)\n+ .context(\"stored nip05 resolution value did not parse as public key\")\n+}\n+\n+fn save_nip05_to_git_config_cache(\n+ nip05: \u0026str,\n+ public_key: \u0026PublicKey,\n+ git_repo: \u0026Option\u003c\u0026Repo\u003e,\n+) -\u003e Result\u003c()\u003e {\n+ if save_git_config_item(\n+ git_repo,\n+ \u0026format!(\"nostr.nip05.{}\", urlencoding::encode(nip05)),\n+ \u0026public_key.to_bech32()?,\n+ )\n+ .is_err()\n+ {\n+ save_git_config_item(\n+ \u0026None,\n+ \u0026format!(\"nostr.nip05.{}\", urlencoding::encode(nip05)),\n+ \u0026public_key.to_bech32()?,\n+ )\n+ } else {\n+ Ok(())\n+ }\n+}\n+\n #[derive(Debug, PartialEq, Default)]\n pub struct CloneUrl {\n original_string: String,\ndiff --git a/src/lib/repo_ref.rs b/src/lib/repo_ref.rs\nindex 1b25ccf..bf23d30 100644\n--- a/src/lib/repo_ref.rs\n+++ b/src/lib/repo_ref.rs\n@@ -35,12 +35,14 @@ pub struct RepoRef {\n pub maintainers: Vec\u003cPublicKey\u003e,\n pub trusted_maintainer: PublicKey,\n pub events: HashMap\u003cCoordinate, nostr::Event\u003e,\n+ pub nip05: Option\u003cString\u003e,\n }\n \n impl TryFrom\u003c(nostr::Event, Option\u003cPublicKey\u003e)\u003e for RepoRef {\n type Error = anyhow::Error;\n \n fn try_from((event, trusted_maintainer): (nostr::Event, Option\u003cPublicKey\u003e)) -\u003e Result\u003cSelf\u003e {\n+ // TODO: turn trusted maintainer into NostrUrlDecoded\n if !event.kind.eq(\u0026Kind::GitRepoAnnouncement) {\n bail!(\"incorrect kind\");\n }\n@@ -56,6 +58,7 @@ impl TryFrom\u003c(nostr::Event, Option\u003cPublicKey\u003e)\u003e for RepoRef {\n maintainers: Vec::new(),\n trusted_maintainer: trusted_maintainer.unwrap_or(event.pubkey),\n events: HashMap::new(),\n+ nip05: None,\n };\n \n for tag in event.tags.iter() {\n@@ -239,6 +242,7 @@ impl RepoRef {\n coordinate: self.coordinate_with_hint(),\n protocol: None,\n user: None,\n+ nip05: self.nip05.clone(),\n }\n )\n }\n@@ -259,7 +263,7 @@ pub async fn get_repo_coordinates_when_remote_unknown(\n pub async fn try_and_get_repo_coordinates_when_remote_unknown(\n git_repo: \u0026Repo,\n ) -\u003e Result\u003cCoordinate\u003e {\n- let remote_coordinates = get_repo_coordinates_from_nostr_remotes(git_repo)?;\n+ let remote_coordinates = get_repo_coordinates_from_nostr_remotes(git_repo).await?;\n if remote_coordinates.is_empty() {\n if let Ok(c) = get_repo_coordinates_from_git_config(git_repo) {\n Ok(c)\n@@ -327,11 +331,15 @@ fn get_repo_coordinates_from_git_config(git_repo: \u0026Repo) -\u003e Result\u003cCoordinate\u003e {\n .context(\"git config item \\\"nostr.repo\\\" is not an naddr\")\n }\n \n-fn get_repo_coordinates_from_nostr_remotes(git_repo: \u0026Repo) -\u003e Result\u003cHashMap\u003cString, Coordinate\u003e\u003e {\n+async fn get_repo_coordinates_from_nostr_remotes(\n+ git_repo: \u0026Repo,\n+) -\u003e Result\u003cHashMap\u003cString, Coordinate\u003e\u003e {\n let mut repo_coordinates = HashMap::new();\n for remote_name in git_repo.git_repo.remotes()?.iter().flatten() {\n if let Some(remote_url) = git_repo.git_repo.find_remote(remote_name)?.url() {\n- if let Ok(nostr_url_decoded) = NostrUrlDecoded::from_str(remote_url) {\n+ if let Ok(nostr_url_decoded) =\n+ NostrUrlDecoded::parse_and_resolve(remote_url, \u0026Some(git_repo)).await\n+ {\n repo_coordinates.insert(remote_name.to_string(), nostr_url_decoded.coordinate);\n }\n }\n@@ -383,7 +391,9 @@ async fn get_repo_coordinate_from_user_prompt(\n .input(PromptInputParms::default().with_prompt(\"nostr repository\"))?;\n let coordinate = if let Ok(c) = Coordinate::parse(\u0026input) {\n c\n- } else if let Ok(nostr_url) = NostrUrlDecoded::from_str(\u0026input) {\n+ } else if let Ok(nostr_url) =\n+ NostrUrlDecoded::parse_and_resolve(\u0026input, \u0026Some(git_repo)).await\n+ {\n nostr_url.coordinate\n } else {\n eprintln!(\"not a valid naddr or git nostr remote URL starting nostr://\");\n@@ -540,6 +550,7 @@ mod tests {\n trusted_maintainer: TEST_KEY_1_KEYS.public_key(),\n maintainers: vec![TEST_KEY_1_KEYS.public_key(), TEST_KEY_2_KEYS.public_key()],\n events: HashMap::new(),\n+ nip05: None,\n }\n .to_event(\u0026TEST_KEY_1_SIGNER)\n .await\n--\nlibgit2 1.8.1\n\n", "sig": "68bd776e96f633ed769a9b62c7f7c687dd470302ef12453bd81b005821e8954f00dd667fb50a1b0472d95c516aecae4cd878f18647be697a66658db293db7016" }