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)), + ) + } +}