feat: Support relays and naddrs sets
This commit introduces support for 'sets', a feature that simplifies referencing multiple relays or naddrs with a single word. Sets are stored in the config directory under `n34` as `config.toml` in `TOML` format. The config path follows the `$XDG_CONFIG_HOME` standard, defaulting to `$HOME/.config` if unset. The `n34 sets` command allows managing sets, adding, removing, updating, or displaying them. When using the main `--relays` flag, a set name can be provided to automatically expand its relays (an error occurs if the set contains no relays). Similarly, any command accepting naddrs can reference a set to extract its naddrs. Sets can also be merged by updating a set while specifying another set in `--repo` merges their naddrs, while `--set-relays` merges their relays. Removal follows the same pattern. Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
14
CHANGELOG.md
14
CHANGELOG.md
@@ -15,10 +15,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Read the `nostr-address` file in `issue new` command - by Awiteb
|
- Read the `nostr-address` file in `issue new` command - by Awiteb
|
||||||
- Read the `nostr-address` file in `reply` command - by Awiteb
|
- Read the `nostr-address` file in `reply` command - by Awiteb
|
||||||
- A `--quote-to` flag to quote the replied to content in the editor - by Awiteb
|
- A `--quote-to` flag to quote the replied to content in the editor - by Awiteb
|
||||||
|
- Enter repository as nip5 - by Awiteb
|
||||||
|
- Make the relays list optional - by Awiteb
|
||||||
|
- Events and naddrs can starts with `nostr:` - by Awiteb
|
||||||
|
- Support relays and naddrs sets - by Awiteb
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- Add `chrono@0.4.41` to the dependencies - by Awiteb
|
- Add `chrono@0.4.41` to the dependencies - by Awiteb
|
||||||
|
- Enable `nip05` feature of `nostr` crate - by Awiteb
|
||||||
|
- Add `serde@1.0.219`, `dirs@6.0.0` and `toml@0.8.22` - by Awiteb
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Create a valid naddr string - by Awiteb
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Support more than one naddr instead of one - by Awiteb
|
||||||
|
|
||||||
## [0.1.0] - 2025-05-21
|
## [0.1.0] - 2025-05-21
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,15 @@
|
|||||||
|
|
||||||
use clap::{ArgGroup, Args};
|
use clap::{ArgGroup, Args};
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use nostr::{
|
use nostr::event::{EventBuilder, Tag};
|
||||||
event::{EventBuilder, Tag},
|
|
||||||
nips::nip19::Nip19Coordinate,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, CommandRunner, parsers},
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
CommandRunner,
|
||||||
|
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{
|
nostr_utils::{
|
||||||
NostrClient,
|
NostrClient,
|
||||||
@@ -47,12 +49,12 @@ use crate::{
|
|||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub struct NewArgs {
|
pub struct NewArgs {
|
||||||
/// Repository address in `naddr` format or `<nip5>/repo_id`. e.g.
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
/// `4rs.nl/n34` and `_@4rs.nl/n34`
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
///
|
///
|
||||||
/// If not provided, `n34` will look for the `nostr-address` file.
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
#[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)]
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
naddrs: Option<Vec<Nip19Coordinate>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
/// 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)]
|
||||||
@@ -95,9 +97,14 @@ impl NewArgs {
|
|||||||
|
|
||||||
impl CommandRunner for NewArgs {
|
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 config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = utils::naddrs_or_file(
|
||||||
|
self.naddrs.flat_naddrs(&config.sets)?,
|
||||||
|
&utils::nostr_address_path()?,
|
||||||
|
)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = options.pubkey().await?;
|
let user_pubk = options.pubkey().await?;
|
||||||
let naddrs = utils::naddrs_or_file(self.naddrs.clone(), &utils::nostr_address_path()?)?;
|
|
||||||
let mut naddrs_iter = naddrs.clone().into_iter();
|
let mut naddrs_iter = naddrs.clone().into_iter();
|
||||||
|
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
@@ -128,7 +135,7 @@ impl CommandRunner for NewArgs {
|
|||||||
let event_id = event.id.expect("There is an id");
|
let event_id = event.id.expect("There is an id");
|
||||||
|
|
||||||
let write_relays = [
|
let write_relays = [
|
||||||
options.relays,
|
relays,
|
||||||
utils::add_write_relays(relays_list.as_ref()),
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
client
|
client
|
||||||
.fetch_repos(&naddrs.into_coordinates())
|
.fetch_repos(&naddrs.into_coordinates())
|
||||||
|
|||||||
@@ -14,27 +14,34 @@
|
|||||||
// 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>.
|
||||||
|
|
||||||
|
|
||||||
/// `issue` subcommands
|
/// `issue` subcommands
|
||||||
mod issue;
|
mod issue;
|
||||||
/// 'reply` command
|
/// 'reply` command
|
||||||
mod reply;
|
mod reply;
|
||||||
/// `repo` subcommands
|
/// `repo` subcommands
|
||||||
mod repo;
|
mod repo;
|
||||||
|
/// `sets` subcommands
|
||||||
|
mod sets;
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{ArgGroup, Args, Parser};
|
use clap::{ArgGroup, Args, Parser};
|
||||||
use nostr::key::{Keys, PublicKey, SecretKey};
|
use nostr::key::{Keys, PublicKey, SecretKey};
|
||||||
use nostr::types::RelayUrl;
|
|
||||||
|
|
||||||
use self::issue::IssueSubcommands;
|
use self::issue::IssueSubcommands;
|
||||||
use self::reply::ReplyArgs;
|
use self::reply::ReplyArgs;
|
||||||
use self::repo::RepoSubcommands;
|
use self::repo::RepoSubcommands;
|
||||||
|
use self::sets::SetsSubcommands;
|
||||||
use super::traits::CommandRunner;
|
use super::traits::CommandRunner;
|
||||||
|
use super::types::RelayOrSet;
|
||||||
use crate::error::{N34Error, N34Result};
|
use crate::error::{N34Error, N34Result};
|
||||||
|
|
||||||
|
/// Default path used when no path is provided via command line arguments.
|
||||||
|
///
|
||||||
|
/// This is a workaround since Clap doesn't support lazy evaluation of defaults.
|
||||||
|
pub const DEFAULT_FALLBACK_PATH: &str = "I_DO_NOT_KNOW_WHY_CLAP_DO_NOT_SUPPORT_LAZY_DEFAULT";
|
||||||
|
|
||||||
// TODO: Make the `signer` group optional
|
// TODO: Make the `signer` group optional
|
||||||
/// The command-line interface options
|
/// The command-line interface options
|
||||||
#[derive(Args, Clone)]
|
#[derive(Args, Clone)]
|
||||||
@@ -52,15 +59,25 @@ pub struct CliOptions {
|
|||||||
/// Fallbacks relay to write and read from it. Multiple relays can be
|
/// Fallbacks relay to write and read from it. Multiple relays can be
|
||||||
/// passed.
|
/// passed.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub relays: Vec<RelayUrl>,
|
pub relays: Vec<RelayOrSet>,
|
||||||
/// Proof of Work difficulty when creatring events
|
/// Proof of Work difficulty when creatring events
|
||||||
#[arg(long, default_value_t = 0)]
|
#[arg(long, default_value_t = 0)]
|
||||||
pub pow: u8,
|
pub pow: u8,
|
||||||
|
/// Config path [default: `$XDG_CONFIG_HOME` or `$HOME/.config`]
|
||||||
|
#[arg(long, value_name = "PATH", default_value = DEFAULT_FALLBACK_PATH,
|
||||||
|
hide_default_value = true
|
||||||
|
)]
|
||||||
|
pub config_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// N34 commands
|
/// N34 commands
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
|
/// Manage reposotoies and relays sets
|
||||||
|
Sets {
|
||||||
|
#[command(subcommand)]
|
||||||
|
subcommands: SetsSubcommands,
|
||||||
|
},
|
||||||
/// Manage repositories
|
/// Manage repositories
|
||||||
Repo {
|
Repo {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
@@ -89,6 +106,7 @@ impl CommandRunner for Commands {
|
|||||||
Self::Repo { subcommands } => subcommands.run(options).await,
|
Self::Repo { subcommands } => subcommands.run(options).await,
|
||||||
Self::Issue { subcommands } => subcommands.run(options).await,
|
Self::Issue { subcommands } => subcommands.run(options).await,
|
||||||
Commands::Reply(args) => args.run(options).await,
|
Commands::Reply(args) => args.run(options).await,
|
||||||
|
Commands::Sets { subcommands } => subcommands.run(options).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,17 @@ use nostr::{
|
|||||||
filter::Filter,
|
filter::Filter,
|
||||||
nips::{
|
nips::{
|
||||||
nip01::{Coordinate, Metadata},
|
nip01::{Coordinate, Metadata},
|
||||||
nip19::{self, FromBech32, Nip19Coordinate, ToBech32},
|
nip19::{self, FromBech32, ToBech32},
|
||||||
},
|
},
|
||||||
types::RelayUrl,
|
types::RelayUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{CliOptions, CommandRunner};
|
use super::{CliOptions, CommandRunner};
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::parsers,
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
error::{N34Error, N34Result},
|
error::{N34Error, N34Result},
|
||||||
nostr_utils::{
|
nostr_utils::{
|
||||||
NostrClient,
|
NostrClient,
|
||||||
@@ -101,13 +104,12 @@ pub struct ReplyArgs {
|
|||||||
/// Quote the replied-to event in the editor
|
/// Quote the replied-to event in the editor
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
quote_to: bool,
|
quote_to: bool,
|
||||||
/// Repository address in `naddr` format or `<nip5>/repo_id`. e.g.
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
/// `4rs.nl/n34` and `_@4rs.nl/n34`
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
///
|
///
|
||||||
/// If not provided, `n34` will look for the `nostr-address` file and if not
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
/// found, will get it from the root event if found.
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
#[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)]
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
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>,
|
||||||
@@ -119,10 +121,12 @@ pub struct ReplyArgs {
|
|||||||
impl CommandRunner for ReplyArgs {
|
impl CommandRunner for ReplyArgs {
|
||||||
async fn run(self, options: CliOptions) -> N34Result<()> {
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
let nostr_address_path = utils::nostr_address_path()?;
|
let nostr_address_path = utils::nostr_address_path()?;
|
||||||
let client = NostrClient::init(&options).await;
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
let user_pubk = options.pubkey().await?;
|
let user_pubk = options.pubkey().await?;
|
||||||
|
|
||||||
let repo_naddrs = if let Some(naddrs) = self.naddrs {
|
let repo_naddrs = if let Some(naddrs) = self.naddrs.flat_naddrs(&config.sets)? {
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
Some(naddrs)
|
Some(naddrs)
|
||||||
} else if fs::exists(&nostr_address_path).is_ok() {
|
} else if fs::exists(&nostr_address_path).is_ok() {
|
||||||
@@ -176,7 +180,7 @@ impl CommandRunner for ReplyArgs {
|
|||||||
let author_read_relays =
|
let author_read_relays =
|
||||||
utils::add_read_relays(client.user_relays_list(user_pubk).await?.as_ref());
|
utils::add_read_relays(client.user_relays_list(user_pubk).await?.as_ref());
|
||||||
let write_relays = [
|
let write_relays = [
|
||||||
options.relays,
|
relays,
|
||||||
utils::add_write_relays(relays_list.as_ref()),
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
// Merge repository announcement relays into write relays
|
// Merge repository announcement relays into write relays
|
||||||
repos.extract_relays(),
|
repos.extract_relays(),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use futures::future;
|
|||||||
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
use nostr::{event::EventBuilder, key::PublicKey, types::Url};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, CommandRunner, NOSTR_ADDRESS_FILE},
|
cli::{CliConfig, CliOptions, CommandRunner, NOSTR_ADDRESS_FILE, types::RelayOrSetVecExt},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement, utils},
|
||||||
};
|
};
|
||||||
@@ -81,7 +81,9 @@ impl CommandRunner for AnnounceArgs {
|
|||||||
async fn run(mut self, options: CliOptions) -> N34Result<()> {
|
async fn run(mut self, options: CliOptions) -> N34Result<()> {
|
||||||
options.ensure_relays()?;
|
options.ensure_relays()?;
|
||||||
|
|
||||||
let client = NostrClient::init(&options).await;
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).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?;
|
||||||
|
|
||||||
@@ -89,14 +91,14 @@ impl CommandRunner for AnnounceArgs {
|
|||||||
self.maintainers.insert(0, user_pubk);
|
self.maintainers.insert(0, user_pubk);
|
||||||
}
|
}
|
||||||
|
|
||||||
let naddr = utils::repo_naddr(&self.repo_id, user_pubk, &options.relays)?;
|
let naddr = utils::repo_naddr(&self.repo_id, user_pubk, &relays)?;
|
||||||
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),
|
||||||
self.description.map(utils::str_trim),
|
self.description.map(utils::str_trim),
|
||||||
self.web,
|
self.web,
|
||||||
self.clone,
|
self.clone,
|
||||||
options.relays.clone(),
|
relays.clone(),
|
||||||
self.maintainers.clone(),
|
self.maintainers.clone(),
|
||||||
self.label.into_iter().map(utils::str_trim).collect(),
|
self.label.into_iter().map(utils::str_trim).collect(),
|
||||||
self.force_id,
|
self.force_id,
|
||||||
@@ -125,7 +127,7 @@ impl CommandRunner for AnnounceArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let write_relays = [
|
let write_relays = [
|
||||||
options.relays.clone(),
|
relays,
|
||||||
utils::add_write_relays(relays_list.as_ref()),
|
utils::add_write_relays(relays_list.as_ref()),
|
||||||
// Include read relays for each maintainer (if found)
|
// Include read relays for each maintainer (if found)
|
||||||
future::join_all(
|
future::join_all(
|
||||||
|
|||||||
@@ -17,10 +17,14 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use nostr::nips::nip19::Nip19Coordinate;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{CliOptions, CommandRunner, parsers},
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
CommandRunner,
|
||||||
|
types::{NaddrOrSet, OptionNaddrOrSetVecExt, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
error::N34Result,
|
error::N34Result,
|
||||||
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
nostr_utils::{NostrClient, traits::NaddrsUtils, utils},
|
||||||
};
|
};
|
||||||
@@ -28,20 +32,25 @@ use crate::{
|
|||||||
/// Arguments for the `repo view` command
|
/// Arguments for the `repo view` command
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub struct ViewArgs {
|
pub struct ViewArgs {
|
||||||
/// Repository address in `naddr` format or `<nip5>/repo_id`. e.g.
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
/// `4rs.nl/n34` and `_@4rs.nl/n34`
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
///
|
///
|
||||||
/// If not provided, `n34` will look for the `nostr-address` file.
|
/// If omitted, looks for a `nostr-address` file.
|
||||||
#[arg(value_name = "NADDR-OR-NIP05", long = "repo", value_parser = parsers::repo_naddr)]
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
naddrs: Option<Vec<Nip19Coordinate>>,
|
naddrs: Option<Vec<NaddrOrSet>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 naddrs = utils::naddrs_or_file(self.naddrs, &utils::nostr_address_path()?)?;
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
let client = NostrClient::init(&options).await;
|
let naddrs = utils::naddrs_or_file(
|
||||||
|
self.naddrs.flat_naddrs(&config.sets)?,
|
||||||
|
&utils::nostr_address_path()?,
|
||||||
|
)?;
|
||||||
|
let relays = options.relays.clone().flat_relays(&config.sets)?;
|
||||||
|
let client = NostrClient::init(&options, &relays).await;
|
||||||
client.add_relays(&naddrs.extract_relays()).await;
|
client.add_relays(&naddrs.extract_relays()).await;
|
||||||
|
|
||||||
let repos = client.fetch_repos(&naddrs.into_coordinates()).await?;
|
let repos = client.fetch_repos(&naddrs.into_coordinates()).await?;
|
||||||
|
|||||||
57
src/cli/commands/sets/mod.rs
Normal file
57
src/cli/commands/sets/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
/// `sets new` command
|
||||||
|
mod new;
|
||||||
|
/// `sets remove` command
|
||||||
|
mod remove;
|
||||||
|
/// `sets show` commands
|
||||||
|
mod show;
|
||||||
|
/// `sets update` command
|
||||||
|
mod update;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use self::new::NewArgs;
|
||||||
|
use self::remove::RemoveArgs;
|
||||||
|
use self::show::ShowArgs;
|
||||||
|
use self::update::UpdateArgs;
|
||||||
|
use super::{CliOptions, CommandRunner};
|
||||||
|
use crate::error::N34Result;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum SetsSubcommands {
|
||||||
|
/// Remove a set, or specific repos and relays within it
|
||||||
|
Remove(RemoveArgs),
|
||||||
|
/// Create a new set
|
||||||
|
New(NewArgs),
|
||||||
|
/// Modify an existing set
|
||||||
|
Update(UpdateArgs),
|
||||||
|
/// Show a single set or all the stored sets
|
||||||
|
Show(ShowArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl CommandRunner for SetsSubcommands {
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Remove(args) => args.run(options).await,
|
||||||
|
Self::New(args) => args.run(options).await,
|
||||||
|
Self::Update(args) => args.run(options).await,
|
||||||
|
Self::Show(args) => args.run(options).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/cli/commands/sets/new.rs
Normal file
59
src/cli/commands/sets/new.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
ConfigError,
|
||||||
|
MutRepoRelaySetsExt,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct NewArgs {
|
||||||
|
/// Unique name for the set
|
||||||
|
name: String,
|
||||||
|
/// Optional relay to add it to the set, either as URL or set name to
|
||||||
|
/// extract its relays. [aliases: `--sr`]
|
||||||
|
#[arg(long = "set-relay", alias("sr"))]
|
||||||
|
relays: Vec<RelayOrSet>,
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Vec<NaddrOrSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for NewArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let mut config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = self.naddrs.flat_naddrs(&config.sets)?;
|
||||||
|
let relays = self.relays.flat_relays(&config.sets)?;
|
||||||
|
|
||||||
|
if relays.is_empty() && naddrs.is_empty() {
|
||||||
|
return Err(ConfigError::NewEmptySet.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
config.sets.push_set(self.name, naddrs, relays)?;
|
||||||
|
config.dump_toml(&options.config_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/cli/commands/sets/remove.rs
Normal file
65
src/cli/commands/sets/remove.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
MutRepoRelaySetsExt,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct RemoveArgs {
|
||||||
|
/// Set name to delete
|
||||||
|
name: String,
|
||||||
|
/// Specific relay to remove it from the set, either as URL or set name to
|
||||||
|
/// extract its relays. [aliases: `--sr`]
|
||||||
|
#[arg(long = "set-relay", alias("sr"))]
|
||||||
|
relays: Vec<RelayOrSet>,
|
||||||
|
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Vec<NaddrOrSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for RemoveArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let mut config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = self.naddrs.flat_naddrs(&config.sets)?;
|
||||||
|
let relays = self.relays.flat_relays(&config.sets)?;
|
||||||
|
|
||||||
|
if relays.is_empty() && naddrs.is_empty() {
|
||||||
|
config.sets.remove_set(self.name)?;
|
||||||
|
} else {
|
||||||
|
if !relays.is_empty() {
|
||||||
|
config.sets.remove_relays(&self.name, relays.into_iter())?;
|
||||||
|
}
|
||||||
|
if !naddrs.is_empty() {
|
||||||
|
config.sets.remove_naddrs(self.name, naddrs.into_iter())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config.dump_toml(&options.config_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/cli/commands/sets/show.rs
Normal file
85
src/cli/commands/sets/show.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
use nostr::nips::nip19::ToBech32;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{CliConfig, CliOptions, RepoRelaySet, RepoRelaySetsExt, traits::CommandRunner},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct ShowArgs {
|
||||||
|
/// Name of the set to display. If not provided, lists all available sets.
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for ShowArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
|
||||||
|
if let Some(name) = self.name {
|
||||||
|
println!("{}", format_set(config.sets.as_slice().get_set(&name)?));
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
config
|
||||||
|
.sets
|
||||||
|
.iter()
|
||||||
|
.map(format_set)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n----------\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a set to view it to the user
|
||||||
|
fn format_set(set: &RepoRelaySet) -> String {
|
||||||
|
let naddrs = if set.naddrs.is_empty() {
|
||||||
|
"Nothing".to_owned()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"\n- {}",
|
||||||
|
set.naddrs
|
||||||
|
.iter()
|
||||||
|
.map(|naddr| naddr.to_bech32().expect("We did decoded before"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n- ")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let relays = if set.relays.is_empty() {
|
||||||
|
"Nothing".to_owned()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"\n- {}",
|
||||||
|
set.relays
|
||||||
|
.iter()
|
||||||
|
.map(|relay| relay.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n- ")
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Name: {}\nّّRepositories: {naddrs}\nRelays: {relays}",
|
||||||
|
set.name
|
||||||
|
)
|
||||||
|
}
|
||||||
72
src/cli/commands/sets/update.rs
Normal file
72
src/cli/commands/sets/update.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use clap::Args;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cli::{
|
||||||
|
CliConfig,
|
||||||
|
CliOptions,
|
||||||
|
MutRepoRelaySetsExt,
|
||||||
|
traits::CommandRunner,
|
||||||
|
types::{NaddrOrSet, NaddrOrSetVecExt, RelayOrSet, RelayOrSetVecExt},
|
||||||
|
},
|
||||||
|
error::N34Result,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct UpdateArgs {
|
||||||
|
/// Name of the set to update
|
||||||
|
name: String,
|
||||||
|
/// Add relay to the set, either as URL or set name to extract its relays.
|
||||||
|
/// [aliases: `--sr`]
|
||||||
|
#[arg(long = "set-relay", alias("sr"))]
|
||||||
|
relays: Vec<RelayOrSet>,
|
||||||
|
/// Repository address in `naddr` format (`naddr1...`), NIP-05 format
|
||||||
|
/// (`4rs.nl/n34` or `_@4rs.nl/n34`), or a set name like `kernel`.
|
||||||
|
#[arg(value_name = "NADDR-NIP05-OR-SET", long = "repo")]
|
||||||
|
naddrs: Vec<NaddrOrSet>,
|
||||||
|
/// Replace existing relays/repositories instead of adding to them
|
||||||
|
#[arg(long = "override")]
|
||||||
|
override_set: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CommandRunner for UpdateArgs {
|
||||||
|
// FIXME: The signer is not required here
|
||||||
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
||||||
|
let mut config = CliConfig::load_toml(&options.config_path)?;
|
||||||
|
let naddrs = self.naddrs.flat_naddrs(&config.sets)?;
|
||||||
|
let relays = self.relays.flat_relays(&config.sets)?;
|
||||||
|
|
||||||
|
let set = config.sets.get_mut_set(&self.name)?;
|
||||||
|
|
||||||
|
if self.override_set {
|
||||||
|
if !relays.is_empty() {
|
||||||
|
set.relays = HashSet::from_iter(relays);
|
||||||
|
}
|
||||||
|
if !naddrs.is_empty() {
|
||||||
|
set.naddrs = HashSet::from_iter(naddrs)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
set.relays.extend(relays);
|
||||||
|
set.naddrs.extend(naddrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.dump_toml(&options.config_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
303
src/cli/config.rs
Normal file
303
src/cli/config.rs
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
use std::{collections::HashSet, fs, path::Path};
|
||||||
|
|
||||||
|
use nostr::{
|
||||||
|
nips::nip19::{FromBech32, Nip19Coordinate, ToBech32},
|
||||||
|
types::RelayUrl,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::error::{N34Error, N34Result};
|
||||||
|
|
||||||
|
/// Errors that can occur when working with configuration files.
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error(
|
||||||
|
"Could not determine the default config path: both `$XDG_CONFIG_HOME` and `$HOME` \
|
||||||
|
environment variables are missing or unset."
|
||||||
|
)]
|
||||||
|
CanNotFindConfigPath,
|
||||||
|
#[error("Couldn't read the config file: {0}")]
|
||||||
|
ReadFile(std::io::Error),
|
||||||
|
#[error("Couldn't write in the config file: {0}")]
|
||||||
|
WriteFile(std::io::Error),
|
||||||
|
#[error("Couldn't serialize the config. This is a bug, please report it: {0}")]
|
||||||
|
Serialize(toml::ser::Error),
|
||||||
|
#[error("Failed to parse the config file: {0}")]
|
||||||
|
ParseFile(toml::de::Error),
|
||||||
|
#[error("Duplicate configuration set name detected: '{0}'. Each set must have a unique name.")]
|
||||||
|
SetDuplicateName(String),
|
||||||
|
#[error("No set with the given name `{0}`")]
|
||||||
|
SetNotFound(String),
|
||||||
|
#[error("You can't create an new empty set.")]
|
||||||
|
NewEmptySet,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the command-line interface.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)]
|
||||||
|
pub struct CliConfig {
|
||||||
|
/// Groups of repositories and relays.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub sets: Vec<RepoRelaySet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named group of repositories and relays.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize, Default, Clone, Debug)]
|
||||||
|
pub struct RepoRelaySet {
|
||||||
|
/// Unique identifier for this group.
|
||||||
|
pub name: String,
|
||||||
|
/// Repository addresses in this group.
|
||||||
|
#[serde(
|
||||||
|
default,
|
||||||
|
skip_serializing_if = "HashSet::is_empty",
|
||||||
|
serialize_with = "ser_naddrs",
|
||||||
|
deserialize_with = "de_naddrs"
|
||||||
|
)]
|
||||||
|
pub naddrs: HashSet<Nip19Coordinate>,
|
||||||
|
/// Relay URLs in this group.
|
||||||
|
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
|
||||||
|
pub relays: HashSet<RelayUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliConfig {
|
||||||
|
/// Reads and parse a TOML config file from the given path, creating it if
|
||||||
|
/// missing.
|
||||||
|
pub fn load_toml(file_path: &Path) -> N34Result<Self> {
|
||||||
|
tracing::info!(path = %file_path.display(), "Loading configuration from file");
|
||||||
|
// Make sure the file is exist
|
||||||
|
if let Some(parent) = file_path.parent() {
|
||||||
|
if !parent.exists() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = fs::File::create_new(file_path);
|
||||||
|
|
||||||
|
let mut config: Self =
|
||||||
|
toml::from_str(&fs::read_to_string(file_path).map_err(ConfigError::ReadFile)?)
|
||||||
|
.map_err(ConfigError::ParseFile)?;
|
||||||
|
|
||||||
|
config.post_sets()?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dump the config as toml in a file
|
||||||
|
pub fn dump_toml(mut self, file_path: &Path) -> N34Result<()> {
|
||||||
|
tracing::debug!(config = ?self, "Writing configuration to {}", file_path.display());
|
||||||
|
self.post_sets()?;
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
file_path,
|
||||||
|
toml::to_string_pretty(&self).map_err(ConfigError::Serialize)?,
|
||||||
|
)
|
||||||
|
.map_err(ConfigError::WriteFile)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs post-processing validation on the sets after loading or before
|
||||||
|
/// dumping.
|
||||||
|
fn post_sets(&mut self) -> N34Result<()> {
|
||||||
|
self.sets.as_slice().ensure_names()?;
|
||||||
|
self.sets.dedup_naddrs();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepoRelaySet {
|
||||||
|
/// Create a new [`RepoRelaySet`]
|
||||||
|
pub fn new(
|
||||||
|
name: impl Into<String>,
|
||||||
|
naddrs: impl IntoIterator<Item = Nip19Coordinate>,
|
||||||
|
relays: impl IntoIterator<Item = RelayUrl>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
naddrs: HashSet::from_iter(naddrs),
|
||||||
|
relays: HashSet::from_iter(relays),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes duplicate repository addresses by comparing their coordinates,
|
||||||
|
/// ignoring embedded relays.
|
||||||
|
pub fn dedup_naddrs(&mut self) {
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
self.naddrs.retain(|n| seen.insert(n.coordinate.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(MutRepoRelaySetsExt)]
|
||||||
|
impl Vec<RepoRelaySet> {
|
||||||
|
/// Removes duplicate repository addresses from each set.
|
||||||
|
///
|
||||||
|
/// Relays are automatically deduplicated by the HashSet, but
|
||||||
|
/// repository addresses may appear duplicated if relays are sorted
|
||||||
|
/// differently or when relay counts vary. This compares addresses by
|
||||||
|
/// their coordinates, ignoring any embedded relay details.
|
||||||
|
pub fn dedup_naddrs(&mut self) {
|
||||||
|
self.iter_mut().for_each(RepoRelaySet::dedup_naddrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds and returns a mutable reference a set with the given name. Returns
|
||||||
|
/// an error if no set with this name exists.
|
||||||
|
pub fn get_mut_set(&mut self, name: impl AsRef<str>) -> N34Result<&mut RepoRelaySet> {
|
||||||
|
let name = name.as_ref();
|
||||||
|
let set = self
|
||||||
|
.iter_mut()
|
||||||
|
.find(|set| set.name == name)
|
||||||
|
.ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
name = %name, set = ?set,
|
||||||
|
"Successfully located a set with the giving name"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates and pushes a new set with the given name.
|
||||||
|
///
|
||||||
|
/// Returns an error if a set with the same name already exists.
|
||||||
|
pub fn push_set(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
repos: impl IntoIterator<Item = Nip19Coordinate>,
|
||||||
|
relays: impl IntoIterator<Item = RelayUrl>,
|
||||||
|
) -> N34Result<()> {
|
||||||
|
let set_name: String = name.into();
|
||||||
|
tracing::trace!(sets = ?self, "Pushing set '{set_name}' to sets collection");
|
||||||
|
|
||||||
|
if self.as_slice().exists(&set_name) {
|
||||||
|
return Err(ConfigError::SetDuplicateName(set_name).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push(RepoRelaySet::new(set_name, repos, relays));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the set with the given name if it exists. Returns an error if
|
||||||
|
/// the set is not found.
|
||||||
|
pub fn remove_set(&mut self, name: impl Into<String>) -> N34Result<()> {
|
||||||
|
let set_name: String = name.into();
|
||||||
|
tracing::trace!(set_name, sets = ?self, "Removing set '{set_name}' from sets collection");
|
||||||
|
|
||||||
|
if !self.as_slice().exists(&set_name) {
|
||||||
|
return Err(ConfigError::SetNotFound(set_name).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.retain(|s| s.name != set_name);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the given relays from the specified set.
|
||||||
|
pub fn remove_relays(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
relays: impl Iterator<Item = RelayUrl>,
|
||||||
|
) -> N34Result<()> {
|
||||||
|
let relays = Vec::from_iter(relays);
|
||||||
|
let set = self.get_mut_set(name.into())?;
|
||||||
|
|
||||||
|
set.relays.retain(|r| !relays.contains(r));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the given naddrs from the specified set.
|
||||||
|
pub fn remove_naddrs(
|
||||||
|
&mut self,
|
||||||
|
name: impl Into<String>,
|
||||||
|
naddrs: impl Iterator<Item = Nip19Coordinate>,
|
||||||
|
) -> N34Result<()> {
|
||||||
|
let coordinates = Vec::from_iter(naddrs.map(|n| n.coordinate));
|
||||||
|
let set = self.get_mut_set(name.into())?;
|
||||||
|
|
||||||
|
set.naddrs.retain(|n| !coordinates.contains(&n.coordinate));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(RepoRelaySetsExt)]
|
||||||
|
impl &[RepoRelaySet] {
|
||||||
|
/// Checks for duplicate set names. Returns an error if any duplicates are
|
||||||
|
/// found.
|
||||||
|
pub fn ensure_names(&self) -> N34Result<()> {
|
||||||
|
let mut names = Vec::with_capacity(self.len());
|
||||||
|
names.extend(self.iter().map(|s| s.name.to_owned()));
|
||||||
|
|
||||||
|
names.sort_unstable();
|
||||||
|
|
||||||
|
if let Some(duplicate) = duplicate_in_sorted(&names) {
|
||||||
|
return Err(ConfigError::SetDuplicateName(duplicate.clone()).into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a set with the given name exists.
|
||||||
|
pub fn exists(&self, set_name: &str) -> bool {
|
||||||
|
self.iter().any(|set| set.name == set_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds and returns a reference a set with the given name. Returns an
|
||||||
|
/// error if no set with this name exists.
|
||||||
|
pub fn get_set(&self, name: impl AsRef<str>) -> N34Result<&RepoRelaySet> {
|
||||||
|
let name = name.as_ref();
|
||||||
|
let set = self
|
||||||
|
.iter()
|
||||||
|
.find(|set| set.name == name)
|
||||||
|
.ok_or_else(|| N34Error::from(ConfigError::SetNotFound(name.to_owned())))?;
|
||||||
|
tracing::trace!(
|
||||||
|
name = %name, set = ?set,
|
||||||
|
"Successfully located a set with the giving name"
|
||||||
|
);
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function that checks for duplicates in a sorted slice
|
||||||
|
fn duplicate_in_sorted<T: PartialEq + Clone>(items: &[T]) -> Option<&T> {
|
||||||
|
items.windows(2).find(|w| w[0] == w[1]).map(|w| &w[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ser_naddrs<S>(naddr: &HashSet<Nip19Coordinate>, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let str_naddrs = naddr
|
||||||
|
.iter()
|
||||||
|
.map(|n| n.to_bech32().map_err(|err| err.to_string()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(serde::ser::Error::custom)?;
|
||||||
|
|
||||||
|
str_naddrs.serialize(serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn de_naddrs<'de, D>(deserializer: D) -> Result<HashSet<Nip19Coordinate>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Vec::<String>::deserialize(deserializer)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|naddr| Nip19Coordinate::from_bech32(&naddr))
|
||||||
|
.collect::<Result<HashSet<_>, _>>()
|
||||||
|
.map_err(serde::de::Error::custom)
|
||||||
|
}
|
||||||
28
src/cli/defaults.rs
Normal file
28
src/cli/defaults.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{cli::ConfigError, error::N34Result};
|
||||||
|
|
||||||
|
|
||||||
|
/// Default config path
|
||||||
|
pub fn config_path() -> N34Result<PathBuf> {
|
||||||
|
Ok(dirs::config_dir()
|
||||||
|
.ok_or(ConfigError::CanNotFindConfigPath)?
|
||||||
|
.join("n34")
|
||||||
|
.join("config.toml"))
|
||||||
|
}
|
||||||
@@ -15,16 +15,23 @@
|
|||||||
// 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>.
|
||||||
|
|
||||||
/// Commands module
|
/// Commands module
|
||||||
mod commands;
|
pub mod commands;
|
||||||
|
/// The CLI config
|
||||||
|
pub mod config;
|
||||||
|
/// Default lazy values for CLI arguments
|
||||||
|
pub mod defaults;
|
||||||
/// CLI arguments parsers
|
/// CLI arguments parsers
|
||||||
pub mod parsers;
|
pub mod parsers;
|
||||||
/// CLI traits
|
/// CLI traits
|
||||||
mod traits;
|
pub mod traits;
|
||||||
|
/// Common helper types used throughout the CLI.
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use clap_verbosity_flag::Verbosity;
|
use clap_verbosity_flag::Verbosity;
|
||||||
|
|
||||||
pub use self::commands::*;
|
pub use self::commands::*;
|
||||||
|
pub use self::config::*;
|
||||||
use self::traits::CommandRunner;
|
use self::traits::CommandRunner;
|
||||||
use crate::error::N34Result;
|
use crate::error::N34Result;
|
||||||
|
|
||||||
|
|||||||
@@ -18,35 +18,16 @@ use std::{fs, path::Path};
|
|||||||
|
|
||||||
use nostr::{
|
use nostr::{
|
||||||
Kind,
|
Kind,
|
||||||
nips::{
|
nips::nip19::{FromBech32, Nip19Coordinate},
|
||||||
self,
|
|
||||||
nip01::Coordinate,
|
|
||||||
nip19::{FromBech32, Nip19Coordinate},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use tokio::runtime::Handle;
|
|
||||||
|
|
||||||
use crate::error::{N34Error, N34Result};
|
use super::Cli;
|
||||||
|
use crate::{
|
||||||
|
cli::DEFAULT_FALLBACK_PATH,
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<Nip19Coordinate, String> {
|
pub fn parse_repo_naddr(repo_naddr: &str) -> Result<Nip19Coordinate, String> {
|
||||||
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
|
|
||||||
|
|
||||||
let nip5_profile = tokio::task::block_in_place(|| {
|
|
||||||
Handle::current().block_on(async {
|
|
||||||
nips::nip05::profile(format!("{username}@{domain}"), None)
|
|
||||||
.await
|
|
||||||
.map_err(|err| err.to_string())
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Nip19Coordinate::new(
|
|
||||||
Coordinate::new(Kind::GitRepoAnnouncement, nip5_profile.public_key).identifier(repo_id),
|
|
||||||
nip5_profile.relays,
|
|
||||||
)
|
|
||||||
.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())?;
|
let naddr = Nip19Coordinate::from_bech32(repo_naddr).map_err(|err| err.to_string())?;
|
||||||
if naddr.relays.is_empty() {
|
if naddr.relays.is_empty() {
|
||||||
tracing::warn!("The repository naddr does not contain any relay hints");
|
tracing::warn!("The repository naddr does not contain any relay hints");
|
||||||
@@ -74,27 +55,11 @@ pub fn parse_nostr_address_file(file_path: &Path) -> N34Result<Vec<Nip19Coordina
|
|||||||
Ok(addresses)
|
Ok(addresses)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a Git repository address which can be either:
|
/// Post parse cli arguments
|
||||||
/// - A bech32-encoded naddr (e.g. "naddr1...") for Git repository announcements
|
pub fn post_parse_cli(mut cli: Cli) -> N34Result<Cli> {
|
||||||
/// (kind 30617)
|
if let Some(DEFAULT_FALLBACK_PATH) = cli.options.config_path.to_str() {
|
||||||
/// - A NIP-05 identifier with repository ID (e.g. "4rs.nl/n34" or
|
cli.options.config_path = super::defaults::config_path()?;
|
||||||
/// "_@4rs.nl/n34")
|
}
|
||||||
///
|
|
||||||
/// Returns an error for invalid formats, failed bech32 decoding, wrong event
|
|
||||||
/// kind.
|
|
||||||
pub fn repo_naddr(repo: &str) -> Result<Nip19Coordinate, String> {
|
|
||||||
let repo = repo.trim().trim_start_matches("nostr:");
|
|
||||||
|
|
||||||
if repo.contains("/") {
|
Ok(cli)
|
||||||
let (nip5, repo_id) = repo.split_once("/").expect("There is a `/`");
|
|
||||||
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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
194
src/cli/types.rs
Normal file
194
src/cli/types.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
|
||||||
|
// Copyright (C) 2025 Awiteb <a@4rs.nl>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// 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>.
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use nostr::{
|
||||||
|
event::Kind,
|
||||||
|
nips::{self, nip01::Coordinate, nip19::Nip19Coordinate},
|
||||||
|
types::RelayUrl,
|
||||||
|
};
|
||||||
|
use tokio::runtime::Handle;
|
||||||
|
|
||||||
|
use super::{RepoRelaySetsExt, parsers};
|
||||||
|
use crate::{
|
||||||
|
cli::RepoRelaySet,
|
||||||
|
error::{N34Error, N34Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Either a NIP-19 coordinate (naddr) or a named set.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum NaddrOrSet {
|
||||||
|
/// NIP-19 coordinate.
|
||||||
|
Naddr(Nip19Coordinate),
|
||||||
|
/// Name of a set (may not exist).
|
||||||
|
Set(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Either relay URL or a named set.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RelayOrSet {
|
||||||
|
/// Relay URL.
|
||||||
|
Relay(RelayUrl),
|
||||||
|
/// Name of a set (may not exist).
|
||||||
|
Set(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NaddrOrSet {
|
||||||
|
/// Returns the naddr if `Naddr` or try to get the relays from the set.
|
||||||
|
/// Returns error if the set naddrs are empty or the set not found.
|
||||||
|
pub fn get_naddrs(self, sets: &[RepoRelaySet]) -> N34Result<Vec<Nip19Coordinate>> {
|
||||||
|
match self {
|
||||||
|
Self::Naddr(nip19_coordinate) => Ok(vec![nip19_coordinate]),
|
||||||
|
Self::Set(name) => {
|
||||||
|
let set = sets
|
||||||
|
.get_set(&name)
|
||||||
|
.map_err(|_| N34Error::InvalidNaddrArg(name.clone()))?;
|
||||||
|
if set.naddrs.is_empty() {
|
||||||
|
Err(N34Error::EmptySetNaddrs(name))
|
||||||
|
} else {
|
||||||
|
Ok(Vec::from_iter(set.naddrs.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl RelayOrSet {
|
||||||
|
/// Returns the relay if `Relay` or try to get the relays from the set.
|
||||||
|
/// Returns error if the set relays are empty or the set not found
|
||||||
|
pub fn get_relays(self, sets: &[RepoRelaySet]) -> N34Result<Vec<RelayUrl>> {
|
||||||
|
match self {
|
||||||
|
Self::Relay(relay) => Ok(vec![relay]),
|
||||||
|
Self::Set(name) => {
|
||||||
|
let set = sets.get_set(&name)?;
|
||||||
|
if set.relays.is_empty() {
|
||||||
|
Err(N34Error::EmptySetRelays(name))
|
||||||
|
} else {
|
||||||
|
Ok(Vec::from_iter(set.relays.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NaddrOrSet {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
/// Parses a Git repository address which can be either:
|
||||||
|
/// - A bech32-encoded naddr (e.g. "naddr1...") for Git repository
|
||||||
|
/// announcements (kind 30617)
|
||||||
|
/// - A NIP-05 identifier with repository ID (e.g. "4rs.nl/n34" or
|
||||||
|
/// "_@4rs.nl/n34")
|
||||||
|
/// - A set name.
|
||||||
|
///
|
||||||
|
/// Returns an error for invalid formats, failed bech32 decoding, wrong
|
||||||
|
/// event kind.
|
||||||
|
fn from_str(naddr_or_set: &str) -> Result<Self, Self::Err> {
|
||||||
|
let naddr_or_set = naddr_or_set.trim();
|
||||||
|
|
||||||
|
if naddr_or_set.contains("/") {
|
||||||
|
let (nip5, repo_id) = naddr_or_set.split_once("/").expect("There is a `/`");
|
||||||
|
parse_nip5_repo(nip5, repo_id)
|
||||||
|
} else if naddr_or_set.starts_with("naddr1") || naddr_or_set.starts_with("nostr:naddr1") {
|
||||||
|
parsers::parse_repo_naddr(naddr_or_set.trim_start_matches("nostr:")).map(Self::Naddr)
|
||||||
|
} else {
|
||||||
|
Ok(Self::Set(naddr_or_set.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RelayOrSet {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
/// Parse a string into a relay URL or a set name.
|
||||||
|
/// If the string is a valid URL (e.g., "wss://example.com"), it's treated
|
||||||
|
/// as a relay URL. Otherwise, it's treated as a set name, and its
|
||||||
|
/// associated relays will be merged.
|
||||||
|
fn from_str(relay_or_set: &str) -> Result<Self, Self::Err> {
|
||||||
|
let relay_or_set = relay_or_set.trim();
|
||||||
|
|
||||||
|
if relay_or_set.starts_with("wss://") {
|
||||||
|
RelayUrl::from_str(relay_or_set)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
.map(Self::Relay)
|
||||||
|
} else {
|
||||||
|
Ok(Self::Set(relay_or_set.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_nip5_repo(nip5: &str, repo_id: &str) -> Result<NaddrOrSet, String> {
|
||||||
|
let (username, domain) = nip5.split_once("@").unwrap_or(("_", nip5));
|
||||||
|
|
||||||
|
let nip5_profile = tokio::task::block_in_place(|| {
|
||||||
|
Handle::current().block_on(async {
|
||||||
|
nips::nip05::profile(format!("{username}@{domain}"), None)
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(NaddrOrSet::Naddr(
|
||||||
|
Nip19Coordinate::new(
|
||||||
|
Coordinate::new(Kind::GitRepoAnnouncement, nip5_profile.public_key).identifier(repo_id),
|
||||||
|
nip5_profile.relays,
|
||||||
|
)
|
||||||
|
.expect("The relays is `RelayUrl`"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(NaddrOrSetVecExt)]
|
||||||
|
impl Vec<NaddrOrSet> {
|
||||||
|
/// Converts this vector of [`NaddrOrSet`] into a flat vector of
|
||||||
|
/// [`Nip19Coordinates`] using the given sets.
|
||||||
|
pub fn flat_naddrs(self, sets: &[RepoRelaySet]) -> N34Result<Vec<Nip19Coordinate>> {
|
||||||
|
self.into_iter()
|
||||||
|
.map(|n| n.get_naddrs(sets))
|
||||||
|
.try_fold(Vec::new(), |mut acc, item| {
|
||||||
|
acc.extend(item?);
|
||||||
|
Ok(acc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[easy_ext::ext(RelayOrSetVecExt)]
|
||||||
|
impl Vec<RelayOrSet> {
|
||||||
|
/// Converts this vector of [`RelayOrSet`] into a flat vector of
|
||||||
|
/// [`RelayUrl`] using the given sets.
|
||||||
|
pub fn flat_relays(self, sets: &[RepoRelaySet]) -> N34Result<Vec<RelayUrl>> {
|
||||||
|
self.into_iter()
|
||||||
|
.map(|n| n.get_relays(sets))
|
||||||
|
.try_fold(Vec::new(), |mut acc, item| {
|
||||||
|
acc.extend(item?);
|
||||||
|
Ok(acc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[easy_ext::ext(OptionNaddrOrSetVecExt)]
|
||||||
|
impl Option<Vec<NaddrOrSet>> {
|
||||||
|
/// Converts this vector of [`NaddrOrSet`] into a flat vector of
|
||||||
|
/// [`Nip19Coordinates`] using the given sets.
|
||||||
|
pub fn flat_naddrs(&self, sets: &[RepoRelaySet]) -> N34Result<Option<Vec<Nip19Coordinate>>> {
|
||||||
|
// Clones self here to simplify command code
|
||||||
|
self.clone()
|
||||||
|
.map(|naddrs| naddrs.flat_naddrs(sets))
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/error.rs
17
src/error.rs
@@ -19,6 +19,8 @@ use std::process::ExitCode;
|
|||||||
use nostr::event::builder::Error as EventBuilderError;
|
use nostr::event::builder::Error as EventBuilderError;
|
||||||
use nostr_sdk::client::Error as ClientError;
|
use nostr_sdk::client::Error as ClientError;
|
||||||
|
|
||||||
|
use crate::cli::ConfigError;
|
||||||
|
|
||||||
pub type N34Result<T> = Result<T, N34Error>;
|
pub type N34Result<T> = Result<T, N34Error>;
|
||||||
|
|
||||||
/// N34 errors
|
/// N34 errors
|
||||||
@@ -26,6 +28,8 @@ pub type N34Result<T> = Result<T, N34Error>;
|
|||||||
pub enum N34Error {
|
pub enum N34Error {
|
||||||
#[error("IO: {0}")]
|
#[error("IO: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("{0}")]
|
||||||
|
Config(#[from] ConfigError),
|
||||||
#[error("No editor specified in the `EDITOR` environment variable")]
|
#[error("No editor specified in the `EDITOR` environment variable")]
|
||||||
EditorNotFound,
|
EditorNotFound,
|
||||||
#[error("The file you edited is empty. Please save your changes before exiting the editor.")]
|
#[error("The file you edited is empty. Please save your changes before exiting the editor.")]
|
||||||
@@ -62,6 +66,19 @@ pub enum N34Error {
|
|||||||
InvalidNostrAddressFileContent(String),
|
InvalidNostrAddressFileContent(String),
|
||||||
#[error("This command requires at least one relay, but none were provided")]
|
#[error("This command requires at least one relay, but none were provided")]
|
||||||
EmptyRelays,
|
EmptyRelays,
|
||||||
|
#[error(
|
||||||
|
"Invalid repository address. Expected one of these formats:\n- NIP-05 identifier with \
|
||||||
|
repository ID: `<user@domain.com>/<repo_id>`\n- Valid NIP-19 naddr string (starts with \
|
||||||
|
'naddr1...')\n- Existing set name (merges all repositories in set)\nError: No set named \
|
||||||
|
'{0}' exists."
|
||||||
|
)]
|
||||||
|
InvalidNaddrArg(String),
|
||||||
|
#[error(
|
||||||
|
"The set '{0}' doesn't contain any addresses. Use 'sets update' to add addresses to it."
|
||||||
|
)]
|
||||||
|
EmptySetNaddrs(String),
|
||||||
|
#[error("The set '{0}' doesn't contain any relays. Use 'sets update' to add addresses to it.")]
|
||||||
|
EmptySetRelays(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl N34Error {
|
impl N34Error {
|
||||||
|
|||||||
@@ -61,7 +61,14 @@ fn set_log_level(verbosity: Verbosity) {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> ExitCode {
|
async fn main() -> ExitCode {
|
||||||
let cli = Cli::parse();
|
let cli = match cli::parsers::post_parse_cli(Cli::parse()) {
|
||||||
|
Ok(cli) => cli,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{err}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
set_log_level(cli.verbosity);
|
set_log_level(cli.verbosity);
|
||||||
|
|
||||||
if let Err(err) = cli.run().await {
|
if let Err(err) = cli.run().await {
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ impl NostrClient {
|
|||||||
|
|
||||||
/// Initializes a new [`NostrClient`] instance and connects to the specified
|
/// Initializes a new [`NostrClient`] instance and connects to the specified
|
||||||
/// relays.
|
/// relays.
|
||||||
pub async fn init(options: &CliOptions) -> Self {
|
pub async fn init(options: &CliOptions, relays: &[RelayUrl]) -> Self {
|
||||||
let client = Self::new(
|
let client = Self::new(
|
||||||
Client::builder()
|
Client::builder()
|
||||||
.signer(Keys::new(
|
.signer(Keys::new(
|
||||||
@@ -115,7 +115,7 @@ impl NostrClient {
|
|||||||
.build(),
|
.build(),
|
||||||
);
|
);
|
||||||
|
|
||||||
client.add_relays(&options.relays).await;
|
client.add_relays(relays).await;
|
||||||
client
|
client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user