From b444aeba0ac6d19cb32715fb70c7dc1da297f8e9 Mon Sep 17 00:00:00 2001
From: Awiteb
Date: Mon, 5 May 2025 19:55:53 +0000
Subject: [PATCH] feat: Add `repo announce` command
Signed-off-by: Awiteb
---
src/cli/repo/announce.rs | 107 ++++++++++++++++++++++++++++++++++++++
src/cli/repo/mod.rs | 8 +++
src/error.rs | 7 +++
src/nostr_utils/traits.rs | 46 +++++++++++++++-
4 files changed, 167 insertions(+), 1 deletion(-)
create mode 100644 src/cli/repo/announce.rs
diff --git a/src/cli/repo/announce.rs b/src/cli/repo/announce.rs
new file mode 100644
index 0000000..f6dbf69
--- /dev/null
+++ b/src/cli/repo/announce.rs
@@ -0,0 +1,107 @@
+// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
+// Copyright (C) 2025 Awiteb
+//
+// 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 .
+
+use clap::Args;
+use convert_case::{Case, Casing};
+use nostr::{
+ event::{EventBuilder, Kind, Tag},
+ key::PublicKey,
+ nips::{
+ nip01::Coordinate,
+ nip19::{Nip19Coordinate, Nip19Event, ToBech32},
+ nip34::GitRepositoryAnnouncement,
+ },
+ types::Url,
+};
+
+use crate::{
+ cli::{CliOptions, CommandRunner},
+ error::{N34Error, N34Result},
+ nostr_utils::{NostrClient, traits::NewGitRepositoryAnnouncement},
+};
+
+
+/// Arguments for the `repo announce` command
+#[derive(Args, Debug)]
+pub struct AnnounceArgs {
+ /// Unique identifier for the repository in kebab-case.
+ #[arg(long = "id")]
+ repo_id: String,
+ /// A name for the repository.
+ #[arg(short, long)]
+ name: Option,
+ /// A description for the repository.
+ #[arg(short, long)]
+ description: Option,
+ /// Webpage URLs for the repository (if provided by the git server).
+ #[arg(short, long)]
+ web: Vec,
+ /// URLs for cloning the repository.
+ #[arg(short, long)]
+ clone: Vec,
+ /// Additional maintainers of the repository (besides yourself).
+ #[arg(short, long)]
+ maintainers: Vec,
+ /// Labels to categorize the repository.
+ #[arg(short, long)]
+ labels: Vec,
+}
+
+impl CommandRunner for AnnounceArgs {
+ async fn run(self, options: CliOptions) -> N34Result<()> {
+ let client = NostrClient::init(&options).await;
+ let naddr = Nip19Coordinate::new(
+ Coordinate::new(Kind::GitRepoAnnouncement, options.pubkey().await),
+ options.relays.iter().take(3),
+ )
+ .expect("Valid relays");
+
+ let mut maintainers = vec![naddr.public_key];
+ maintainers.extend(self.maintainers);
+
+ let event_builder = EventBuilder::new_git_repo(
+ self.repo_id,
+ self.name,
+ self.description,
+ self.web,
+ self.clone,
+ options.relays.clone(),
+ maintainers,
+ self.labels,
+ )?;
+
+ let result = client
+ .send_builder_to(event_builder, &options.relays)
+ .await?;
+
+ for relay in &result.success {
+ tracing::info!(relay = %relay, "Event sent successfully");
+ }
+ for (relay, reason) in &result.failed {
+ tracing::warn!(relay = %relay, reason = %reason, "Failed to send event");
+ }
+
+ println!(
+ "Event: {}",
+ Nip19Event::new(result.val)
+ .relays(options.relays.into_iter().take(3))
+ .to_bech32()?
+ );
+ println!("Repo Address: {}", naddr.to_bech32()?);
+
+ Ok(())
+ }
+}
diff --git a/src/cli/repo/mod.rs b/src/cli/repo/mod.rs
index 9b4ed46..a77fc73 100644
--- a/src/cli/repo/mod.rs
+++ b/src/cli/repo/mod.rs
@@ -14,11 +14,14 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+/// `repo announce` subcommand
+mod announce;
/// `repo view` subcommand
mod view;
use clap::Subcommand;
+use self::announce::AnnounceArgs;
use self::view::ViewArgs;
use super::{CliOptions, CommandRunner};
use crate::error::N34Result;
@@ -27,12 +30,17 @@ use crate::error::N34Result;
pub enum RepoSubcommands {
/// View details of a nostr git repository
View(ViewArgs),
+ /// Publish information about a git repository to Nostr for collaboration
+ /// and feedback. Can also be used to update an existing repository's
+ /// details.
+ Announce(AnnounceArgs),
}
impl CommandRunner for RepoSubcommands {
async fn run(self, options: CliOptions) -> N34Result<()> {
match self {
Self::View(args) => args.run(options).await,
+ Self::Announce(args) => args.run(options).await,
}
}
}
diff --git a/src/error.rs b/src/error.rs
index ff68760..9d80f93 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -16,6 +16,7 @@
use std::process::ExitCode;
+use nostr::event::builder::Error as EventBuilderError;
use nostr_sdk::client::Error as ClientError;
pub type N34Result = Result;
@@ -27,6 +28,12 @@ pub enum N34Error {
Client(#[from] ClientError),
#[error("Unable to locate the repository. The repository may not exists in the given relays")]
NotFoundRepo,
+ #[error("Failed building an event: {0}")]
+ EventBuilder(#[from] EventBuilderError),
+ #[error("Invalid repository id, it can't be empty and must be kebab-case")]
+ InvalidRepoId,
+ #[error("Bech32 error: {0}")]
+ Bech32(#[from] nostr::nips::nip19::Error),
}
impl N34Error {
diff --git a/src/nostr_utils/traits.rs b/src/nostr_utils/traits.rs
index 22cfb5d..6c1afbd 100644
--- a/src/nostr_utils/traits.rs
+++ b/src/nostr_utils/traits.rs
@@ -14,7 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use nostr::event::{TagKind, TagStandard, Tags};
+use convert_case::{Case, Casing};
+use nostr::{
+ event::{EventBuilder, Tag, TagKind, TagStandard, Tags},
+ key::PublicKey,
+ nips::nip34::GitRepositoryAnnouncement,
+ types::{RelayUrl, Url},
+};
+
+use crate::error::{N34Error, N34Result};
/// A trait to add helper instance function to [`Tags`] type
@@ -47,3 +55,39 @@ impl Tags {
.map(f)
}
}
+
+/// Trait for building [`GitRepositoryAnnouncement`] events
+#[easy_ext::ext(NewGitRepositoryAnnouncement)]
+impl EventBuilder {
+ /// Creates a new [`GitRepositoryAnnouncement`] event builder with the given
+ /// repository details.
+ #[allow(clippy::too_many_arguments)]
+ pub fn new_git_repo(
+ repo_id: String,
+ name: Option,
+ description: Option,
+ web: Vec,
+ clone: Vec,
+ relays: Vec,
+ maintainers: Vec,
+ labels: Vec,
+ ) -> N34Result {
+ if repo_id.is_empty() || repo_id != repo_id.to_case(Case::Kebab) {
+ return Err(N34Error::InvalidRepoId);
+ }
+
+ Ok(
+ EventBuilder::git_repository_announcement(GitRepositoryAnnouncement {
+ id: repo_id.to_owned(),
+ name,
+ description,
+ web,
+ clone,
+ relays,
+ euc: None,
+ maintainers,
+ })?
+ .tags(labels.into_iter().map(Tag::hashtag)),
+ )
+ }
+}