Support for pull requests, it's not standard in NIPs yet[1] but there must be two clients that support it to be included in NIPs, n34 is one of them. This support: - Create a new PR `n34 pr new` - Update PR tip `n34 pr update` - View a PR `n34 pr view` - List PRs `n34 pr list` - Add `PR` and `PR-update` kinds as roots in `NostrClient::find_root` - Support issuing statuses for PRs - Support GRASP PR and PR-update - Update the docs [1] https://github.com/nostr-protocol/nips/pull/1966
211 lines
6.6 KiB
Rust
211 lines
6.6 KiB
Rust
// 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>.
|
|
|
|
/// `config` subcommands
|
|
pub mod config;
|
|
/// `issue` subcommands
|
|
pub mod issue;
|
|
/// `patch` subcommands
|
|
pub mod patch;
|
|
/// `pr` subcommands
|
|
pub mod pr;
|
|
/// `reply` command
|
|
pub mod reply;
|
|
/// `repo` subcommands
|
|
pub mod repo;
|
|
/// `sets` subcommands
|
|
pub mod sets;
|
|
|
|
use std::fmt;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use clap::{Args, Parser};
|
|
use nostr::key::{Keys, SecretKey};
|
|
use nostr::nips::nip46::NostrConnectURI;
|
|
use nostr::signer::{IntoNostrSigner, NostrSigner};
|
|
use nostr_connect::client::NostrConnect;
|
|
|
|
use self::config::ConfigSubcommands;
|
|
use self::issue::IssueSubcommands;
|
|
use self::patch::PatchSubcommands;
|
|
use self::pr::PrSubcommands;
|
|
use self::reply::ReplyArgs;
|
|
use self::repo::RepoSubcommands;
|
|
use self::sets::SetsSubcommands;
|
|
use super::CliConfig;
|
|
use super::options_state::OptionsState;
|
|
use super::types::RelayOrSet;
|
|
use super::{parsers, traits::CommandRunner};
|
|
use crate::cli::Cli;
|
|
use crate::cli::types::EchoAuthUrl;
|
|
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";
|
|
|
|
/// How long to wait for bunker response (3 minutes).
|
|
const BUNKER_TIMEOUT: Duration = Duration::from_secs(60 * 3);
|
|
|
|
/// The command-line interface options
|
|
#[derive(Args)]
|
|
pub struct CliOptions {
|
|
/// Your Nostr secret key
|
|
#[arg(short, long, group = "signer")]
|
|
pub secret_key: Option<SecretKey>,
|
|
/// NIP-46 bunker url used for signing events
|
|
#[arg(short, long, group = "signer", value_parser = parsers::parse_bunker_url)]
|
|
pub bunker_url: Option<NostrConnectURI>,
|
|
/// Enables signing events using the browser's NIP-07 extension. Listens on
|
|
/// `127.0.0.1:51034`.
|
|
#[arg(short = '7', long, group = "signer")]
|
|
pub nip07: bool,
|
|
/// Fallbacks relay to write and read from it. Multiple relays can be
|
|
/// passed, separated by commas.
|
|
#[arg(short, long, value_delimiter = ',')]
|
|
pub relays: Vec<RelayOrSet>,
|
|
/// Proof of Work difficulty when creatring events
|
|
#[arg(long, value_name = "DIFFICULTY")]
|
|
pub pow: Option<u8>,
|
|
/// Config path [default: `$XDG_CONFIG_HOME` or `$HOME/.config`]
|
|
#[arg(long, value_name = "PATH", default_value = DEFAULT_FALLBACK_PATH,
|
|
hide_default_value = true, value_parser = parsers::parse_config_path
|
|
)]
|
|
pub config: CliConfig,
|
|
/// The state of options. Some values that are used by them but should not
|
|
/// be entered via the CLI
|
|
#[arg(skip)]
|
|
pub state: OptionsState,
|
|
}
|
|
|
|
/// N34 commands
|
|
#[derive(Parser, Debug)]
|
|
pub enum Commands {
|
|
/// Manage repositories and relays sets
|
|
Sets {
|
|
#[command(subcommand)]
|
|
subcommands: SetsSubcommands,
|
|
},
|
|
/// Manage repositories
|
|
Repo {
|
|
#[command(subcommand)]
|
|
subcommands: RepoSubcommands,
|
|
},
|
|
/// Manage issues
|
|
Issue {
|
|
#[command(subcommand)]
|
|
subcommands: IssueSubcommands,
|
|
},
|
|
/// Manage patches
|
|
Patch {
|
|
#[command(subcommand)]
|
|
subcommands: PatchSubcommands,
|
|
},
|
|
/// Manage pull requests
|
|
Pr {
|
|
#[command(subcommand)]
|
|
subcommands: PrSubcommands,
|
|
},
|
|
/// Manage configuration
|
|
Config {
|
|
#[command(subcommand)]
|
|
subcommands: ConfigSubcommands,
|
|
},
|
|
/// Reply to issues and patches.
|
|
Reply(ReplyArgs),
|
|
}
|
|
|
|
|
|
impl CliOptions {
|
|
/// Returns the signer
|
|
pub async fn signer(&self) -> N34Result<Option<Arc<dyn NostrSigner + 'static>>> {
|
|
if self.nip07 {
|
|
self.state.browser_signer_proxy.start().await?;
|
|
|
|
println!(
|
|
"Browser signer proxy started at: {}",
|
|
self.state.browser_signer_proxy.url()
|
|
);
|
|
|
|
// FIXME: Use `BrowserSignerProxy::is_session_active` after it release
|
|
// nostr@0.44.0
|
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
|
|
|
return Ok(Some(
|
|
self.state.browser_signer_proxy.clone().into_nostr_signer(),
|
|
));
|
|
}
|
|
|
|
if let Some(sk) = &self.secret_key {
|
|
return Ok(Some(Keys::new(sk.clone()).into_nostr_signer()));
|
|
}
|
|
|
|
if let Some(ref bunker_url) = self.bunker_url {
|
|
let mut nostrconnect = NostrConnect::new(
|
|
bunker_url.clone(),
|
|
Cli::n34_keypair()?,
|
|
BUNKER_TIMEOUT,
|
|
None,
|
|
)
|
|
.expect("It's a bunker url and not a client");
|
|
|
|
nostrconnect.auth_url_handler(EchoAuthUrl);
|
|
return Ok(Some(nostrconnect.into_nostr_signer()));
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
/// Returns an error if there are no relays.
|
|
pub fn ensure_relays(&self) -> N34Result<()> {
|
|
if self.relays.is_empty() {
|
|
return Err(N34Error::EmptyRelays);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns an error if there are no signers
|
|
pub fn ensure_signer(&self) -> N34Result<()> {
|
|
if self.secret_key.is_none() && self.bunker_url.is_none() && !self.nip07 {
|
|
return Err(N34Error::SignerRequired);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl fmt::Debug for CliOptions {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("CliOptions")
|
|
.field("secret_key", &self.secret_key.as_ref().map(|_| "*******"))
|
|
.field("bunker_url", &self.bunker_url.as_ref().map(|_| "*******"))
|
|
.field("nip07", &self.nip07)
|
|
.field("relays", &self.relays)
|
|
.field("pow", &self.pow)
|
|
.field("config", &self.config)
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl CommandRunner for Commands {
|
|
async fn run(self, options: CliOptions) -> N34Result<()> {
|
|
tracing::trace!("Options: {options:#?}");
|
|
tracing::trace!("Handling: {self:#?}");
|
|
|
|
crate::run_command!(self, options, Repo Issue Sets Patch Pr Config & Reply)
|
|
}
|
|
}
|