feat: Add repo announce command

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb
2025-05-05 19:55:53 +00:00
parent 273058fe3f
commit b444aeba0a
4 changed files with 167 additions and 1 deletions

107
src/cli/repo/announce.rs Normal file
View File

@@ -0,0 +1,107 @@
// 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 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<String>,
/// A description for the repository.
#[arg(short, long)]
description: Option<String>,
/// Webpage URLs for the repository (if provided by the git server).
#[arg(short, long)]
web: Vec<Url>,
/// URLs for cloning the repository.
#[arg(short, long)]
clone: Vec<Url>,
/// Additional maintainers of the repository (besides yourself).
#[arg(short, long)]
maintainers: Vec<PublicKey>,
/// Labels to categorize the repository.
#[arg(short, long)]
labels: Vec<String>,
}
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(())
}
}

View File

@@ -14,11 +14,14 @@
// 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>.
/// `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,
}
}
}

View File

@@ -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<T> = Result<T, N34Error>;
@@ -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 {

View File

@@ -14,7 +14,15 @@
// 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 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<String>,
description: Option<String>,
web: Vec<Url>,
clone: Vec<Url>,
relays: Vec<RelayUrl>,
maintainers: Vec<PublicKey>,
labels: Vec<String>,
) -> N34Result<EventBuilder> {
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)),
)
}
}