diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
| commit | 000901c0cbca8464b5a89bcc93c5474f6564bafd (patch) | |
| tree | 0ae11836c173ec6246e8b1eab7dc1e265e125426 /src | |
| parent | b9a88672b8734448615354e3f46748d2fdc2f647 (diff) | |
feat(prs-create) send to multiple relays
add tests but these currently don't work when run together
Diffstat (limited to 'src')
| -rw-r--r-- | src/client.rs | 23 | ||||
| -rw-r--r-- | src/git.rs | 17 | ||||
| -rw-r--r-- | src/login.rs | 3 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/sub_commands/prs/create.rs | 279 |
5 files changed, 292 insertions, 33 deletions
diff --git a/src/client.rs b/src/client.rs index a6f7dda..e0e0494 100644 --- a/src/client.rs +++ b/src/client.rs | |||
| @@ -18,6 +18,7 @@ use nostr::Event; | |||
| 18 | 18 | ||
| 19 | pub struct Client { | 19 | pub struct Client { |
| 20 | client: nostr_sdk::Client, | 20 | client: nostr_sdk::Client, |
| 21 | pub fallback_relays: Vec<String>, | ||
| 21 | } | 22 | } |
| 22 | 23 | ||
| 23 | #[async_trait] | 24 | #[async_trait] |
| @@ -26,6 +27,7 @@ pub trait Connect { | |||
| 26 | fn default() -> Self; | 27 | fn default() -> Self; |
| 27 | fn new(opts: Params) -> Self; | 28 | fn new(opts: Params) -> Self; |
| 28 | async fn connect(&self) -> Result<()>; | 29 | async fn connect(&self) -> Result<()>; |
| 30 | async fn disconnect(&self) -> Result<()>; | ||
| 29 | async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>; | 31 | async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>; |
| 30 | } | 32 | } |
| 31 | 33 | ||
| @@ -34,19 +36,31 @@ impl Connect for Client { | |||
| 34 | fn default() -> Self { | 36 | fn default() -> Self { |
| 35 | Client { | 37 | Client { |
| 36 | client: nostr_sdk::Client::new(&nostr::Keys::generate()), | 38 | client: nostr_sdk::Client::new(&nostr::Keys::generate()), |
| 39 | fallback_relays: vec![ | ||
| 40 | "ws://localhost:8080".to_string(), | ||
| 41 | "ws://localhost:8052".to_string(), | ||
| 42 | ], | ||
| 37 | } | 43 | } |
| 38 | } | 44 | } |
| 39 | fn new(opts: Params) -> Self { | 45 | fn new(opts: Params) -> Self { |
| 40 | Client { | 46 | Client { |
| 41 | client: nostr_sdk::Client::new(&opts.keys.unwrap_or(nostr::Keys::generate())), | 47 | client: nostr_sdk::Client::new(&opts.keys.unwrap_or(nostr::Keys::generate())), |
| 48 | fallback_relays: opts.fallback_relays, | ||
| 42 | } | 49 | } |
| 43 | } | 50 | } |
| 44 | async fn connect(&self) -> Result<()> { | 51 | async fn connect(&self) -> Result<()> { |
| 45 | self.client.add_relay("ws://localhost:8080", None).await?; | 52 | for relay in &self.fallback_relays { |
| 53 | self.client.add_relay(relay.as_str(), None).await?; | ||
| 54 | } | ||
| 46 | self.client.connect().await; | 55 | self.client.connect().await; |
| 47 | // self.client.s | ||
| 48 | Ok(()) | 56 | Ok(()) |
| 49 | } | 57 | } |
| 58 | |||
| 59 | async fn disconnect(&self) -> Result<()> { | ||
| 60 | self.client.disconnect().await?; | ||
| 61 | Ok(()) | ||
| 62 | } | ||
| 63 | |||
| 50 | async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> { | 64 | async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> { |
| 51 | Ok(self.client.send_event_to(url, event).await?) | 65 | Ok(self.client.send_event_to(url, event).await?) |
| 52 | } | 66 | } |
| @@ -55,6 +69,7 @@ impl Connect for Client { | |||
| 55 | #[derive(Default)] | 69 | #[derive(Default)] |
| 56 | pub struct Params { | 70 | pub struct Params { |
| 57 | pub keys: Option<nostr::Keys>, | 71 | pub keys: Option<nostr::Keys>, |
| 72 | pub fallback_relays: Vec<String>, | ||
| 58 | } | 73 | } |
| 59 | 74 | ||
| 60 | impl Params { | 75 | impl Params { |
| @@ -62,4 +77,8 @@ impl Params { | |||
| 62 | self.keys = Some(keys); | 77 | self.keys = Some(keys); |
| 63 | self | 78 | self |
| 64 | } | 79 | } |
| 80 | pub fn with_fallback_relays(mut self, fallback_relays: Vec<String>) -> Self { | ||
| 81 | self.fallback_relays = fallback_relays; | ||
| 82 | self | ||
| 83 | } | ||
| 65 | } | 84 | } |
| @@ -245,6 +245,23 @@ mod tests { | |||
| 245 | 245 | ||
| 246 | use super::*; | 246 | use super::*; |
| 247 | 247 | ||
| 248 | #[test] | ||
| 249 | fn get_commit_parent() -> Result<()> { | ||
| 250 | let test_repo = GitTestRepo::default(); | ||
| 251 | let parent_oid = test_repo.populate()?; | ||
| 252 | std::fs::write(test_repo.dir.join("t100.md"), "some content")?; | ||
| 253 | let child_oid = test_repo.stage_and_commit("add t100.md")?; | ||
| 254 | |||
| 255 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 256 | |||
| 257 | assert_eq!( | ||
| 258 | // Sha1Hash::from_byte_array("bla".to_string().as_bytes()), | ||
| 259 | oid_to_sha1(&parent_oid), | ||
| 260 | git_repo.get_commit_parent(&oid_to_sha1(&child_oid))?, | ||
| 261 | ); | ||
| 262 | Ok(()) | ||
| 263 | } | ||
| 264 | |||
| 248 | mod make_patch_from_commit { | 265 | mod make_patch_from_commit { |
| 249 | use super::*; | 266 | use super::*; |
| 250 | #[test] | 267 | #[test] |
diff --git a/src/login.rs b/src/login.rs index a6ce76d..12fe76e 100644 --- a/src/login.rs +++ b/src/login.rs | |||
| @@ -81,5 +81,8 @@ pub fn launch(nsec: &Option<String>, password: &Option<String>) -> Result<nostr: | |||
| 81 | .to_bech32() | 81 | .to_bech32() |
| 82 | .context("public key should always produce bech32")? | 82 | .context("public key should always produce bech32")? |
| 83 | ); | 83 | ); |
| 84 | |||
| 85 | // fetching metdata | ||
| 86 | |||
| 84 | Ok(key) | 87 | Ok(key) |
| 85 | } | 88 | } |
diff --git a/src/main.rs b/src/main.rs index 9c37aa7..5c8518b 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -24,6 +24,9 @@ pub struct Cli { | |||
| 24 | /// password to decrypt nsec | 24 | /// password to decrypt nsec |
| 25 | #[arg(short, long, global = true)] | 25 | #[arg(short, long, global = true)] |
| 26 | password: Option<String>, | 26 | password: Option<String>, |
| 27 | /// disable spinner animations | ||
| 28 | #[arg(long, action)] | ||
| 29 | disable_cli_spinners: bool, | ||
| 27 | } | 30 | } |
| 28 | 31 | ||
| 29 | #[derive(Subcommand)] | 32 | #[derive(Subcommand)] |
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs index 89ea652..3047e92 100644 --- a/src/sub_commands/prs/create.rs +++ b/src/sub_commands/prs/create.rs | |||
| @@ -1,4 +1,9 @@ | |||
| 1 | use std::time::Duration; | ||
| 2 | |||
| 1 | use anyhow::{bail, Context, Result}; | 3 | use anyhow::{bail, Context, Result}; |
| 4 | use console::Term; | ||
| 5 | use futures::future::join_all; | ||
| 6 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; | ||
| 2 | use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; | 7 | use nostr::{prelude::sha1::Hash as Sha1Hash, EventBuilder, Marker, Tag, TagKind}; |
| 3 | 8 | ||
| 4 | use crate::{ | 9 | use crate::{ |
| @@ -79,14 +84,245 @@ pub async fn launch( | |||
| 79 | .input(PromptInputParms::default().with_prompt("description (Optional)"))?, | 84 | .input(PromptInputParms::default().with_prompt("description (Optional)"))?, |
| 80 | }; | 85 | }; |
| 81 | 86 | ||
| 82 | let root_commit = git_repo | ||
| 83 | .get_root_commit(to_branch.as_str()) | ||
| 84 | .context("failed to get root commit of the repository")?; | ||
| 85 | |||
| 86 | // create PR event | 87 | // create PR event |
| 87 | 88 | ||
| 88 | let keys = login::launch(&cli_args.nsec, &cli_args.password)?; | 89 | let keys = login::launch(&cli_args.nsec, &cli_args.password)?; |
| 89 | 90 | ||
| 91 | let events = | ||
| 92 | generate_pr_and_patch_events(&title, &description, &to_branch, &git_repo, &ahead, keys)?; | ||
| 93 | |||
| 94 | let my_write_relays: Vec<String> = vec![ | ||
| 95 | "ws://localhost:8051".to_string(), | ||
| 96 | "ws://localhost:8052".to_string(), | ||
| 97 | ]; | ||
| 98 | |||
| 99 | let repo_read_relays: Vec<String> = vec![ | ||
| 100 | "ws://localhost:8051".to_string(), | ||
| 101 | "ws://localhost:8053".to_string(), | ||
| 102 | ]; | ||
| 103 | |||
| 104 | send_events( | ||
| 105 | events, | ||
| 106 | keys, | ||
| 107 | my_write_relays, | ||
| 108 | repo_read_relays, | ||
| 109 | !cli_args.disable_cli_spinners, | ||
| 110 | ) | ||
| 111 | .await?; | ||
| 112 | // TODO check if there is already a similarly named PR | ||
| 113 | |||
| 114 | // println!("failures:"); | ||
| 115 | // println!("ws://relay.anon.io Error: Payment Required"); | ||
| 116 | |||
| 117 | // should we have a relays in Repository event? | ||
| 118 | // yes | ||
| 119 | // | ||
| 120 | |||
| 121 | // TODO connect to relays and post | ||
| 122 | |||
| 123 | Ok(()) | ||
| 124 | } | ||
| 125 | |||
| 126 | async fn send_events( | ||
| 127 | events: Vec<nostr::Event>, | ||
| 128 | keys: nostr::Keys, | ||
| 129 | my_write_relays: Vec<String>, | ||
| 130 | repo_read_relays: Vec<String>, | ||
| 131 | animate: bool, | ||
| 132 | ) -> Result<()> { | ||
| 133 | let (_, _, _, all) = unique_and_duplicate_all(&my_write_relays, &repo_read_relays); | ||
| 134 | |||
| 135 | let client = Client::new( | ||
| 136 | ClientParams::default() | ||
| 137 | .with_keys(keys) | ||
| 138 | // .with_fallback_relays(vec!["ws://localhost:8080".to_string()]), | ||
| 139 | .with_fallback_relays(all.iter().map(std::string::ToString::to_string).collect()), | ||
| 140 | ); | ||
| 141 | |||
| 142 | let term = Term::stdout(); | ||
| 143 | term.write_line("connecting to relays...")?; | ||
| 144 | client.connect().await?; | ||
| 145 | term.clear_last_lines(1)?; | ||
| 146 | |||
| 147 | println!( | ||
| 148 | "posting 1 pull request with {} commits...", | ||
| 149 | events.len() - 1 | ||
| 150 | ); | ||
| 151 | |||
| 152 | let m = MultiProgress::new(); | ||
| 153 | let pb_style = ProgressStyle::with_template(if animate { | ||
| 154 | " {spinner} {prefix} {bar} {pos}/{len} {msg}" | ||
| 155 | } else { | ||
| 156 | " - {prefix} {bar} {pos}/{len} {msg}" | ||
| 157 | })? | ||
| 158 | .progress_chars("##-"); | ||
| 159 | |||
| 160 | let pb_after_style = | ||
| 161 | |symbol| ProgressStyle::with_template(format!(" {symbol} {}", "{prefix} {msg}",).as_str()); | ||
| 162 | let pb_after_style_succeeded = pb_after_style(if animate { | ||
| 163 | console::style("✔".to_string()) | ||
| 164 | .for_stderr() | ||
| 165 | .green() | ||
| 166 | .to_string() | ||
| 167 | } else { | ||
| 168 | "y".to_string() | ||
| 169 | })?; | ||
| 170 | |||
| 171 | let pb_after_style_failed = pb_after_style(if animate { | ||
| 172 | console::style("✘".to_string()) | ||
| 173 | .for_stderr() | ||
| 174 | .red() | ||
| 175 | .to_string() | ||
| 176 | } else { | ||
| 177 | "x".to_string() | ||
| 178 | })?; | ||
| 179 | |||
| 180 | join_all(all.iter().map(|&relay| async { | ||
| 181 | let details = format!( | ||
| 182 | "{}{} {}", | ||
| 183 | if my_write_relays.iter().any(|r| relay.eq(r)) { | ||
| 184 | " [my-relay]" | ||
| 185 | } else { | ||
| 186 | "" | ||
| 187 | }, | ||
| 188 | if repo_read_relays.iter().any(|r| relay.eq(r)) { | ||
| 189 | " [repo-relay]" | ||
| 190 | } else { | ||
| 191 | "" | ||
| 192 | }, | ||
| 193 | *relay, | ||
| 194 | ); | ||
| 195 | let pb = m.add( | ||
| 196 | ProgressBar::new(events.len() as u64) | ||
| 197 | .with_prefix(details.to_string()) | ||
| 198 | .with_style(pb_style.clone()), | ||
| 199 | ); | ||
| 200 | if animate { | ||
| 201 | pb.enable_steady_tick(Duration::from_millis(300)); | ||
| 202 | } | ||
| 203 | pb.inc(0); // need to make pb display intially | ||
| 204 | let mut failed = false; | ||
| 205 | for event in &events { | ||
| 206 | match client.send_event_to(relay.as_str(), event.clone()).await { | ||
| 207 | Ok(_) => pb.inc(1), | ||
| 208 | Err(e) => { | ||
| 209 | pb.set_style(pb_after_style_failed.clone()); | ||
| 210 | pb.finish_with_message( | ||
| 211 | console::style( | ||
| 212 | e.to_string() | ||
| 213 | .replace("relay pool error:", "error:") | ||
| 214 | .replace("event not published: ", ""), | ||
| 215 | ) | ||
| 216 | .for_stderr() | ||
| 217 | .red() | ||
| 218 | .to_string(), | ||
| 219 | ); | ||
| 220 | failed = true; | ||
| 221 | break; | ||
| 222 | } | ||
| 223 | }; | ||
| 224 | } | ||
| 225 | if !failed { | ||
| 226 | pb.set_style(pb_after_style_succeeded.clone()); | ||
| 227 | pb.finish_with_message(""); | ||
| 228 | } | ||
| 229 | })) | ||
| 230 | .await; | ||
| 231 | client.disconnect().await?; | ||
| 232 | Ok(()) | ||
| 233 | } | ||
| 234 | |||
| 235 | /// returns `(unique_vec1, unique_vec2, duplicates, all)` | ||
| 236 | fn unique_and_duplicate_all<'a, S>( | ||
| 237 | vec1: &'a Vec<S>, | ||
| 238 | vec2: &'a Vec<S>, | ||
| 239 | ) -> (Vec<&'a S>, Vec<&'a S>, Vec<&'a S>, Vec<&'a S>) | ||
| 240 | where | ||
| 241 | S: PartialEq, | ||
| 242 | { | ||
| 243 | let mut vec1_u = vec![]; | ||
| 244 | let mut vec2_u = vec![]; | ||
| 245 | let mut dup = vec![]; | ||
| 246 | let mut all = vec![]; | ||
| 247 | for s1 in vec1 { | ||
| 248 | if vec2.iter().any(|s2| s1.eq(s2)) { | ||
| 249 | dup.push(s1); | ||
| 250 | } else { | ||
| 251 | vec1_u.push(s1); | ||
| 252 | } | ||
| 253 | } | ||
| 254 | for s2 in vec2 { | ||
| 255 | if !vec1.iter().any(|s1| s2.eq(s1)) { | ||
| 256 | vec2_u.push(s2); | ||
| 257 | } | ||
| 258 | } | ||
| 259 | for a in [&dup, &vec1_u, &vec2_u] { | ||
| 260 | for e in a { | ||
| 261 | all.push(&**e); | ||
| 262 | } | ||
| 263 | } | ||
| 264 | (vec1_u, vec2_u, dup, all) | ||
| 265 | } | ||
| 266 | |||
| 267 | mod tests_unique_and_duplicate { | ||
| 268 | |||
| 269 | #[test] | ||
| 270 | fn correct_number_of_unique_and_duplicate_items() { | ||
| 271 | let v1 = vec![ | ||
| 272 | "t1".to_string(), | ||
| 273 | "t2".to_string(), | ||
| 274 | "t3".to_string(), | ||
| 275 | "t4".to_string(), | ||
| 276 | "t5".to_string(), | ||
| 277 | ]; | ||
| 278 | let v2 = vec![ | ||
| 279 | "t3".to_string(), | ||
| 280 | "t4".to_string(), | ||
| 281 | "t5".to_string(), | ||
| 282 | "t6".to_string(), | ||
| 283 | ]; | ||
| 284 | |||
| 285 | let (v1_u, v2_u, d, a) = super::unique_and_duplicate_all(&v1, &v2); | ||
| 286 | |||
| 287 | assert_eq!(v1_u.len(), 2); | ||
| 288 | assert_eq!(v2_u.len(), 1); | ||
| 289 | assert_eq!(d.len(), 3); | ||
| 290 | assert_eq!(a.len(), 6); | ||
| 291 | } | ||
| 292 | #[test] | ||
| 293 | fn all_begins_with_duplicates() { | ||
| 294 | let v1 = vec![ | ||
| 295 | "t1".to_string(), | ||
| 296 | "t2".to_string(), | ||
| 297 | "t3".to_string(), | ||
| 298 | "t4".to_string(), | ||
| 299 | "t5".to_string(), | ||
| 300 | ]; | ||
| 301 | let v2 = vec![ | ||
| 302 | "t3".to_string(), | ||
| 303 | "t4".to_string(), | ||
| 304 | "t5".to_string(), | ||
| 305 | "t6".to_string(), | ||
| 306 | ]; | ||
| 307 | |||
| 308 | let (_, _, d, a) = super::unique_and_duplicate_all(&v1, &v2); | ||
| 309 | |||
| 310 | assert_eq!(a[0], d[0]); | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | fn generate_pr_and_patch_events( | ||
| 315 | title: &String, | ||
| 316 | description: &String, | ||
| 317 | to_branch: &str, | ||
| 318 | git_repo: &Repo, | ||
| 319 | commits: &Vec<Sha1Hash>, | ||
| 320 | keys: nostr::Keys, | ||
| 321 | ) -> Result<Vec<nostr::Event>> { | ||
| 322 | let root_commit = git_repo | ||
| 323 | .get_root_commit(to_branch) | ||
| 324 | .context("failed to get root commit of the repository")?; | ||
| 325 | |||
| 90 | let pr_event = EventBuilder::new( | 326 | let pr_event = EventBuilder::new( |
| 91 | nostr::event::Kind::Custom(318), | 327 | nostr::event::Kind::Custom(318), |
| 92 | format!("{title}\r\n\r\n{description}"), | 328 | format!("{title}\r\n\r\n{description}"), |
| @@ -103,12 +339,14 @@ pub async fn launch( | |||
| 103 | .to_event(&keys) | 339 | .to_event(&keys) |
| 104 | .context("failed to create pr event")?; | 340 | .context("failed to create pr event")?; |
| 105 | 341 | ||
| 106 | let mut patch_events = vec![]; | 342 | let pr_event_id = pr_event.id; |
| 107 | for commit in &ahead { | 343 | |
| 344 | let mut events = vec![pr_event]; | ||
| 345 | for commit in commits { | ||
| 108 | let commit_parent = git_repo | 346 | let commit_parent = git_repo |
| 109 | .get_commit_parent(commit) | 347 | .get_commit_parent(commit) |
| 110 | .context("failed to create patch event")?; | 348 | .context("failed to create patch event")?; |
| 111 | patch_events.push( | 349 | events.push( |
| 112 | EventBuilder::new( | 350 | EventBuilder::new( |
| 113 | nostr::event::Kind::Custom(317), | 351 | nostr::event::Kind::Custom(317), |
| 114 | git_repo | 352 | git_repo |
| @@ -119,7 +357,7 @@ pub async fn launch( | |||
| 119 | Tag::Hashtag(commit.to_string()), | 357 | Tag::Hashtag(commit.to_string()), |
| 120 | Tag::Hashtag(commit_parent.to_string()), | 358 | Tag::Hashtag(commit_parent.to_string()), |
| 121 | Tag::Event( | 359 | Tag::Event( |
| 122 | pr_event.id, | 360 | pr_event_id, |
| 123 | None, // TODO: add relay | 361 | None, // TODO: add relay |
| 124 | Some(Marker::Root), | 362 | Some(Marker::Root), |
| 125 | ), | 363 | ), |
| @@ -136,32 +374,11 @@ pub async fn launch( | |||
| 136 | // TODO: add relay tags | 374 | // TODO: add relay tags |
| 137 | ], | 375 | ], |
| 138 | ) | 376 | ) |
| 139 | .to_event(&keys), | 377 | .to_event(&keys)?, |
| 140 | ); | 378 | ); |
| 141 | } | 379 | } |
| 142 | 380 | Ok(events) | |
| 143 | let client = Client::new(ClientParams::default().with_keys(keys)); | ||
| 144 | |||
| 145 | println!("connecting..."); | ||
| 146 | client.connect().await?; | ||
| 147 | println!("connected..."); | ||
| 148 | |||
| 149 | // TODO check if there is already a similarly named PR | ||
| 150 | let _ = client | ||
| 151 | .send_event_to("ws://localhost:8080", pr_event) | ||
| 152 | .await?; | ||
| 153 | // TODO post each PR | ||
| 154 | // TODO report | ||
| 155 | println!("posted successfully to 4/5 of your relays and 0/4 of maintainers relays"); | ||
| 156 | // should we have a relays in Repository event? | ||
| 157 | // yes | ||
| 158 | // | ||
| 159 | |||
| 160 | // TODO connect to relays and post | ||
| 161 | |||
| 162 | Ok(()) | ||
| 163 | } | 381 | } |
| 164 | |||
| 165 | // TODO | 382 | // TODO |
| 166 | // - find profile | 383 | // - find profile |
| 167 | // - file relays | 384 | // - file relays |