// 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 convert_case::{Case, Casing}; use nostr::{ event::{EventBuilder, EventId, Tag, TagKind, TagStandard, Tags}, key::PublicKey, nips::{ nip01::Coordinate, nip21::Nip21, nip34::{GitIssue, GitRepositoryAnnouncement}, }, parser::Token, types::{RelayUrl, Url}, }; use crate::error::{N34Error, N34Result}; /// A trait to add helper instance function to [`Tags`] type #[easy_ext::ext(TagsExt)] impl Tags { /// Search for the given tag and map it value to a function pub fn map_tag(&self, kind: TagKind, f: impl FnOnce(&TagStandard) -> T) -> Option { self.find_standardized(kind).map(f) } /// Search for the given tag and map it value to a function. If the tag not /// found return the default `T` pub fn dmap_tag(&self, kind: TagKind, f: impl FnOnce(&TagStandard) -> T) -> T where T: Default, { self.map_tag(kind, f).unwrap_or_default() } /// Finds the first standard tag of the given kind with the specified /// marker, then applies the function to the tag and returns the result. pub fn map_marker( &self, kind: TagKind, marker: &str, f: impl FnOnce(&TagStandard) -> T, ) -> Option { self.filter_standardized(kind) .find(|t| (*t).clone().to_vec().last().is_some_and(|m| m == marker)) .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, force_id: bool, ) -> N34Result { let repo_id = repo_id.trim(); let kebab_repo_id = repo_id.to_case(Case::Kebab); if repo_id.is_empty() || (!force_id && repo_id != kebab_repo_id) { if repo_id != kebab_repo_id { tracing::error!( "The repo id should be `{kebab_repo_id}` (kebab-case). Use `--force-id` to \ override this check" ); } 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)), ) } /// Creates a new [`GitIssue`] event builder with the given /// issue details. pub fn new_git_issue( repository: Coordinate, content: String, subject: Option, labels: Vec, ) -> N34Result { EventBuilder::git_issue(GitIssue { repository, content, subject, labels: labels.into_iter().map(|l| l.trim().to_owned()).collect(), }) .map_err(N34Error::from) } } /// Helper functions for [`Token`] type #[easy_ext::ext(TokenUtils)] impl Token<'_> { /// Returns `Some((public_key, relays))` from the givin token if it's npub1 /// or nprofile1 pub fn extract_public_key(&self) -> Option<(PublicKey, Vec)> { match self { Token::Nostr(nip21) => { match nip21 { Nip21::Pubkey(pkey) => Some((*pkey, Vec::new())), Nip21::Profile(profile) => Some((profile.public_key, profile.relays.clone())), _ => None, } } _ => None, } } /// Returns `Some((note_id, relays))` from the givin token if it's note1 or /// nevent1 pub fn extract_event_id(&self) -> Option<(EventId, Vec)> { match self { Token::Nostr(nip21) => { match nip21 { Nip21::EventId(event_id) => Some((*event_id, Vec::new())), Nip21::Event(event) => Some((event.event_id, event.relays.clone())), _ => None, } } _ => None, } } /// Returns `Some(hashtag)` from the givin token if it's a hashtag pub fn extract_hashtag(&self) -> Option { match self { Token::Hashtag(tag) => Some(tag.trim().to_owned()), _ => None, } } }