upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2023-05-21 11:18:29 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2023-05-21 11:18:29 +0000
commitfda0fdd81caab1ca92eb7ed601058e6c2fdc59f5 (patch)
treedd91fc1a7b41d02aead655ea2dc07463b3487d5d /src
parent0067804cc00e94ce2b7043e67f9ff50968525479 (diff)
helpers and utilities
Diffstat (limited to 'src')
-rw-r--r--src/branch_refs.rs274
-rw-r--r--src/cli_helpers.rs160
-rw-r--r--src/config.rs32
-rw-r--r--src/kind.rs137
-rw-r--r--src/ngit_tag.rs180
-rw-r--r--src/utils.rs212
6 files changed, 995 insertions, 0 deletions
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 @@
1use std::{path::PathBuf, fs, str::FromStr};
2
3use nostr::{Event, Filter, Timestamp, secp256k1::XOnlyPublicKey, EventId};
4use nostr_sdk::blocking::Client;
5
6use crate::{utils::{load_event, save_event}, kind::Kind, repos::repo::Repo, groups::group::Group, repo_config::RepoConfig};
7
8
9pub struct BranchRefs {
10 pub branches: Vec<Event>,
11 pub pull_requests: Vec<Event>,
12 pub merges: Vec<Event>,
13 pub groups: Vec<Event>,
14 repo_dir_path: PathBuf,
15 pub most_recent_timestamp: Timestamp,
16}
17
18impl BranchRefs {
19 pub fn new (branch_events: Vec<Event>, repo_dir_path: PathBuf) -> Self {
20 let mut refs = Self {
21 branches: vec![],
22 pull_requests: vec![],
23 merges: vec![],
24 groups: vec![],
25 repo_dir_path,
26 most_recent_timestamp: Timestamp::from(0),
27 };
28
29 // add repo first branch in branches vector
30 refs.update(
31 load_event(refs.repo_dir_path.join(".ngit/repo.json"))
32 .expect("repo.json to be present and load as event")
33 );
34
35 //load locally
36 for dir_name in [
37 "groups",
38 "branches",
39 "merges",
40 "prs",
41 ] {
42 let dir_path = refs.repo_dir_path.join(".ngit").join(&dir_name);
43 if dir_path.exists() {
44 let dir = fs::read_dir(&dir_path)
45 .expect("read_dir to produce ReadDir from a path that exists");
46 for entry in dir {
47 let path = entry
48 .expect("DirEntry to return from ReadDir")
49 .path();
50 // load each BranchRef event in .ngit and call update
51 refs.update(
52 load_event(path)
53 .expect("every file in .ngit paths is a valid json event")
54 );
55 }
56 }
57 else {
58 panic!("expected dir to exist in branch_refs");
59 }
60 }
61 refs.updates(branch_events);
62 refs
63 }
64
65 pub fn updates (&mut self, branch_events: Vec<Event>) {
66 for event in branch_events.clone().into_iter() {
67 self.update(event);
68 }
69 let mut repo_config = RepoConfig::open(&self.repo_dir_path);
70 repo_config.set_last_branch_ref_update_time(self.most_recent_timestamp.clone());
71 }
72
73 pub fn update (&mut self, event: Event) {
74 let event_to_store = event.clone();
75 // /// check event is for repo
76 // fn event_is_for_repo(event: &Event,branch_refs: &BranchRefs) -> bool {
77 // match event.tags.iter().find(|tag| tag_is_repo(tag)) {
78 // None => false,
79 // Some(tag) => {
80 // match branch_refs.branches.get(0) {
81 // None => true, // current repo unknown
82 // Some(b) => tag_extract_value(tag) == b.id.to_string(),
83 // }
84 // },
85 // }
86 // }
87
88 // update most_recent_timestamp
89 if event.created_at > self.most_recent_timestamp {
90 self.most_recent_timestamp = event.created_at.clone();
91 }
92
93 // add events to vectors
94 let dir_name = match Kind::from(event.clone().kind.as_u64()) {
95 Kind::InitializeRepo => {
96 // if !self.branches.iter().any(|e| e.id == event.id)
97 // && event_is_for_repo(&event, &self) {
98 if !self.branches.iter().any(|e| e.id == event.id) {
99 self.branches.push(event);
100 Some("branches")
101 }
102 else { None }
103 },
104 Kind::InitializeBranch => {
105 // if !self.branches.iter().any(|e| e.id == event.id)
106 // && event_is_for_repo(&event, &self) {
107 if !self.branches.iter().any(|e| e.id == event.id) {
108 self.branches.push(event);
109 Some("branches")
110 }
111 else { None }
112 },
113 Kind::PullRequest => {
114 // if !self.pull_requests.iter().any(|e| e.id == event.id)
115 // && event_is_for_repo(&event, &self) {
116 if !self.pull_requests.iter().any(|e| e.id == event.id) {
117 self.pull_requests.push(event);
118 Some("prs")
119 }
120 else { None }
121 }
122 Kind::Merge => {
123 // if !self.merges.iter().any(|e| e.id == event.id)
124 // && event_is_for_repo(&event, &self) {
125 if !self.merges.iter().any(|e| e.id == event.id) {
126 self.merges.push(event);
127 Some("merges")
128 }
129 else { None }
130 },
131 Kind::InitializeGroup => {
132 if !self.groups.iter().any(|e| e.id == event.id) {
133 self.groups.push(event);
134 Some("groups")
135 }
136 else { None }
137 },
138 _ => None,
139 };
140
141 // store events in .ngit directory
142 match dir_name {
143 Some(dir_name) => {
144 let path = self.repo_dir_path.join(".ngit").join(format!("{}/{}.json",dir_name, event_to_store.id));
145 if !path.exists() {
146 save_event(&path, &event_to_store)
147 .expect(format!("save_event will store BranchRefs event in {}",&path.to_string_lossy()).as_str());
148 }
149 },
150 None => (),
151 }
152 }
153
154 fn branch_event(&self, branch_id: Option<&String>) -> Event {
155 match branch_id {
156 None => self.branches[0].clone(),
157 Some(branch_id) => self.branches.iter().find(|b| b.id.to_string() == *branch_id)
158 .expect("BranchRefs.branch_event() will always be called with a branch_id from a branch in its cache")
159 .clone(),
160 }
161 }
162
163 pub fn branch_as_repo(&self, branch_id: Option<&String>) -> Repo {
164 Repo::new_from_event(self.branch_event(branch_id))
165 .expect("event in BranchRefs.branches to produce Repo")
166 }
167
168 /// assumes the branch_id is in cachse
169 pub fn maintainers_group_id(&self, branch_id: Option<&String>) -> EventId {
170 self.branch_as_repo(branch_id)
171 .maintainers_group.get_first_active_group()
172 .expect("a repo will always have an active maintainers group")
173 .clone()
174 }
175
176 /// assumes the branch_id is in cache. returns None if maintainers group event cannot be found.
177 pub fn maintainers_group(&self, branch_or_group_id: Option<&String>) -> Option<Group> {
178 match self.groups.iter().find(|g|
179 // for branch id
180 g.id == self.maintainers_group_id(branch_or_group_id)
181 // for group id
182 || match branch_or_group_id {
183 None => false,
184 Some(id) => g.id == EventId::from_str(id).expect("id to be valid event id"),
185 },
186 ) {
187 None => None,
188 Some(event) => Some(
189 Group::new_from_event(event.clone())
190 .expect("group stored in BranchRefs.groups will always produce Group")
191 ),
192 }
193 }
194
195 /// returns None if maintainers group event cannot be found
196 pub fn is_authorized(&self, branch_id: Option<&String>, pubkey: &XOnlyPublicKey) -> Option<bool> {
197 match self.maintainers_group(branch_id) {
198 None => None,
199 Some(group) => Some(
200 group.is_member(pubkey)
201 // TODO - add support for nested groups so 'is_member' checks for indirect membership
202 // for it will just be members of the branch group or maintainers group
203 ||
204 match self.maintainers_group(None) {
205 None => false,
206 Some(group) => group.is_member(pubkey),
207 }
208 ),
209 }
210 }
211
212 pub fn group_ids_for_branches_without_cached_groups(&self) -> Vec<EventId> {
213 self.branches.iter()
214 .map(|b|
215 self.maintainers_group_id(Some(&b.id.to_string()))
216 .clone()
217 )
218 .filter(|id|!self.groups.iter().any(|e|e.id == *id))
219 .collect()
220
221 }
222}
223
224pub fn get_branch_refs (repo: &Repo, client: &Client, repo_dir_path: &PathBuf) -> BranchRefs {
225
226 let mut refs = BranchRefs::new(vec![],repo_dir_path.clone());
227
228 let repo_config = RepoConfig::open(repo_dir_path);
229
230 // filter for branches, PRs and Merges
231 let mut tag_filter = Filter::new()
232 .event(repo.id)
233 .kinds(vec![
234 Kind::InitializeBranch.into_sdk_custom_kind(),
235 Kind::PullRequest.into_sdk_custom_kind(),
236 Kind::Merge.into_sdk_custom_kind(),
237 ]);
238 match repo_config.last_branch_ref_update_time() {
239 None => (),
240 Some(timestamp) => {
241 tag_filter = tag_filter.since(timestamp.clone());
242 }
243 };
244
245 let branch_events: Vec<Event> = client.get_events_of(
246 vec![
247 // branch maintainer groups
248 Filter::new().ids(refs.group_ids_for_branches_without_cached_groups()),
249 tag_filter,
250 ],
251 None,
252 )
253 .expect("get_events_of to not return an error");
254
255 refs.updates(branch_events);
256 // refs.merged_branches_ids.push(repo.id.to_string());
257
258 // for event in refs.merges.iter() {
259 // match &event.tags.iter().find(|t|tag_is_branch(t)) {
260 // None => {},
261 // Some(t) => {
262 // match &refs.maintainers_group {
263 // None => (),
264 // Some(g) => {
265 // if g.is_member(&event.pubkey) {
266 // refs.merged_branches_ids.push(tag_extract_value(t));
267 // }
268 // }
269 // }
270 // }
271 // }
272 // }
273 refs
274}
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 @@
1use confy::ConfyError;
2use dialoguer::{theme::ColorfulTheme, Input, MultiSelect, Confirm};
3use nostr::{EventId, prelude::{Nip19Event, FromBech32}};
4
5use crate::config::{MyConfig, save_conifg};
6/// Renders a dialoguer multi select prompt with a free-form option
7pub fn multi_select_with_add(
8 proposed:Vec<String>,
9 selected:Vec<bool>,
10 prompt: &str,
11 add_prompt: &str,
12) -> Vec<String> {
13 // add option with add_prompt
14 let mut options:Vec<String> = proposed.clone();
15 options.push(add_prompt.to_string());
16 let mut options_selected = selected.clone();
17 options_selected.push(false);
18 // present options
19 let chosen : Vec<usize> = MultiSelect::new()
20 .with_prompt(prompt)
21 .items(&options)
22 .defaults(&options_selected)
23 .report(false)
24 .interact()
25 .unwrap();
26 // reduce options list
27 let mut new_proposed: Vec<String> = [].to_vec();
28 for (i, _el) in proposed.iter().enumerate() {
29 if chosen.contains(&i) {
30 new_proposed.push(proposed[i].clone())
31 }
32 }
33 let mut new_selected: Vec<bool> = vec![true;new_proposed.len()];
34 // if add_prompt selected
35 let last = chosen.last();
36 if last == None || *last.unwrap() == options.len() - 1 {
37 // get user to input new item
38 let new_relay: String = Input::with_theme(&ColorfulTheme::default())
39 .with_prompt(add_prompt)
40 .report(false)
41 .interact_text()
42 .unwrap();
43 // prepare new proposed options
44 // if new item is not blank add it as a selected option
45 if new_relay.len() > 0 {
46 new_proposed.push(new_relay);
47 new_selected.push(true);
48 }
49 // re run selection
50 return multi_select_with_add(
51 new_proposed,
52 new_selected,
53 prompt,
54 add_prompt,
55 )
56 }
57 else {
58 let mut items: Vec<String> = [].to_vec();
59 for i in chosen {
60 items.push(options[i].clone());
61 }
62 println!("{}: {:?}",prompt,items);
63 return items;
64 }
65}
66
67pub fn select_relays(cfg:&mut MyConfig, selected_defaults:&Vec<String>) -> Result<Vec<String>,ConfyError> {
68 // set default relays (selected by default)
69 let default_relays =
70 if selected_defaults.is_empty() { cfg.default_relays.clone() }
71 else { selected_defaults.clone() };
72 // set full proposed list
73 let mut proposed_relays = default_relays.clone();
74 // add config defaults to proposed unless duplicate
75 for s in &cfg.default_relays {
76 if !(proposed_relays.iter().any(|df| s.eq(df))) {
77 proposed_relays.push(s.clone());
78 }
79 }
80 // add example options to proposed list unless duplicate
81 for s in vec![
82 String::from("wss://relay.damus.io"),
83 String::from("wss://nostr.wine"),
84 String::from("wss://nos.lol"),
85 ] {
86 if !(proposed_relays.iter().any(|df| s.eq(df))) {
87 proposed_relays.push(s.clone());
88 }
89 }
90 // select only cli attribute relays or thie first in the proposed list
91 // this does the same thing but which is more idiumatic?
92 // let mut selected: Vec<bool> = vec![];
93 // for i in 0..proposed_relays.len() {
94 // selected.push(i < relays.len());
95 // }
96 let selected: Vec<bool> = proposed_relays
97 .iter()
98 .enumerate()
99 .map(|r| r.0 < default_relays.len() ).collect();
100
101 // get user relay selection
102 let relay_selection: Vec<String> = crate::cli_helpers::multi_select_with_add(
103 proposed_relays,
104 selected,
105 "Relays",
106 "Other Relay"
107 );
108
109 // offer to save as default config
110 if relay_selection.ne(&cfg.default_relays) {
111 if Confirm::with_theme(&ColorfulTheme::default())
112 .with_prompt("Save relays as ngit default?")
113 .default(true)
114 .interact()
115 .unwrap() {
116 cfg.default_relays = relay_selection.clone();
117 save_conifg(&cfg);
118 }
119 }
120 Ok(relay_selection)
121}
122
123pub fn valid_event_id_from_input(
124 proposed_event_id: Option<String>,
125 prompt:&String,
126) -> EventId {
127 let mut string_param = proposed_event_id.clone();
128 loop {
129 string_param = match string_param {
130 None => {
131 let response: String = Input::with_theme(&ColorfulTheme::default())
132 .with_prompt(prompt.clone())
133 .report(true)
134 .interact_text()
135 .unwrap();
136 Some(response)
137 }
138 Some(ref s) => { Some(s.clone()) },
139 };
140
141 let _valid_id = match Nip19Event::from_bech32(&string_param.clone().unwrap()) {
142 Ok(n19) => { break n19.event_id }
143 Err(_) => {
144 match EventId::from_bech32(&string_param.clone().unwrap()) {
145 Ok(id) => { break id }
146 Err(_) => {
147 match EventId::from_hex(&string_param.clone().unwrap()) {
148 Ok(id) => { break id }
149 Err(_) => {
150 println!("not a valid nevent, note or hex string. try again.");
151 string_param = None;
152 continue;
153 }
154 }
155 }
156 }
157 }
158 };
159 }
160} \ 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 @@
1use nostr::{secp256k1::SecretKey};
2use serde::{Deserialize, Serialize};
3
4#[derive(Serialize, Deserialize)]
5pub struct MyConfig {
6 version: u8,
7 pub default_admin_group_event_serialized: Option<String>,
8 pub default_relays:Vec<String>,
9 pub private_key:Option<SecretKey>,
10}
11
12/// `MyConfig` implements `Default`
13impl ::std::default::Default for MyConfig {
14 fn default() -> Self { Self {
15 version: 0,
16 default_admin_group_event_serialized: None,
17 default_relays:vec![],
18 private_key: None,
19 } }
20}
21
22pub fn load_config() -> MyConfig {
23 confy::load("ngit-cli", None)
24 .expect("load_config always to load confy custom config or defaults for ngit-cli")
25}
26
27pub fn save_conifg(cfg:&MyConfig) -> &MyConfig {
28 confy::store("ngit-cli",None, &cfg)
29 .expect("save_conifg always to save confy custom config or defaults for ngit-cli and return it");
30 cfg
31}
32
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 @@
1// Copyright (c) 2022-2023 Yuki Kishimoto
2// Distributed under the MIT software license
3
4//! Kind
5
6use std::fmt;
7use std::num::ParseIntError;
8use std::str::FromStr;
9
10use serde::de::{Deserialize, Deserializer, Error, Visitor};
11use serde::ser::{Serialize, Serializer};
12
13/// Event [`Kind`]
14#[derive(Debug, Copy, Clone, Eq, Ord, PartialOrd)]
15pub enum Kind {
16 /// Initialize Group
17 InitializeGroup,
18 /// Update Group
19 UpdateGroup,
20 /// Initialize Repository
21 InitializeRepo,
22 /// Update Repository
23 UpdateRepo,
24 /// Initialize Branch
25 InitializeBranch,
26 /// Update Branch
27 UpdateBranch,
28 /// Patch
29 Patch,
30 /// Pull Request
31 PullRequest,
32 /// Merge
33 Merge,
34 /// Custom
35 Custom(u64),
36}
37
38impl Kind {
39 /// Get [`Kind`] as `u32`
40 pub fn as_u32(&self) -> u32 {
41 self.as_u64() as u32
42 }
43
44 /// Get [`Kind`] as `u64`
45 pub fn as_u64(&self) -> u64 {
46 (*self).into()
47 }
48
49 /// Convert to nostr::event::Kind::Custom()
50 pub fn into_sdk_custom_kind(&self) -> nostr::event::Kind {
51 nostr::event::Kind::Custom((*self).into())
52 }
53}
54
55impl From<u64> for Kind {
56 fn from(u: u64) -> Self {
57 match u {
58 40000 => Self::InitializeGroup,
59 40001 => Self::UpdateGroup,
60 40010 => Self::InitializeRepo,
61 40011 => Self::UpdateRepo,
62 40020 => Self::InitializeBranch,
63 40021 => Self::UpdateBranch,
64 410 => Self::Patch,
65 1 => Self::PullRequest,
66 421 => Self::Merge,
67 x => Self::Custom(x),
68
69 }
70 }
71}
72
73impl From<Kind> for u64 {
74 fn from(e: Kind) -> u64 {
75 match e {
76 Kind::InitializeGroup => 40000,
77 Kind::UpdateGroup => 40001,
78 Kind::InitializeRepo => 40010,
79 Kind::UpdateRepo => 40011,
80 Kind::InitializeBranch => 40020,
81 Kind::UpdateBranch => 40021,
82 Kind::Patch => 410,
83 Kind::PullRequest => 1,
84 Kind::Merge => 421,
85 Kind::Custom(u) => u,
86 }
87 }
88}
89
90impl FromStr for Kind {
91 type Err = ParseIntError;
92 fn from_str(kind: &str) -> Result<Self, Self::Err> {
93 let kind: u64 = kind.parse()?;
94 Ok(Self::from(kind))
95 }
96}
97
98impl PartialEq<Kind> for Kind {
99 fn eq(&self, other: &Kind) -> bool {
100 self.as_u64() == other.as_u64()
101 }
102}
103
104impl Serialize for Kind {
105 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
106 where
107 S: Serializer,
108 {
109 serializer.serialize_u64(From::from(*self))
110 }
111}
112
113impl<'de> Deserialize<'de> for Kind {
114 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
115 where
116 D: Deserializer<'de>,
117 {
118 deserializer.deserialize_u64(KindVisitor)
119 }
120}
121
122struct KindVisitor;
123
124impl Visitor<'_> for KindVisitor {
125 type Value = Kind;
126
127 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
128 write!(f, "an unsigned number")
129 }
130
131 fn visit_u64<E>(self, v: u64) -> Result<Kind, E>
132 where
133 E: Error,
134 {
135 Ok(From::<u64>::from(v))
136 }
137}
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 @@
1use core::fmt;
2use std::str::FromStr;
3
4use nostr::{Tag, prelude::{self, UncheckedUrl}, EventId};
5
6/// Tag kind
7#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
8pub enum TagKind {
9 /// Group
10 Group,
11 /// Admin group
12 AdminGroup,
13 /// Repository
14 Repo,
15 /// Branch
16 Branch,
17 /// Branch merged from
18 BranchMergeFrom,
19 /// Patch
20 Patch,
21 /// Patch Parent
22 PatchParent,
23 /// Commit
24 Commit,
25 /// Commit Parent
26 CommitParent,
27 /// Commit Message
28 CommitMessage,
29 /// Initial Commit
30 InitialCommit,
31 /// Relays
32 Relays,
33 /// Custom tag kind
34 Custom(String),
35}
36
37impl fmt::Display for TagKind {
38 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
39 match self {
40 Self::Group => write!(f, "group"),
41 Self::AdminGroup => write!(f, "admin-group"),
42 Self::Repo => write!(f, "repo"),
43 Self::Branch => write!(f, "branch"),
44 Self::BranchMergeFrom => write!(f, "from-branch"),
45 Self::Patch => write!(f, "patch"),
46 Self::PatchParent => write!(f, "parent-patch"),
47 Self::Commit => write!(f, "commit"),
48 Self::CommitParent => write!(f, "parent-commit"),
49 Self::CommitMessage => write!(f, "commit-message"),
50 Self::InitialCommit => write!(f, "initial-commit"),
51 Self::Relays => write!(f, "relays"),
52 Self::Custom(tag) => write!(f, "{tag}"),
53 }
54 }
55}
56
57impl<S> From<S> for TagKind
58where
59 S: Into<String>,
60{
61 fn from(s: S) -> Self {
62 let s: String = s.into();
63 match s.as_str() {
64 "group" => Self::Group,
65 "admin-group" => Self::AdminGroup,
66 "repo" => Self::Repo, // single letter tags are searchable under NIP-12
67 "branch" => Self::Branch, // single letter tags are searchable under NIP-12
68 "from-branch" => Self::BranchMergeFrom,
69 "patch" => Self::Patch,
70 "parent-patch" => Self::PatchParent,
71 "commit" => Self::Commit,
72 "parent-commit" => Self::CommitParent,
73 "commit-message" => Self::CommitMessage,
74 "initial-commit" => Self::InitialCommit,
75 "relays" => Self::Relays,
76 tag => Self::Custom(tag.to_string()),
77 }
78 }
79}
80
81fn tag(label:TagKind,value:&String) -> Tag {
82 Tag::Generic(
83 prelude::TagKind::Custom(label.to_string()),
84 vec![value.clone()],
85 )
86}
87fn tag_multi_value(label:TagKind,value:&Vec<String>) -> Tag {
88 Tag::Generic(
89 prelude::TagKind::Custom(label.to_string()),
90 value.clone(),
91 )
92}
93
94pub fn tag_group(event_id: &String) -> Tag { tag(TagKind::Group, event_id) }
95pub fn tag_group_with_relays(group_id: &String, vec_relays: &Vec<String>) -> Tag {
96 let mut combined = vec![group_id.clone()];
97 for r in vec_relays {
98 combined.push(r.to_string());
99 }
100 tag_multi_value(
101 TagKind::Group,
102 &combined,
103 )
104}
105pub fn tag_admin_group(event_id: &String) -> Tag { tag(TagKind::AdminGroup, event_id) }
106pub fn tag_admin_group_with_relays(group_id: &String, vec_relays: &Vec<UncheckedUrl>) -> Tag {
107 let mut combined = vec![group_id.clone()];
108 for r in vec_relays {
109 combined.push(r.to_string());
110 }
111 tag_multi_value(
112 TagKind::AdminGroup,
113 &combined,
114 )
115}
116// 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.
117pub fn tag_into_event(tag:Tag) -> Tag {
118 Tag::Event(
119 tag_extract_value_as_event_id(&tag),
120 tag_extract_relays(&tag).get(0).cloned(),
121 None,
122 )
123}
124pub fn tag_repo(event_id: &String) -> Tag { tag(TagKind::Repo, event_id) }
125pub fn tag_branch(event_id: &String) -> Tag { tag(TagKind::Branch, event_id) }
126pub fn tag_branch_merge_from(event_id: &String) -> Tag { tag(TagKind::BranchMergeFrom, event_id) }
127pub fn tag_patch(event_id: &String) -> Tag { tag(TagKind::Patch, event_id) }
128pub fn tag_patch_parent(event_id: &String) -> Tag { tag(TagKind::PatchParent, event_id) }
129pub fn tag_commit(commit_id: &String) -> Tag { tag(TagKind::Commit, commit_id) }
130pub fn tag_commit_parent(commit_id: &String) -> Tag { tag(TagKind::CommitParent, commit_id) }
131pub fn tag_commit_message(message: &String) -> Tag { tag(TagKind::CommitMessage, message) }
132pub fn tag_initial_commit() -> Tag { Tag::Hashtag(TagKind::InitialCommit.to_string()) }
133pub fn tag_relays(relays:&Vec<String>) -> Tag {
134 let mut relays_unchecked_url = vec![];
135 for r in relays {
136 relays_unchecked_url.push(
137 UncheckedUrl::from_str(r)
138 .expect("relay in string to not produce error on uncheckedUrl"),
139 )
140 }
141 Tag::Relays(relays_unchecked_url)
142}
143pub fn tag_hashtag(hashtag:&str) -> Tag { Tag::Hashtag(hashtag.to_string()) }
144pub fn tag_extract_value(tag:&Tag) -> String { tag.as_vec()[1].clone() }
145pub fn tag_extract_value_as_event_id(tag:&Tag) -> EventId {
146 EventId::from_str(tag.as_vec()[1].clone().as_str())
147 .expect("first tag value is a event id")
148}
149pub fn tag_extract_relays(tag:&Tag) -> Vec<UncheckedUrl> {
150 let mut relays = vec![];
151 let tag_vec = tag.as_vec();
152 for (i, s) in tag_vec.iter().enumerate() {
153 if i > 1 || (
154 i == 1 && tag_vec[0] == TagKind::Relays.to_string()
155 )
156 {
157 relays.push(
158 UncheckedUrl::from_str(s)
159 .expect("relay strings to not produce error on uncheckedUrl"),
160 );
161 }
162 }
163 relays
164}
165
166pub fn tag_is_group(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Group.to_string() }
167pub fn tag_is_admin_group(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::AdminGroup.to_string() }
168pub fn tag_is_repo(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Repo.to_string() }
169pub fn tag_is_branch(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Branch.to_string() }
170pub fn tag_is_branch_merged_from(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::BranchMergeFrom.to_string() }
171pub fn tag_is_patch(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Patch.to_string() }
172pub fn tag_is_patch_parent(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::PatchParent.to_string() }
173pub fn tag_is_commit(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::Commit.to_string() }
174pub fn tag_is_commit_parent(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::CommitParent.to_string() }
175pub fn tag_is_commit_message(tag:&Tag) -> bool { tag.kind().to_string() == TagKind::CommitMessage.to_string() }
176pub fn tag_is_initial_commit(tag:&Tag) -> bool {
177 tag.kind().to_string() == prelude::TagKind::T.to_string()
178 && tag.as_vec()[1] == TagKind::InitialCommit.to_string()
179}
180pub 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 @@
1use std::fs::File;
2use std::io::{Read, Write};
3use std::path::{Path};
4use std::time::Duration;
5
6use dialoguer::{Select, Input};
7use dialoguer::theme::ColorfulTheme;
8use nostr_sdk::blocking::Client;
9use nostr_sdk::prelude::*;
10
11use crate::config::{MyConfig, save_conifg};
12
13pub fn handle_keys(private_key: Option<String>, hex: bool) -> Result<Keys> {
14 // Parse and validate private key
15 let keys = match private_key {
16 Some(pk) => {
17 // create a new identity using the provided private key
18 Keys::from_sk_str(pk.as_str())?
19 }
20 None => {
21 // create a new identity with a new keypair
22 println!("No private key provided, creating new identity");
23 Keys::generate()
24 }
25 };
26
27 if !hex {
28 println!("Private key: {}", keys.secret_key()?.to_bech32()?);
29 println!("Public key: {}", keys.public_key().to_bech32()?);
30 } else {
31 println!("Private key: {}", keys.secret_key()?.display_secret());
32 println!("Public key: {}", keys.public_key());
33 }
34 Ok(keys)
35}
36
37// Creates the websocket client that is used for communicating with relays
38pub fn create_client(keys: &Keys, relays: Vec<String>) -> Result<Client> {
39 let opts = Options::new()
40 .wait_for_send(true)
41 .timeout(Some(Duration::from_secs(7)));
42 let client = Client::with_opts(keys, opts);
43 let relays = relays.iter().map(|url| (url, None)).collect();
44 client.add_relays(relays)?;
45 client.connect();
46 Ok(client)
47}
48
49// Accepts both hex and bech32 keys and returns the hex encoded key
50pub fn parse_key(key: String) -> Result<String> {
51 // Check if the key is a bech32 encoded key
52 let parsed_key = if key.starts_with("npub") {
53 XOnlyPublicKey::from_bech32(key)?.to_string()
54 } else if key.starts_with("nsec") {
55 SecretKey::from_bech32(key)?.display_secret().to_string()
56 } else if key.starts_with("note") {
57 EventId::from_bech32(key)?.to_hex()
58 } else if key.starts_with("nchannel") {
59 ChannelId::from_bech32(key)?.to_hex()
60 } else {
61 // If the key is not bech32 encoded, return it as is
62 key
63 };
64 Ok(parsed_key)
65}
66
67pub fn get_stored_keys(cfg:&mut MyConfig) -> Option<Keys> {
68 match &cfg.private_key {
69 None => None,
70 Some(k) => Some(Keys::new(*k)),
71 }
72}
73
74pub fn get_or_generate_keys(cfg:&mut MyConfig) -> Keys {
75 match cfg.private_key {
76 None => {
77 let selection = Select::with_theme(&ColorfulTheme::default())
78 .items(&vec!["enter existing private key", "generate new keys"])
79 .default(0)
80 .with_prompt("no keys are stored")
81 .interact().unwrap();
82 let key = match selection {
83 0 => {
84 let mut prompt = "secret key (nsec, hex, etc)";
85 loop {
86 let pk: String = Input::with_theme(&ColorfulTheme::default())
87 .with_prompt(prompt)
88 .interact_text()
89 .unwrap();
90 match Keys::from_sk_str(&pk) {
91 Ok(key) => { break key; },
92 Err(_e) => { prompt = "error interpeting secret key. try again with nsec, hex, etc"; },
93 }
94 }
95
96 }
97 _ => Keys::generate(),
98 };
99 cfg.private_key = Some(key.secret_key().unwrap());
100 save_conifg(&cfg);
101 key
102 }
103 Some(k) => Keys::new(k),
104 }
105}
106
107#[derive(clap::ValueEnum, Clone, Debug)]
108pub enum Prefix {
109 Npub,
110 Nsec,
111 Note,
112 Nchannel,
113}
114
115
116/// [`LoadFile`] error
117#[derive(Debug, thiserror::Error)]
118pub enum Error {
119 /// Error loading event file
120 #[error("cannot load event file.")]
121 // LoadFile(#[from] init::Error),
122 LoadFile(),
123}
124
125pub fn load_file<P: AsRef<Path>>(path: P) -> Result<String,Error> {
126 let mut buf = vec![];
127 match File::open(path) {
128 Ok(mut f) => {
129 f.read_to_end(&mut buf)
130 .expect("read_to_end not to error on file");
131 Ok(
132 std::str::from_utf8(&buf[..])
133 .expect("file contents u8 to convert to str")
134 .to_string(),
135 )
136 },
137 Err(_e) => { Err(Error::LoadFile()) },
138 }
139
140}
141
142pub fn load_event<P: AsRef<Path>>(path: P) -> Result<Event,Error> {
143 if let Ok(mut file) = File::open(path) {
144 let mut buf = vec![];
145 if file.read_to_end(&mut buf).is_ok() {
146 if let Ok(event) = Event::from_json(std::str::from_utf8(&buf[..]).unwrap()) {
147 return Ok(event)
148 }
149 }
150 }
151 Err(Error::LoadFile())
152}
153
154pub fn save_event<P: AsRef<Path>>(path: P, event: &Event) -> Result<()> {
155 let mut f = File::create(path)?;
156 f.write_all(&event.as_json().as_bytes())?;
157 Ok(())
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn test_parse_key_hex_input() {
166 let hex_key =
167 String::from("f4deaad98b61fa24d86ef315f1d5d57c1a6a533e1e87e777e5d0b48dcd332cdb");
168 let result = parse_key(hex_key.clone());
169
170 assert!(result.is_ok());
171 assert_eq!(result.unwrap(), hex_key);
172 }
173
174 #[test]
175 fn test_parse_key_bech32_note_input() {
176 let bech32_note_id =
177 String::from("note1h445ule4je70k7kvddate8kpsh2fd6n77esevww5hmgda2qwssjsw957wk");
178 let result = parse_key(bech32_note_id);
179
180 assert!(result.is_ok());
181 assert_eq!(
182 result.unwrap(),
183 String::from("bd6b4e7f35967cfb7acc6b7abc9ec185d496ea7ef6619639d4bed0dea80e8425")
184 );
185 }
186
187 #[test]
188 fn test_parse_bech32_public_key_input() {
189 let bech32_encoded_key =
190 String::from("npub1ktt8phjnkfmfrsxrgqpztdjuxk3x6psf80xyray0l3c7pyrln49qhkyhz0");
191 let result = parse_key(bech32_encoded_key);
192
193 assert!(result.is_ok());
194 assert_eq!(
195 result.unwrap(),
196 String::from("b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a")
197 );
198 }
199
200 #[test]
201 fn test_parse_bech32_private_key() {
202 let bech32_encoded_key =
203 String::from("nsec1hdeqm0y8vgzuucqv4840h7rlpy4qfu928ulxh3dzj6s2nqupdtzqagtew3");
204 let result = parse_key(bech32_encoded_key);
205
206 assert!(result.is_ok());
207 assert_eq!(
208 result.unwrap(),
209 String::from("bb720dbc876205ce600ca9eafbf87f092a04f0aa3f3e6bc5a296a0a983816ac4")
210 );
211 }
212}