refactor: Support more than one naddr instead of one

This commit supports multiple naddrs for sending issues to and searching
for repositories by extracting the embedded relays, and appending to the
address file instead of overwriting its previous contents. A header has
also been added to the `nostr-address` file to clarify its purpose.

This enhancement accommodates maintainers who distribute issue tracking
across multiple repositories, preventing confusion if the primary
maintainer steps down. Now, issues and patches will reference all
relevant repositories, not just one.

Note that repositories won't automatically include all maintainers'
addresses unless explicitly specified. Maintainers may manage a
repository under a project-specific public key rather than individual
keys. Addresses are only added to issues and patches when explicitly
provided via the command or written to the address file. This prevents
unintended inclusions and ensures clarity.

Suggested-by: DanConwayDev <DanConwayDev@protonmail.com>
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb
2025-05-27 06:42:05 +00:00
parent 87da5035da
commit 37cf601c01
8 changed files with 272 additions and 113 deletions

View File

@@ -16,12 +16,20 @@
use clap::{ArgGroup, Args}; use clap::{ArgGroup, Args};
use nostr::{event::EventBuilder, nips::nip19::Nip19Coordinate}; use futures::future;
use nostr::{
event::{EventBuilder, Tag},
nips::nip19::Nip19Coordinate,
};
use crate::{ use crate::{
cli::{CliOptions, CommandRunner, parsers}, cli::{CliOptions, CommandRunner, parsers},
error::N34Result, error::N34Result,
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils}, nostr_utils::{
NostrClient,
traits::{NaddrsUtils, NewGitRepositoryAnnouncement, ReposUtils},
utils,
},
}; };
@@ -44,7 +52,7 @@ pub struct NewArgs {
/// ///
/// If not provided, `n34` will look for the `nostr-address` file. /// If not provided, `n34` will look for the `nostr-address` file.
#[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)] #[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)]
naddr: Option<Nip19Coordinate>, naddrs: Option<Vec<Nip19Coordinate>>,
/// Markdown content for the issue. Cannot be used together with the /// Markdown content for the issue. Cannot be used together with the
/// `--editor` flag. /// `--editor` flag.
#[arg(short, long)] #[arg(short, long)]
@@ -89,24 +97,54 @@ impl CommandRunner for NewArgs {
async fn run(self, options: CliOptions) -> N34Result<()> { async fn run(self, options: CliOptions) -> N34Result<()> {
let client = NostrClient::init(&options).await; let client = NostrClient::init(&options).await;
let user_pubk = options.pubkey().await?; let user_pubk = options.pubkey().await?;
let naddr = utils::naddr_or_file(self.naddr.clone(), &utils::nostr_address_path()?)?; let naddrs = utils::naddrs_or_file(self.naddrs.clone(), &utils::nostr_address_path()?)?;
let mut naddrs_iter = naddrs.clone().into_iter();
client.add_relays(&naddr.relays).await; client.add_relays(&naddrs.extract_relays()).await;
let relays_list = client.user_relays_list(user_pubk).await?; let relays_list = client.user_relays_list(user_pubk).await?;
let mut write_relays = let mut write_relays =
utils::add_write_relays(options.relays.clone(), relays_list.as_ref()); utils::add_write_relays(options.relays.clone(), relays_list.as_ref());
write_relays.extend(client.fetch_repo(&naddr.coordinate).await?.relays); write_relays.extend(
client
.fetch_repos(&naddrs.into_coordinates())
.await?
.extract_relays(),
);
let (subject, content) = self.issue_content()?; let (subject, content) = self.issue_content()?;
let content_details = client.parse_content(&content).await; let content_details = client.parse_content(&content).await;
write_relays.extend(content_details.write_relays.clone()); write_relays.extend(content_details.write_relays.clone());
let event = // Include read relays for each repository owner (if found)
EventBuilder::new_git_issue(naddr.coordinate.clone(), content, subject, self.label)? write_relays.extend(
.tags(content_details.into_tags()) future::join_all(
.pow(options.pow) naddrs_iter
.build(user_pubk); .clone()
.map(|c| client.read_relays_from_user(Vec::new(), c.public_key)),
)
.await
.into_iter()
.flatten(),
);
let event = EventBuilder::new_git_issue(
naddrs_iter
.next()
.expect("There is at least one address")
.coordinate
.clone(),
content,
subject,
self.label,
)?
.pow(options.pow)
.tags(content_details.into_tags())
// p-tag the reset of the reposotoies owners
.tags(naddrs_iter.clone().map(|n| Tag::public_key(n.public_key)))
// a-tag the reset of the reposotoies
.tags(naddrs_iter.map(|n| Tag::coordinate(n.coordinate, n.relays.first().cloned())))
.build(user_pubk);
let event_id = event.id.expect("There is an id"); let event_id = event.id.expect("There is an id");
tracing::trace!(relays = ?write_relays, "Write relays list"); tracing::trace!(relays = ?write_relays, "Write relays list");

View File

@@ -17,6 +17,7 @@
use std::{fs, str::FromStr}; use std::{fs, str::FromStr};
use clap::{ArgGroup, Args}; use clap::{ArgGroup, Args};
use futures::future;
use nostr::{ use nostr::{
event::{Event, EventBuilder, EventId, Kind, Tag}, event::{Event, EventBuilder, EventId, Kind, Tag},
filter::Filter, filter::Filter,
@@ -31,7 +32,11 @@ use super::{CliOptions, CommandRunner};
use crate::{ use crate::{
cli::parsers, cli::parsers,
error::{N34Error, N34Result}, error::{N34Error, N34Result},
nostr_utils::{NostrClient, utils}, nostr_utils::{
NostrClient,
traits::{NaddrsUtils, ReposUtils},
utils,
},
}; };
/// Length of a Nostr npub (public key) in characters. /// Length of a Nostr npub (public key) in characters.
@@ -101,7 +106,7 @@ pub struct ReplyArgs {
/// If not provided, `n34` will look for the `nostr-address` file and if not /// If not provided, `n34` will look for the `nostr-address` file and if not
/// found, will get it from the root event if found. /// found, will get it from the root event if found.
#[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)] #[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)]
naddr: Option<Nip19Coordinate>, naddrs: Option<Vec<Nip19Coordinate>>,
/// The comment (cannot be used with --editor) /// The comment (cannot be used with --editor)
#[arg(short, long)] #[arg(short, long)]
comment: Option<String>, comment: Option<String>,
@@ -116,13 +121,13 @@ impl CommandRunner for ReplyArgs {
let client = NostrClient::init(&options).await; let client = NostrClient::init(&options).await;
let user_pubk = options.pubkey().await?; let user_pubk = options.pubkey().await?;
let repo_naddr = if let Some(naddr) = self.naddr { let repo_naddrs = if let Some(naddrs) = self.naddrs {
client.add_relays(&naddr.relays).await; client.add_relays(&naddrs.extract_relays()).await;
Some(naddr.coordinate) Some(naddrs)
} else if fs::exists(&nostr_address_path).is_ok() { } else if fs::exists(&nostr_address_path).is_ok() {
let naddr = utils::naddr_or_file(None, &nostr_address_path)?; let naddrs = utils::naddrs_or_file(None, &nostr_address_path)?;
client.add_relays(&naddr.relays).await; client.add_relays(&naddrs.extract_relays()).await;
Some(naddr.coordinate) Some(naddrs)
} else { } else {
None None
}; };
@@ -139,16 +144,28 @@ impl CommandRunner for ReplyArgs {
.ok_or(N34Error::EventNotFound)?; .ok_or(N34Error::EventNotFound)?;
let root = client.find_root(reply_to.clone()).await?; let root = client.find_root(reply_to.clone()).await?;
let repo_naddr = if let Some(naddr) = repo_naddr { let repos_coordinate = if let Some(naddrs) = repo_naddrs {
naddr naddrs.into_coordinates()
} else if let Some(ref root_event) = root { } else if let Some(ref root_event) = root {
naddr_from_root(root_event)? coordinates_from_root(root_event)?
} else { } else {
return Err(N34Error::NotFoundRepo); return Err(N34Error::NotFoundRepo);
}; };
let repo = client.fetch_repo(&repo_naddr).await?; let repos = client.fetch_repos(&repos_coordinate).await?;
write_relays.extend(repo.relays.clone()); // Merge repository announcement relays into write relays
write_relays.extend(repos.extract_relays());
// Include read relays for each repository owner (if found)
write_relays.extend(
future::join_all(
repos_coordinate
.iter()
.map(|c| client.read_relays_from_user(Vec::new(), c.public_key)),
)
.await
.into_iter()
.flatten(),
);
write_relays = client write_relays = client
.read_relays_from_user(write_relays, reply_to.pubkey) .read_relays_from_user(write_relays, reply_to.pubkey)
@@ -173,11 +190,11 @@ impl CommandRunner for ReplyArgs {
content, content,
&reply_to, &reply_to,
root.as_ref(), root.as_ref(),
repo.relays.first().cloned(), repos.first().and_then(|r| r.relays.first()).cloned(),
) )
.tag(Tag::public_key(repo_naddr.public_key))
.tags(content_details.into_tags())
.pow(options.pow) .pow(options.pow)
.tags(content_details.into_tags())
.tag(Tag::public_key(reply_to.pubkey))
.build(user_pubk); .build(user_pubk);
let event_id = event.id.expect("There is an id"); let event_id = event.id.expect("There is an id");
@@ -245,15 +262,19 @@ async fn quote_reply_to_content(client: &NostrClient, quoted_event: &Event) -> S
/// Gets the repository coordinate from a root Nostr event's tags. /// Gets the repository coordinate from a root Nostr event's tags.
/// The event must contain a coordinate tag with GitRepoAnnouncement kind. /// The event must contain a coordinate tag with GitRepoAnnouncement kind.
fn naddr_from_root(root: &Event) -> N34Result<Coordinate> { fn coordinates_from_root(root: &Event) -> N34Result<Vec<Coordinate>> {
Ok(root let coordinates: Vec<Coordinate> = root
.tags .tags
.coordinates() .coordinates()
.find(|c| c.kind == Kind::GitRepoAnnouncement) .filter(|c| c.kind == Kind::GitRepoAnnouncement)
.ok_or_else(|| { .cloned()
N34Error::InvalidEvent( .collect();
"The Git issue/patch does not specify a target repository".to_owned(),
) if coordinates.is_empty() {
})? return Err(N34Error::InvalidEvent(
.clone()) "The Git issue/patch does not specify a target repository".to_owned(),
));
}
Ok(coordinates)
} }

View File

@@ -14,9 +14,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
use std::fs; use std::{fs, io::Write};
use clap::Args; use clap::Args;
use futures::future;
use nostr::{event::EventBuilder, key::PublicKey, types::Url}; use nostr::{event::EventBuilder, key::PublicKey, types::Url};
use crate::{ use crate::{
@@ -25,6 +26,22 @@ use crate::{
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils}, nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
}; };
/// Header written to new `nostr-address` files. Contains two trailing newline
/// for formatting.
const NOSTR_ADDRESS_FILE_HEADER: &str = r##"# This file contains NIP-19 `naddr` entities for repositories that accept this
# project's issues and patches.
#
# The file acts as a **read-only reference** for retrieving repository relays
# when embedded in an `naddr` and mentions those repositories when opening
# patches or issues. Modifications here will not affect in the relays, as the
# file is **explicitly untracked**. Its goal is to simplify contributions by
# removing the need for manual address entry.
#
# Each entry must start with "naddr". Embedded relays are **strongly recommended**
# to assist client-side discovery.
#
# Empty lines are ignored. Lines starting with "#" are treated as comments.
"##;
/// Arguments for the `repo announce` command /// Arguments for the `repo announce` command
#[derive(Args, Debug)] #[derive(Args, Debug)]
@@ -66,12 +83,25 @@ impl CommandRunner for AnnounceArgs {
let client = NostrClient::init(&options).await; let client = NostrClient::init(&options).await;
let user_pubk = options.pubkey().await?; let user_pubk = options.pubkey().await?;
let relays_list = client.user_relays_list(user_pubk).await?; let relays_list = client.user_relays_list(user_pubk).await?;
let write_relays = utils::add_write_relays(options.relays.clone(), relays_list.as_ref()); let mut write_relays =
utils::add_write_relays(options.relays.clone(), relays_list.as_ref());
if !self.maintainers.contains(&user_pubk) { if !self.maintainers.contains(&user_pubk) {
self.maintainers.insert(0, user_pubk); self.maintainers.insert(0, user_pubk);
} }
// Include read relays for each maintainer (if found)
write_relays.extend(
future::join_all(
self.maintainers
.iter()
.map(|pkey| client.read_relays_from_user(Vec::new(), *pkey)),
)
.await
.into_iter()
.flatten(),
);
let event = EventBuilder::new_git_repo( let event = EventBuilder::new_git_repo(
self.repo_id, self.repo_id,
self.name.map(utils::str_trim), self.name.map(utils::str_trim),
@@ -90,9 +120,21 @@ impl CommandRunner for AnnounceArgs {
let naddr = utils::repo_naddr(user_pubk, &options.relays)?; let naddr = utils::repo_naddr(user_pubk, &options.relays)?;
if self.address_file { if self.address_file {
let path = std::env::current_dir()?.join(NOSTR_ADDRESS_FILE); let address_path = std::env::current_dir()?.join(NOSTR_ADDRESS_FILE);
tracing::info!("Create the `nostr-address` file at `{}`", path.display()); if !address_path.exists() {
fs::write(path, &naddr)?; tracing::info!(
"Creating new address file: '{NOSTR_ADDRESS_FILE}' at path '{}' with default \
header",
address_path.display()
);
fs::write(&address_path, NOSTR_ADDRESS_FILE_HEADER)?;
}
let mut file = fs::OpenOptions::new().append(true).open(&address_path)?;
tracing::info!("Appending naddr '{naddr}' to address file: '{NOSTR_ADDRESS_FILE}'",);
file.write_all(naddr.as_bytes())?;
tracing::info!("Successfully wrote naddr to address file");
} }
client client

View File

@@ -22,7 +22,7 @@ use nostr::nips::nip19::Nip19Coordinate;
use crate::{ use crate::{
cli::{CliOptions, CommandRunner, parsers}, cli::{CliOptions, CommandRunner, parsers},
error::N34Result, error::N34Result,
nostr_utils::{NostrClient, utils}, nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
}; };
/// Arguments for the `repo view` command /// Arguments for the `repo view` command
@@ -33,46 +33,51 @@ pub struct ViewArgs {
/// ///
/// If not provided, `n34` will look for the `nostr-address` file. /// If not provided, `n34` will look for the `nostr-address` file.
#[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)] #[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)]
naddr: Option<Nip19Coordinate>, naddrs: Option<Vec<Nip19Coordinate>>,
} }
impl CommandRunner for ViewArgs { impl CommandRunner for ViewArgs {
async fn run(self, options: CliOptions) -> N34Result<()> { async fn run(self, options: CliOptions) -> N34Result<()> {
// FIXME: The signer is not required here // FIXME: The signer is not required here
let naddr = utils::naddr_or_file(self.naddr, &utils::nostr_address_path()?)?; let naddrs = utils::naddrs_or_file(self.naddrs, &utils::nostr_address_path()?)?;
let client = NostrClient::init(&options).await; let client = NostrClient::init(&options).await;
client.add_relays(&naddr.relays).await; client.add_relays(&naddrs.extract_relays()).await;
let repo = client.fetch_repo(&naddr.coordinate).await?; let repos = client.fetch_repos(&naddrs.into_coordinates()).await?;
let mut msg = format!("ID: {}", repo.id); let mut repos_details: Vec<String> = Vec::new();
if let Some(name) = repo.name { for repo in repos {
msg.push_str(&format!("\nName: {name}")); let mut repo_details = format!("ID: {}", repo.id);
}
if let Some(desc) = repo.description { if let Some(name) = repo.name {
msg.push_str(&format!("\nDescription: {desc}")); repo_details.push_str(&format!("\nName: {name}"));
} }
if !repo.web.is_empty() { if let Some(desc) = repo.description {
msg.push_str(&format!("\nWebpages:\n{}", format_list(repo.web))); repo_details.push_str(&format!("\nDescription: {desc}"));
} }
if !repo.clone.is_empty() { if !repo.web.is_empty() {
msg.push_str(&format!("\nClone urls:\n{}", format_list(repo.clone))); repo_details.push_str(&format!("\nWebpages:\n{}", format_list(repo.web)));
} }
if !repo.relays.is_empty() { if !repo.clone.is_empty() {
msg.push_str(&format!("\nRelays:\n{}", format_list(repo.relays))); repo_details.push_str(&format!("\nClone urls:\n{}", format_list(repo.clone)));
} }
if let Some(euc) = repo.euc { if !repo.relays.is_empty() {
msg.push_str(&format!("\nEarliest unique commit: {euc}")); repo_details.push_str(&format!("\nRelays:\n{}", format_list(repo.relays)));
} }
if !repo.maintainers.is_empty() { if let Some(euc) = repo.euc {
msg.push_str(&format!( repo_details.push_str(&format!("\nEarliest unique commit: {euc}"));
"\nMaintainers:\n{}", }
format_list(repo.maintainers) if !repo.maintainers.is_empty() {
)); repo_details.push_str(&format!(
"\nMaintainers:\n{}",
format_list(repo.maintainers)
));
}
repos_details.push(repo_details);
} }
println!("{msg}"); println!("{}", repos_details.join("\n----------\n"));
Ok(()) Ok(())
} }
} }

View File

@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>. // along with this program. If not, see <https://gnu.org/licenses/gpl-3.0.html>.
use std::{fs, path::Path};
use nostr::{ use nostr::{
Kind, Kind,
nips::{ nips::{
@@ -24,6 +26,8 @@ use nostr::{
}; };
use tokio::runtime::Handle; use tokio::runtime::Handle;
use crate::error::{N34Error, N34Result};
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<Nip19Coordinate, String> { fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<Nip19Coordinate, String> {
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5)); let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
@@ -42,6 +46,34 @@ fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<Nip19Coordinate, String>
.expect("The relays is `RelayUrl`")) .expect("The relays is `RelayUrl`"))
} }
fn parse_repo_naddr(repo_naddr: &str) -> Result<Nip19Coordinate, String> {
let naddr = Nip19Coordinate::from_bech32(repo_naddr).map_err(|err| err.to_string())?;
if naddr.relays.is_empty() {
tracing::warn!("The repository naddr does not contain any relay hints");
}
(naddr.kind == Kind::GitRepoAnnouncement)
.then_some(naddr)
.ok_or_else(|| "Invalid naddr: must be of kind 30617 (GitRepoAnnouncement)".to_owned())
}
/// Parses a nostr-address file into a NIP-19 coordinates. Expects the file to
/// contain a repository announcements.
pub fn parse_nostr_address_file(file_path: &Path) -> N34Result<Vec<Nip19Coordinate>> {
let addresses = fs::read_to_string(file_path)
.map_err(N34Error::CanNotReadNostrAddressFile)?
.split("\n")
.filter_map(|line| {
(!line.starts_with("#") && !line.trim().is_empty())
.then_some(parse_repo_naddr(line).map_err(N34Error::InvalidNostrAddressFileContent))
})
.collect::<N34Result<Vec<Nip19Coordinate>>>()?;
if addresses.is_empty() {
return Err(N34Error::EmptyNostrAddressFile);
}
Ok(addresses)
}
/// Parses a Git repository address which can be either: /// Parses a Git repository address which can be either:
/// - A bech32-encoded naddr (e.g. "naddr1...") for Git repository announcements /// - A bech32-encoded naddr (e.g. "naddr1...") for Git repository announcements
/// (kind 30617) /// (kind 30617)
@@ -50,20 +82,19 @@ fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<Nip19Coordinate, String>
/// ///
/// Returns an error for invalid formats, failed bech32 decoding, wrong event /// Returns an error for invalid formats, failed bech32 decoding, wrong event
/// kind. /// kind.
pub fn repo_naddr(repo_address: &str) -> Result<Nip19Coordinate, String> { pub fn repo_naddr(repo: &str) -> Result<Nip19Coordinate, String> {
if repo_address.contains("/") { let repo = repo.trim();
let (nip5, repo_id) = repo_address.split_once("/").expect("There is a `/`");
return parse_nip5_repo(nip5, repo_id);
}
let naddr = Nip19Coordinate::from_bech32(repo_address).map_err(|err| err.to_string())?; if repo.contains("/") {
if naddr.kind != Kind::GitRepoAnnouncement { let (nip5, repo_id) = repo.split_once("/").expect("There is a `/`");
return Err("The naddr is not repo announcement address".to_owned()); parse_nip5_repo(nip5, repo_id)
} else if repo.starts_with("naddr1") {
parse_repo_naddr(repo)
} else {
Err(
"Invalid repository address format. It can be A NIP-05 identifier with repository ID \
in format `<nip5>/<repo_id>` or A valid naddr1 string (NIP-19)"
.to_owned(),
)
} }
if naddr.relays.is_empty() {
tracing::warn!("The repository naddr does not contain any relay hints");
}
Ok(naddr)
} }

View File

@@ -195,20 +195,27 @@ impl NostrClient {
.first_owned()) .first_owned())
} }
/// Try to fetch a repository and returns it /// Try to fetch the reposotoies and returns them
pub async fn fetch_repo( pub async fn fetch_repos(
&self, &self,
repo_naddr: &Coordinate, repo_naddrs: &[Coordinate],
) -> N34Result<GitRepositoryAnnouncement> { ) -> N34Result<Vec<GitRepositoryAnnouncement>> {
let filter = Filter::new() future::join_all(repo_naddrs.iter().map(|c| {
.author(repo_naddr.public_key) async {
.kind(Kind::GitRepoAnnouncement) self.fetch_event(
.identifier(&repo_naddr.identifier); Filter::new()
.author(c.public_key)
self.fetch_event(filter) .identifier(&c.identifier)
.await? .kind(Kind::GitRepoAnnouncement),
.map(|e| utils::event_into_repo(e, &repo_naddr.identifier)) )
.ok_or(N34Error::NotFoundRepo) .await?
.map(|e| utils::event_into_repo(e, &c.identifier))
.ok_or(N34Error::NotFoundRepo)
}
}))
.await
.into_iter()
.collect()
} }
/// Finds the root issue or patch for a given event. If the event is already /// Finds the root issue or patch for a given event. If the event is already

View File

@@ -20,6 +20,7 @@ use nostr::{
key::PublicKey, key::PublicKey,
nips::{ nips::{
nip01::Coordinate, nip01::Coordinate,
nip19::Nip19Coordinate,
nip21::Nip21, nip21::Nip21,
nip34::{GitIssue, GitRepositoryAnnouncement}, nip34::{GitIssue, GitRepositoryAnnouncement},
}, },
@@ -164,3 +165,26 @@ impl Token<'_> {
} }
} }
} }
/// Utility functions for working with lists of NIP-19 coordinates
#[easy_ext::ext(NaddrsUtils)]
impl Vec<Nip19Coordinate> {
/// Converts these coordinate addresses to basic coordinates
pub fn into_coordinates(self) -> Vec<Coordinate> {
self.into_iter().map(|n| n.coordinate).collect()
}
/// Extracts all relay URLs from these coordinates
pub fn extract_relays(&self) -> Vec<RelayUrl> {
self.iter().flat_map(|n| n.relays.clone()).collect()
}
}
/// Utility functions for working with lists of repository announcement
#[easy_ext::ext(ReposUtils)]
impl Vec<GitRepositoryAnnouncement> {
/// Extracts all relay URLs from these reposotoies
pub fn extract_relays(&self) -> Vec<RelayUrl> {
self.iter().flat_map(|n| n.relays.clone()).collect()
}
}

View File

@@ -220,21 +220,12 @@ pub fn nostr_address_path() -> std::io::Result<PathBuf> {
/// If the given coordinate is Some, return it. Otherwise, try to read and parse /// If the given coordinate is Some, return it. Otherwise, try to read and parse
/// the first non-comment line from the `nostr-address` file. /// the first non-comment line from the `nostr-address` file.
pub fn naddr_or_file( pub fn naddrs_or_file(
naddr: Option<Nip19Coordinate>, naddrs: Option<Vec<Nip19Coordinate>>,
address_file_path: &Path, address_file_path: &Path,
) -> N34Result<Nip19Coordinate> { ) -> N34Result<Vec<Nip19Coordinate>> {
if let Some(naddr) = naddr { if let Some(naddrs) = naddrs {
return Ok(naddr); return Ok(naddrs);
} }
parsers::parse_nostr_address_file(address_file_path)
parsers::repo_naddr(
fs::read_to_string(address_file_path)
.map_err(N34Error::CanNotReadNostrAddressFile)?
.split("\n")
.find(|line| !line.starts_with("#") && !line.trim().is_empty())
.ok_or(N34Error::EmptyNostrAddressFile)?
.trim(),
)
.map_err(N34Error::InvalidNostrAddressFileContent)
} }