From fda0fdd81caab1ca92eb7ed601058e6c2fdc59f5 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Sun, 21 May 2023 11:18:29 +0000 Subject: helpers and utilities --- src/branch_refs.rs | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/cli_helpers.rs | 160 +++++++++++++++++++++++++++++++ src/config.rs | 32 +++++++ src/kind.rs | 137 +++++++++++++++++++++++++++ src/ngit_tag.rs | 180 +++++++++++++++++++++++++++++++++++ src/utils.rs | 212 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 995 insertions(+) create mode 100644 src/branch_refs.rs create mode 100644 src/cli_helpers.rs create mode 100644 src/config.rs create mode 100644 src/kind.rs create mode 100644 src/ngit_tag.rs create mode 100644 src/utils.rs diff --git a/src/branch_refs.rs b/src/branch_refs.rs new file mode 100644 index 0000000..d6f9613 --- /dev/null +++ b/src/branch_refs.rs @@ -0,0 +1,274 @@ +use std::{path::PathBuf, fs, str::FromStr}; + +use nostr::{Event, Filter, Timestamp, secp256k1::XOnlyPublicKey, EventId}; +use nostr_sdk::blocking::Client; + +use crate::{utils::{load_event, save_event}, kind::Kind, repos::repo::Repo, groups::group::Group, repo_config::RepoConfig}; + + +pub struct BranchRefs { + pub branches: Vec, + pub pull_requests: Vec, + pub merges: Vec, + pub groups: Vec, + repo_dir_path: PathBuf, + pub most_recent_timestamp: Timestamp, +} + +impl BranchRefs { + pub fn new (branch_events: Vec, repo_dir_path: PathBuf) -> Self { + let mut refs = Self { + branches: vec![], + pull_requests: vec![], + merges: vec![], + groups: vec![], + repo_dir_path, + most_recent_timestamp: Timestamp::from(0), + }; + + // add repo first branch in branches vector + refs.update( + load_event(refs.repo_dir_path.join(".ngit/repo.json")) + .expect("repo.json to be present and load as event") + ); + + //load locally + for dir_name in [ + "groups", + "branches", + "merges", + "prs", + ] { + let dir_path = refs.repo_dir_path.join(".ngit").join(&dir_name); + if dir_path.exists() { + let dir = fs::read_dir(&dir_path) + .expect("read_dir to produce ReadDir from a path that exists"); + for entry in dir { + let path = entry + .expect("DirEntry to return from ReadDir") + .path(); + // load each BranchRef event in .ngit and call update + refs.update( + load_event(path) + .expect("every file in .ngit paths is a valid json event") + ); + } + } + else { + panic!("expected dir to exist in branch_refs"); + } + } + refs.updates(branch_events); + refs + } + + pub fn updates (&mut self, branch_events: Vec) { + for event in branch_events.clone().into_iter() { + self.update(event); + } + let mut repo_config = RepoConfig::open(&self.repo_dir_path); + repo_config.set_last_branch_ref_update_time(self.most_recent_timestamp.clone()); + } + + pub fn update (&mut self, event: Event) { + let event_to_store = event.clone(); + // /// check event is for repo + // fn event_is_for_repo(event: &Event,branch_refs: &BranchRefs) -> bool { + // match event.tags.iter().find(|tag| tag_is_repo(tag)) { + // None => false, + // Some(tag) => { + // match branch_refs.branches.get(0) { + // None => true, // current repo unknown + // Some(b) => tag_extract_value(tag) == b.id.to_string(), + // } + // }, + // } + // } + + // update most_recent_timestamp + if event.created_at > self.most_recent_timestamp { + self.most_recent_timestamp = event.created_at.clone(); + } + + // add events to vectors + let dir_name = match Kind::from(event.clone().kind.as_u64()) { + Kind::InitializeRepo => { + // if !self.branches.iter().any(|e| e.id == event.id) + // && event_is_for_repo(&event, &self) { + if !self.branches.iter().any(|e| e.id == event.id) { + self.branches.push(event); + Some("branches") + } + else { None } + }, + Kind::InitializeBranch => { + // if !self.branches.iter().any(|e| e.id == event.id) + // && event_is_for_repo(&event, &self) { + if !self.branches.iter().any(|e| e.id == event.id) { + self.branches.push(event); + Some("branches") + } + else { None } + }, + Kind::PullRequest => { + // if !self.pull_requests.iter().any(|e| e.id == event.id) + // && event_is_for_repo(&event, &self) { + if !self.pull_requests.iter().any(|e| e.id == event.id) { + self.pull_requests.push(event); + Some("prs") + } + else { None } + } + Kind::Merge => { + // if !self.merges.iter().any(|e| e.id == event.id) + // && event_is_for_repo(&event, &self) { + if !self.merges.iter().any(|e| e.id == event.id) { + self.merges.push(event); + Some("merges") + } + else { None } + }, + Kind::InitializeGroup => { + if !self.groups.iter().any(|e| e.id == event.id) { + self.groups.push(event); + Some("groups") + } + else { None } + }, + _ => None, + }; + + // store events in .ngit directory + match dir_name { + Some(dir_name) => { + let path = self.repo_dir_path.join(".ngit").join(format!("{}/{}.json",dir_name, event_to_store.id)); + if !path.exists() { + save_event(&path, &event_to_store) + .expect(format!("save_event will store BranchRefs event in {}",&path.to_string_lossy()).as_str()); + } + }, + None => (), + } + } + + fn branch_event(&self, branch_id: Option<&String>) -> Event { + match branch_id { + None => self.branches[0].clone(), + Some(branch_id) => self.branches.iter().find(|b| b.id.to_string() == *branch_id) + .expect("BranchRefs.branch_event() will always be called with a branch_id from a branch in its cache") + .clone(), + } + } + + pub fn branch_as_repo(&self, branch_id: Option<&String>) -> Repo { + Repo::new_from_event(self.branch_event(branch_id)) + .expect("event in BranchRefs.branches to produce Repo") + } + + /// assumes the branch_id is in cachse + pub fn maintainers_group_id(&self, branch_id: Option<&String>) -> EventId { + self.branch_as_repo(branch_id) + .maintainers_group.get_first_active_group() + .expect("a repo will always have an active maintainers group") + .clone() + } + + /// assumes the branch_id is in cache. returns None if maintainers group event cannot be found. + pub fn maintainers_group(&self, branch_or_group_id: Option<&String>) -> Option { + match self.groups.iter().find(|g| + // for branch id + g.id == self.maintainers_group_id(branch_or_group_id) + // for group id + || match branch_or_group_id { + None => false, + Some(id) => g.id == EventId::from_str(id).expect("id to be valid event id"), + }, + ) { + None => None, + Some(event) => Some( + Group::new_from_event(event.clone()) + .expect("group stored in BranchRefs.groups will always produce Group") + ), + } + } + + /// returns None if maintainers group event cannot be found + pub fn is_authorized(&self, branch_id: Option<&String>, pubkey: &XOnlyPublicKey) -> Option { + match self.maintainers_group(branch_id) { + None => None, + Some(group) => Some( + group.is_member(pubkey) + // TODO - add support for nested groups so 'is_member' checks for indirect membership + // for it will just be members of the branch group or maintainers group + || + match self.maintainers_group(None) { + None => false, + Some(group) => group.is_member(pubkey), + } + ), + } + } + + pub fn group_ids_for_branches_without_cached_groups(&self) -> Vec { + self.branches.iter() + .map(|b| + self.maintainers_group_id(Some(&b.id.to_string())) + .clone() + ) + .filter(|id|!self.groups.iter().any(|e|e.id == *id)) + .collect() + + } +} + +pub fn get_branch_refs (repo: &Repo, client: &Client, repo_dir_path: &PathBuf) -> BranchRefs { + + let mut refs = BranchRefs::new(vec![],repo_dir_path.clone()); + + let repo_config = RepoConfig::open(repo_dir_path); + + // filter for branches, PRs and Merges + let mut tag_filter = Filter::new() + .event(repo.id) + .kinds(vec![ + Kind::InitializeBranch.into_sdk_custom_kind(), + Kind::PullRequest.into_sdk_custom_kind(), + Kind::Merge.into_sdk_custom_kind(), + ]); + match repo_config.last_branch_ref_update_time() { + None => (), + Some(timestamp) => { + tag_filter = tag_filter.since(timestamp.clone()); + } + }; + + let branch_events: Vec = client.get_events_of( + vec![ + // branch maintainer groups + Filter::new().ids(refs.group_ids_for_branches_without_cached_groups()), + tag_filter, + ], + None, + ) + .expect("get_events_of to not return an error"); + + refs.updates(branch_events); + // refs.merged_branches_ids.push(repo.id.to_string()); + + // for event in refs.merges.iter() { + // match &event.tags.iter().find(|t|tag_is_branch(t)) { + // None => {}, + // Some(t) => { + // match &refs.maintainers_group { + // None => (), + // Some(g) => { + // if g.is_member(&event.pubkey) { + // refs.merged_branches_ids.push(tag_extract_value(t)); + // } + // } + // } + // } + // } + // } + refs +} diff --git a/src/cli_helpers.rs b/src/cli_helpers.rs new file mode 100644 index 0000000..91f6dde --- /dev/null +++ b/src/cli_helpers.rs @@ -0,0 +1,160 @@ +use confy::ConfyError; +use dialoguer::{theme::ColorfulTheme, Input, MultiSelect, Confirm}; +use nostr::{EventId, prelude::{Nip19Event, FromBech32}}; + +use crate::config::{MyConfig, save_conifg}; +/// Renders a dialoguer multi select prompt with a free-form option +pub fn multi_select_with_add( + proposed:Vec, + selected:Vec, + prompt: &str, + add_prompt: &str, +) -> Vec { + // add option with add_prompt + let mut options:Vec = proposed.clone(); + options.push(add_prompt.to_string()); + let mut options_selected = selected.clone(); + options_selected.push(false); + // present options + let chosen : Vec = MultiSelect::new() + .with_prompt(prompt) + .items(&options) + .defaults(&options_selected) + .report(false) + .interact() + .unwrap(); + // reduce options list + let mut new_proposed: Vec = [].to_vec(); + for (i, _el) in proposed.iter().enumerate() { + if chosen.contains(&i) { + new_proposed.push(proposed[i].clone()) + } + } + let mut new_selected: Vec = vec![true;new_proposed.len()]; + // if add_prompt selected + let last = chosen.last(); + if last == None || *last.unwrap() == options.len() - 1 { + // get user to input new item + let new_relay: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(add_prompt) + .report(false) + .interact_text() + .unwrap(); + // prepare new proposed options + // if new item is not blank add it as a selected option + if new_relay.len() > 0 { + new_proposed.push(new_relay); + new_selected.push(true); + } + // re run selection + return multi_select_with_add( + new_proposed, + new_selected, + prompt, + add_prompt, + ) + } + else { + let mut items: Vec = [].to_vec(); + for i in chosen { + items.push(options[i].clone()); + } + println!("{}: {:?}",prompt,items); + return items; + } +} + +pub fn select_relays(cfg:&mut MyConfig, selected_defaults:&Vec) -> Result,ConfyError> { + // set default relays (selected by default) + let default_relays = + if selected_defaults.is_empty() { cfg.default_relays.clone() } + else { selected_defaults.clone() }; + // set full proposed list + let mut proposed_relays = default_relays.clone(); + // add config defaults to proposed unless duplicate + for s in &cfg.default_relays { + if !(proposed_relays.iter().any(|df| s.eq(df))) { + proposed_relays.push(s.clone()); + } + } + // add example options to proposed list unless duplicate + for s in vec![ + String::from("wss://relay.damus.io"), + String::from("wss://nostr.wine"), + String::from("wss://nos.lol"), + ] { + if !(proposed_relays.iter().any(|df| s.eq(df))) { + proposed_relays.push(s.clone()); + } + } + // select only cli attribute relays or thie first in the proposed list + // this does the same thing but which is more idiumatic? + // let mut selected: Vec = vec![]; + // for i in 0..proposed_relays.len() { + // selected.push(i < relays.len()); + // } + let selected: Vec = proposed_relays + .iter() + .enumerate() + .map(|r| r.0 < default_relays.len() ).collect(); + + // get user relay selection + let relay_selection: Vec = crate::cli_helpers::multi_select_with_add( + proposed_relays, + selected, + "Relays", + "Other Relay" + ); + + // offer to save as default config + if relay_selection.ne(&cfg.default_relays) { + if Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Save relays as ngit default?") + .default(true) + .interact() + .unwrap() { + cfg.default_relays = relay_selection.clone(); + save_conifg(&cfg); + } + } + Ok(relay_selection) +} + +pub fn valid_event_id_from_input( + proposed_event_id: Option, + prompt:&String, +) -> EventId { + let mut string_param = proposed_event_id.clone(); + loop { + string_param = match string_param { + None => { + let response: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt.clone()) + .report(true) + .interact_text() + .unwrap(); + Some(response) + } + Some(ref s) => { Some(s.clone()) }, + }; + + let _valid_id = match Nip19Event::from_bech32(&string_param.clone().unwrap()) { + Ok(n19) => { break n19.event_id } + Err(_) => { + match EventId::from_bech32(&string_param.clone().unwrap()) { + Ok(id) => { break id } + Err(_) => { + match EventId::from_hex(&string_param.clone().unwrap()) { + Ok(id) => { break id } + Err(_) => { + println!("not a valid nevent, note or hex string. try again."); + string_param = None; + continue; + } + } + } + } + } + }; + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..f3b7f7b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,32 @@ +use nostr::{secp256k1::SecretKey}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct MyConfig { + version: u8, + pub default_admin_group_event_serialized: Option, + pub default_relays:Vec, + pub private_key:Option, +} + +/// `MyConfig` implements `Default` +impl ::std::default::Default for MyConfig { + fn default() -> Self { Self { + version: 0, + default_admin_group_event_serialized: None, + default_relays:vec![], + private_key: None, + } } +} + +pub fn load_config() -> MyConfig { + confy::load("ngit-cli", None) + .expect("load_config always to load confy custom config or defaults for ngit-cli") +} + +pub fn save_conifg(cfg:&MyConfig) -> &MyConfig { + confy::store("ngit-cli",None, &cfg) + .expect("save_conifg always to save confy custom config or defaults for ngit-cli and return it"); + cfg +} + diff --git a/src/kind.rs b/src/kind.rs new file mode 100644 index 0000000..90771b5 --- /dev/null +++ b/src/kind.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Distributed under the MIT software license + +//! Kind + +use std::fmt; +use std::num::ParseIntError; +use std::str::FromStr; + +use serde::de::{Deserialize, Deserializer, Error, Visitor}; +use serde::ser::{Serialize, Serializer}; + +/// Event [`Kind`] +#[derive(Debug, Copy, Clone, Eq, Ord, PartialOrd)] +pub enum Kind { + /// Initialize Group + InitializeGroup, + /// Update Group + UpdateGroup, + /// Initialize Repository + InitializeRepo, + /// Update Repository + UpdateRepo, + /// Initialize Branch + InitializeBranch, + /// Update Branch + UpdateBranch, + /// Patch + Patch, + /// Pull Request + PullRequest, + /// Merge + Merge, + /// Custom + Custom(u64), +} + +impl Kind { + /// Get [`Kind`] as `u32` + pub fn as_u32(&self) -> u32 { + self.as_u64() as u32 + } + + /// Get [`Kind`] as `u64` + pub fn as_u64(&self) -> u64 { + (*self).into() + } + + /// Convert to nostr::event::Kind::Custom() + pub fn into_sdk_custom_kind(&self) -> nostr::event::Kind { + nostr::event::Kind::Custom((*self).into()) + } +} + +impl From for Kind { + fn from(u: u64) -> Self { + match u { + 40000 => Self::InitializeGroup, + 40001 => Self::UpdateGroup, + 40010 => Self::InitializeRepo, + 40011 => Self::UpdateRepo, + 40020 => Self::InitializeBranch, + 40021 => Self::UpdateBranch, + 410 => Self::Patch, + 1 => Self::PullRequest, + 421 => Self::Merge, + x => Self::Custom(x), + + } + } +} + +impl From for u64 { + fn from(e: Kind) -> u64 { + match e { + Kind::InitializeGroup => 40000, + Kind::UpdateGroup => 40001, + Kind::InitializeRepo => 40010, + Kind::UpdateRepo => 40011, + Kind::InitializeBranch => 40020, + Kind::UpdateBranch => 40021, + Kind::Patch => 410, + Kind::PullRequest => 1, + Kind::Merge => 421, + Kind::Custom(u) => u, + } + } +} + +impl FromStr for Kind { + type Err = ParseIntError; + fn from_str(kind: &str) -> Result { + let kind: u64 = kind.parse()?; + Ok(Self::from(kind)) + } +} + +impl PartialEq for Kind { + fn eq(&self, other: &Kind) -> bool { + self.as_u64() == other.as_u64() + } +} + +impl Serialize for Kind { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u64(From::from(*self)) + } +} + +impl<'de> Deserialize<'de> for Kind { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_u64(KindVisitor) + } +} + +struct KindVisitor; + +impl Visitor<'_> for KindVisitor { + type Value = Kind; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "an unsigned number") + } + + fn visit_u64(self, v: u64) -> Result + where + E: Error, + { + Ok(From::::from(v)) + } +} diff --git a/src/ngit_tag.rs b/src/ngit_tag.rs new file mode 100644 index 0000000..9f02da3 --- /dev/null +++ b/src/ngit_tag.rs @@ -0,0 +1,180 @@ +use core::fmt; +use std::str::FromStr; + +use nostr::{Tag, prelude::{self, UncheckedUrl}, EventId}; + +/// Tag kind +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum TagKind { + /// Group + Group, + /// Admin group + AdminGroup, + /// Repository + Repo, + /// Branch + Branch, + /// Branch merged from + BranchMergeFrom, + /// Patch + Patch, + /// Patch Parent + PatchParent, + /// Commit + Commit, + /// Commit Parent + CommitParent, + /// Commit Message + CommitMessage, + /// Initial Commit + InitialCommit, + /// Relays + Relays, + /// Custom tag kind + Custom(String), +} + +impl fmt::Display for TagKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Group => write!(f, "group"), + Self::AdminGroup => write!(f, "admin-group"), + Self::Repo => write!(f, "repo"), + Self::Branch => write!(f, "branch"), + Self::BranchMergeFrom => write!(f, "from-branch"), + Self::Patch => write!(f, "patch"), + Self::PatchParent => write!(f, "parent-patch"), + Self::Commit => write!(f, "commit"), + Self::CommitParent => write!(f, "parent-commit"), + Self::CommitMessage => write!(f, "commit-message"), + Self::InitialCommit => write!(f, "initial-commit"), + Self::Relays => write!(f, "relays"), + Self::Custom(tag) => write!(f, "{tag}"), + } + } +} + +impl From for TagKind +where + S: Into, +{ + fn from(s: S) -> Self { + let s: String = s.into(); + match s.as_str() { + "group" => Self::Group, + "admin-group" => Self::AdminGroup, + "repo" => Self::Repo, // single letter tags are searchable under NIP-12 + "branch" => Self::Branch, // single letter tags are searchable under NIP-12 + "from-branch" => Self::BranchMergeFrom, + "patch" => Self::Patch, + "parent-patch" => Self::PatchParent, + "commit" => Self::Commit, + "parent-commit" => Self::CommitParent, + "commit-message" => Self::CommitMessage, + "initial-commit" => Self::InitialCommit, + "relays" => Self::Relays, + tag => Self::Custom(tag.to_string()), + } + } +} + +fn tag(label:TagKind,value:&String) -> Tag { + Tag::Generic( + prelude::TagKind::Custom(label.to_string()), + vec![value.clone()], + ) +} +fn tag_multi_value(label:TagKind,value:&Vec) -> Tag { + Tag::Generic( + prelude::TagKind::Custom(label.to_string()), + value.clone(), + ) +} + +pub fn tag_group(event_id: &String) -> Tag { tag(TagKind::Group, event_id) } +pub fn tag_group_with_relays(group_id: &String, vec_relays: &Vec) -> Tag { + let mut combined = vec![group_id.clone()]; + for r in vec_relays { + combined.push(r.to_string()); + } + tag_multi_value( + TagKind::Group, + &combined, + ) +} +pub fn tag_admin_group(event_id: &String) -> Tag { tag(TagKind::AdminGroup, event_id) } +pub fn tag_admin_group_with_relays(group_id: &String, vec_relays: &Vec) -> Tag { + let mut combined = vec![group_id.clone()]; + for r in vec_relays { + combined.push(r.to_string()); + } + tag_multi_value( + TagKind::AdminGroup, + &combined, + ) +} +// takes a tag referencing an event with optional relays and turns it into an e tag which is fiterable. perfect for tag_repo, tag_branch, etc. +pub fn tag_into_event(tag:Tag) -> Tag { + Tag::Event( + tag_extract_value_as_event_id(&tag), + tag_extract_relays(&tag).get(0).cloned(), + None, + ) +} +pub fn tag_repo(event_id: &String) -> Tag { tag(TagKind::Repo, event_id) } +pub fn tag_branch(event_id: &String) -> Tag { tag(TagKind::Branch, event_id) } +pub fn tag_branch_merge_from(event_id: &String) -> Tag { tag(TagKind::BranchMergeFrom, event_id) } +pub fn tag_patch(event_id: &String) -> Tag { tag(TagKind::Patch, event_id) } +pub fn tag_patch_parent(event_id: &String) -> Tag { tag(TagKind::PatchParent, event_id) } +pub fn tag_commit(commit_id: &String) -> Tag { tag(TagKind::Commit, commit_id) } +pub fn tag_commit_parent(commit_id: &String) -> Tag { tag(TagKind::CommitParent, commit_id) } +pub fn tag_commit_message(message: &String) -> Tag { tag(TagKind::CommitMessage, message) } +pub fn tag_initial_commit() -> Tag { Tag::Hashtag(TagKind::InitialCommit.to_string()) } +pub fn tag_relays(relays:&Vec) -> Tag { + let mut relays_unchecked_url = vec![]; + for r in relays { + relays_unchecked_url.push( + UncheckedUrl::from_str(r) + .expect("relay in string to not produce error on uncheckedUrl"), + ) + } + Tag::Relays(relays_unchecked_url) +} +pub fn tag_hashtag(hashtag:&str) -> Tag { Tag::Hashtag(hashtag.to_string()) } +pub fn tag_extract_value(tag:&Tag) -> String { tag.as_vec()[1].clone() } +pub fn tag_extract_value_as_event_id(tag:&Tag) -> EventId { + EventId::from_str(tag.as_vec()[1].clone().as_str()) + .expect("first tag value is a event id") +} +pub fn tag_extract_relays(tag:&Tag) -> Vec { + let mut relays = vec![]; + let tag_vec = tag.as_vec(); + for (i, s) in tag_vec.iter().enumerate() { + if i > 1 || ( + i == 1 && tag_vec[0] == TagKind::Relays.to_string() + ) + { + relays.push( + UncheckedUrl::from_str(s) + .expect("relay strings to not produce error on uncheckedUrl"), + ); + } + } + relays +} + +pub fn tag_is_group(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Group.to_string() } +pub fn tag_is_admin_group(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::AdminGroup.to_string() } +pub fn tag_is_repo(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Repo.to_string() } +pub fn tag_is_branch(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Branch.to_string() } +pub fn tag_is_branch_merged_from(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::BranchMergeFrom.to_string() } +pub fn tag_is_patch(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Patch.to_string() } +pub fn tag_is_patch_parent(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::PatchParent.to_string() } +pub fn tag_is_commit(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Commit.to_string() } +pub fn tag_is_commit_parent(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::CommitParent.to_string() } +pub fn tag_is_commit_message(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::CommitMessage.to_string() } +pub fn tag_is_initial_commit(tag:&Tag) -> bool { + tag.kind().to_string() == prelude::TagKind::T.to_string() + && tag.as_vec()[1] == TagKind::InitialCommit.to_string() +} +pub fn tag_is_relays(tag:Tag) -> bool { tag.kind().to_string() == TagKind::Relays.to_string() } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..48aafa9 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,212 @@ +use std::fs::File; +use std::io::{Read, Write}; +use std::path::{Path}; +use std::time::Duration; + +use dialoguer::{Select, Input}; +use dialoguer::theme::ColorfulTheme; +use nostr_sdk::blocking::Client; +use nostr_sdk::prelude::*; + +use crate::config::{MyConfig, save_conifg}; + +pub fn handle_keys(private_key: Option, hex: bool) -> Result { + // Parse and validate private key + let keys = match private_key { + Some(pk) => { + // create a new identity using the provided private key + Keys::from_sk_str(pk.as_str())? + } + None => { + // create a new identity with a new keypair + println!("No private key provided, creating new identity"); + Keys::generate() + } + }; + + if !hex { + println!("Private key: {}", keys.secret_key()?.to_bech32()?); + println!("Public key: {}", keys.public_key().to_bech32()?); + } else { + println!("Private key: {}", keys.secret_key()?.display_secret()); + println!("Public key: {}", keys.public_key()); + } + Ok(keys) +} + +// Creates the websocket client that is used for communicating with relays +pub fn create_client(keys: &Keys, relays: Vec) -> Result { + let opts = Options::new() + .wait_for_send(true) + .timeout(Some(Duration::from_secs(7))); + let client = Client::with_opts(keys, opts); + let relays = relays.iter().map(|url| (url, None)).collect(); + client.add_relays(relays)?; + client.connect(); + Ok(client) +} + +// Accepts both hex and bech32 keys and returns the hex encoded key +pub fn parse_key(key: String) -> Result { + // Check if the key is a bech32 encoded key + let parsed_key = if key.starts_with("npub") { + XOnlyPublicKey::from_bech32(key)?.to_string() + } else if key.starts_with("nsec") { + SecretKey::from_bech32(key)?.display_secret().to_string() + } else if key.starts_with("note") { + EventId::from_bech32(key)?.to_hex() + } else if key.starts_with("nchannel") { + ChannelId::from_bech32(key)?.to_hex() + } else { + // If the key is not bech32 encoded, return it as is + key + }; + Ok(parsed_key) +} + +pub fn get_stored_keys(cfg:&mut MyConfig) -> Option { + match &cfg.private_key { + None => None, + Some(k) => Some(Keys::new(*k)), + } +} + +pub fn get_or_generate_keys(cfg:&mut MyConfig) -> Keys { + match cfg.private_key { + None => { + let selection = Select::with_theme(&ColorfulTheme::default()) + .items(&vec!["enter existing private key", "generate new keys"]) + .default(0) + .with_prompt("no keys are stored") + .interact().unwrap(); + let key = match selection { + 0 => { + let mut prompt = "secret key (nsec, hex, etc)"; + loop { + let pk: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt(prompt) + .interact_text() + .unwrap(); + match Keys::from_sk_str(&pk) { + Ok(key) => { break key; }, + Err(_e) => { prompt = "error interpeting secret key. try again with nsec, hex, etc"; }, + } + } + + } + _ => Keys::generate(), + }; + cfg.private_key = Some(key.secret_key().unwrap()); + save_conifg(&cfg); + key + } + Some(k) => Keys::new(k), + } +} + +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum Prefix { + Npub, + Nsec, + Note, + Nchannel, +} + + +/// [`LoadFile`] error +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Error loading event file + #[error("cannot load event file.")] + // LoadFile(#[from] init::Error), + LoadFile(), +} + +pub fn load_file>(path: P) -> Result { + let mut buf = vec![]; + match File::open(path) { + Ok(mut f) => { + f.read_to_end(&mut buf) + .expect("read_to_end not to error on file"); + Ok( + std::str::from_utf8(&buf[..]) + .expect("file contents u8 to convert to str") + .to_string(), + ) + }, + Err(_e) => { Err(Error::LoadFile()) }, + } + +} + +pub fn load_event>(path: P) -> Result { + if let Ok(mut file) = File::open(path) { + let mut buf = vec![]; + if file.read_to_end(&mut buf).is_ok() { + if let Ok(event) = Event::from_json(std::str::from_utf8(&buf[..]).unwrap()) { + return Ok(event) + } + } + } + Err(Error::LoadFile()) +} + +pub fn save_event>(path: P, event: &Event) -> Result<()> { + let mut f = File::create(path)?; + f.write_all(&event.as_json().as_bytes())?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_key_hex_input() { + let hex_key = + String::from("f4deaad98b61fa24d86ef315f1d5d57c1a6a533e1e87e777e5d0b48dcd332cdb"); + let result = parse_key(hex_key.clone()); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), hex_key); + } + + #[test] + fn test_parse_key_bech32_note_input() { + let bech32_note_id = + String::from("note1h445ule4je70k7kvddate8kpsh2fd6n77esevww5hmgda2qwssjsw957wk"); + let result = parse_key(bech32_note_id); + + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + String::from("bd6b4e7f35967cfb7acc6b7abc9ec185d496ea7ef6619639d4bed0dea80e8425") + ); + } + + #[test] + fn test_parse_bech32_public_key_input() { + let bech32_encoded_key = + String::from("npub1ktt8phjnkfmfrsxrgqpztdjuxk3x6psf80xyray0l3c7pyrln49qhkyhz0"); + let result = parse_key(bech32_encoded_key); + + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + String::from("b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a") + ); + } + + #[test] + fn test_parse_bech32_private_key() { + let bech32_encoded_key = + String::from("nsec1hdeqm0y8vgzuucqv4840h7rlpy4qfu928ulxh3dzj6s2nqupdtzqagtew3"); + let result = parse_key(bech32_encoded_key); + + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + String::from("bb720dbc876205ce600ca9eafbf87f092a04f0aa3f3e6bc5a296a0a983816ac4") + ); + } +} -- cgit v1.2.3