diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-05 13:31:35 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-03-05 13:31:35 +0000 |
| commit | 609f3c3db02d437222e2c8e171189179d06c3e9c (patch) | |
| tree | 23747f31010721fa11c76cca467a4ef89d95eda5 /src | |
| parent | b143abb25d6ece32412629baa3e75e94d139979f (diff) | |
feat(json): emit nevent1 bech32 IDs in --json output
All id and reply_to fields in --json output now use nevent1... bech32
encoding (with relay hint when available) instead of raw hex, making
them directly usable as nostr: URI references in --body text.
Update SKILL.md to document the nevent1 ID format and enforce use of
nostr:nevent1... URIs when cross-referencing events in body text.
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/ngit/sub_commands/issue_list.rs | 37 | ||||
| -rw-r--r-- | src/bin/ngit/sub_commands/list.rs | 40 |
2 files changed, 59 insertions, 18 deletions
diff --git a/src/bin/ngit/sub_commands/issue_list.rs b/src/bin/ngit/sub_commands/issue_list.rs index 8f78d5e..29845ce 100644 --- a/src/bin/ngit/sub_commands/issue_list.rs +++ b/src/bin/ngit/sub_commands/issue_list.rs | |||
| @@ -8,9 +8,9 @@ use ngit::{ | |||
| 8 | use nostr::{ | 8 | use nostr::{ |
| 9 | FromBech32, ToBech32, | 9 | FromBech32, ToBech32, |
| 10 | filter::{Alphabet, SingleLetterTag}, | 10 | filter::{Alphabet, SingleLetterTag}, |
| 11 | nips::nip19::Nip19, | 11 | nips::nip19::{Nip19, Nip19Event}, |
| 12 | }; | 12 | }; |
| 13 | use nostr_sdk::Kind; | 13 | use nostr_sdk::{Kind, RelayUrl}; |
| 14 | 14 | ||
| 15 | use crate::{ | 15 | use crate::{ |
| 16 | client::{Client, Connect, fetching_with_report, get_repo_ref_from_cache}, | 16 | client::{Client, Connect, fetching_with_report, get_repo_ref_from_cache}, |
| @@ -247,17 +247,20 @@ pub async fn launch( | |||
| 247 | } else { | 247 | } else { |
| 248 | vec![] | 248 | vec![] |
| 249 | }; | 249 | }; |
| 250 | let relay_hint = repo_ref.relays.first(); | ||
| 250 | return show_issue_details( | 251 | return show_issue_details( |
| 251 | &filtered, | 252 | &filtered, |
| 252 | event_id_or_nevent, | 253 | event_id_or_nevent, |
| 253 | json, | 254 | json, |
| 254 | show_comments, | 255 | show_comments, |
| 255 | &comments, | 256 | &comments, |
| 257 | relay_hint, | ||
| 256 | ); | 258 | ); |
| 257 | } | 259 | } |
| 258 | 260 | ||
| 261 | let relay_hint = repo_ref.relays.first(); | ||
| 259 | if json { | 262 | if json { |
| 260 | output_json(&filtered)?; | 263 | output_json(&filtered, relay_hint)?; |
| 261 | } else { | 264 | } else { |
| 262 | output_table(&filtered, &status, &label_filter); | 265 | output_table(&filtered, &status, &label_filter); |
| 263 | } | 266 | } |
| @@ -298,6 +301,7 @@ fn show_issue_details( | |||
| 298 | json: bool, | 301 | json: bool, |
| 299 | show_comments: bool, | 302 | show_comments: bool, |
| 300 | comments: &[nostr::Event], | 303 | comments: &[nostr::Event], |
| 304 | relay_hint: Option<&RelayUrl>, | ||
| 301 | ) -> Result<()> { | 305 | ) -> Result<()> { |
| 302 | let target_id = if event_id_or_nevent.starts_with("nevent") { | 306 | let target_id = if event_id_or_nevent.starts_with("nevent") { |
| 303 | let nip19 = Nip19::from_bech32(event_id_or_nevent).context("failed to parse nevent")?; | 307 | let nip19 = Nip19::from_bech32(event_id_or_nevent).context("failed to parse nevent")?; |
| @@ -323,9 +327,10 @@ fn show_issue_details( | |||
| 323 | let comments_json: Vec<serde_json::Value> = comments | 327 | let comments_json: Vec<serde_json::Value> = comments |
| 324 | .iter() | 328 | .iter() |
| 325 | .map(|c| { | 329 | .map(|c| { |
| 326 | let reply_to = comment_reply_to(c).map(|id| id.to_string()); | 330 | let reply_to = |
| 331 | comment_reply_to(c).map(|id| event_id_to_nevent(id, relay_hint)); | ||
| 327 | serde_json::json!({ | 332 | serde_json::json!({ |
| 328 | "id": c.id.to_string(), | 333 | "id": event_id_to_nevent(c.id, relay_hint), |
| 329 | "author": c.pubkey.to_bech32().unwrap_or_default(), | 334 | "author": c.pubkey.to_bech32().unwrap_or_default(), |
| 330 | "created_at": c.created_at.as_secs(), | 335 | "created_at": c.created_at.as_secs(), |
| 331 | "reply_to": reply_to, | 336 | "reply_to": reply_to, |
| @@ -334,7 +339,7 @@ fn show_issue_details( | |||
| 334 | }) | 339 | }) |
| 335 | .collect(); | 340 | .collect(); |
| 336 | serde_json::json!({ | 341 | serde_json::json!({ |
| 337 | "id": issue.id.to_string(), | 342 | "id": event_id_to_nevent(issue.id, relay_hint), |
| 338 | "status": status, | 343 | "status": status, |
| 339 | "subject": title, | 344 | "subject": title, |
| 340 | "author": issue.pubkey.to_bech32().unwrap_or_default(), | 345 | "author": issue.pubkey.to_bech32().unwrap_or_default(), |
| @@ -345,7 +350,7 @@ fn show_issue_details( | |||
| 345 | }) | 350 | }) |
| 346 | } else { | 351 | } else { |
| 347 | serde_json::json!({ | 352 | serde_json::json!({ |
| 348 | "id": issue.id.to_string(), | 353 | "id": event_id_to_nevent(issue.id, relay_hint), |
| 349 | "status": status, | 354 | "status": status, |
| 350 | "subject": title, | 355 | "subject": title, |
| 351 | "author": issue.pubkey.to_bech32().unwrap_or_default(), | 356 | "author": issue.pubkey.to_bech32().unwrap_or_default(), |
| @@ -463,12 +468,26 @@ fn output_table( | |||
| 463 | println!(); | 468 | println!(); |
| 464 | } | 469 | } |
| 465 | 470 | ||
| 466 | fn output_json(issues: &[IssueRow<'_>]) -> Result<()> { | 471 | /// Convert an event ID to a `nevent1…` bech32 string, including a relay hint |
| 472 | /// when one is available. Falls back to the plain hex string on error. | ||
| 473 | fn event_id_to_nevent(event_id: nostr::EventId, relay: Option<&RelayUrl>) -> String { | ||
| 474 | let relays = relay.map(|r| vec![r.clone()]).unwrap_or_default(); | ||
| 475 | Nip19Event { | ||
| 476 | event_id, | ||
| 477 | relays, | ||
| 478 | author: None, | ||
| 479 | kind: None, | ||
| 480 | } | ||
| 481 | .to_bech32() | ||
| 482 | .unwrap_or_else(|_| event_id.to_hex()) | ||
| 483 | } | ||
| 484 | |||
| 485 | fn output_json(issues: &[IssueRow<'_>], relay_hint: Option<&RelayUrl>) -> Result<()> { | ||
| 467 | let json_output: Vec<serde_json::Value> = issues | 486 | let json_output: Vec<serde_json::Value> = issues |
| 468 | .iter() | 487 | .iter() |
| 469 | .map(|(issue, status_kind, labels, comment_count, subject_override)| { | 488 | .map(|(issue, status_kind, labels, comment_count, subject_override)| { |
| 470 | serde_json::json!({ | 489 | serde_json::json!({ |
| 471 | "id": issue.id.to_string(), | 490 | "id": event_id_to_nevent(issue.id, relay_hint), |
| 472 | "status": status_kind_to_str(*status_kind), | 491 | "status": status_kind_to_str(*status_kind), |
| 473 | "subject": get_issue_title(issue, subject_override.as_deref()), | 492 | "subject": get_issue_title(issue, subject_override.as_deref()), |
| 474 | "author": issue.pubkey.to_bech32().unwrap_or_default(), | 493 | "author": issue.pubkey.to_bech32().unwrap_or_default(), |
diff --git a/src/bin/ngit/sub_commands/list.rs b/src/bin/ngit/sub_commands/list.rs index df147bc..ee9840e 100644 --- a/src/bin/ngit/sub_commands/list.rs +++ b/src/bin/ngit/sub_commands/list.rs | |||
| @@ -24,9 +24,9 @@ use ngit::{ | |||
| 24 | use nostr::{ | 24 | use nostr::{ |
| 25 | FromBech32, ToBech32, | 25 | FromBech32, ToBech32, |
| 26 | filter::{Alphabet, SingleLetterTag}, | 26 | filter::{Alphabet, SingleLetterTag}, |
| 27 | nips::nip19::Nip19, | 27 | nips::nip19::{Nip19, Nip19Event}, |
| 28 | }; | 28 | }; |
| 29 | use nostr_sdk::Kind; | 29 | use nostr_sdk::{Kind, RelayUrl}; |
| 30 | 30 | ||
| 31 | use crate::{ | 31 | use crate::{ |
| 32 | cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, | 32 | cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, |
| @@ -246,6 +246,7 @@ pub async fn launch( | |||
| 246 | .await? | 246 | .await? |
| 247 | .len() | 247 | .len() |
| 248 | }; | 248 | }; |
| 249 | let relay_hint = repo_ref.relays.first(); | ||
| 249 | return show_proposal_details( | 250 | return show_proposal_details( |
| 250 | &filtered_proposals, | 251 | &filtered_proposals, |
| 251 | event_id_or_nevent, | 252 | event_id_or_nevent, |
| @@ -253,11 +254,13 @@ pub async fn launch( | |||
| 253 | show_comments, | 254 | show_comments, |
| 254 | comment_count, | 255 | comment_count, |
| 255 | &comments, | 256 | &comments, |
| 257 | relay_hint, | ||
| 256 | ); | 258 | ); |
| 257 | } | 259 | } |
| 258 | 260 | ||
| 261 | let relay_hint = repo_ref.relays.first(); | ||
| 259 | if json { | 262 | if json { |
| 260 | output_json(&filtered_proposals)?; | 263 | output_json(&filtered_proposals, relay_hint)?; |
| 261 | } else { | 264 | } else { |
| 262 | output_table(&filtered_proposals, &status, &label_filter); | 265 | output_table(&filtered_proposals, &status, &label_filter); |
| 263 | } | 266 | } |
| @@ -384,11 +387,28 @@ fn output_table( | |||
| 384 | ); | 387 | ); |
| 385 | } | 388 | } |
| 386 | 389 | ||
| 387 | fn output_json(proposals: &[(&nostr::Event, Kind, Vec<String>, Option<String>)]) -> Result<()> { | 390 | /// Convert an event ID to a `nevent1…` bech32 string, including a relay hint |
| 391 | /// when one is available. Falls back to the plain hex string on error. | ||
| 392 | fn event_id_to_nevent(event_id: nostr::EventId, relay: Option<&RelayUrl>) -> String { | ||
| 393 | let relays = relay.map(|r| vec![r.clone()]).unwrap_or_default(); | ||
| 394 | Nip19Event { | ||
| 395 | event_id, | ||
| 396 | relays, | ||
| 397 | author: None, | ||
| 398 | kind: None, | ||
| 399 | } | ||
| 400 | .to_bech32() | ||
| 401 | .unwrap_or_else(|_| event_id.to_hex()) | ||
| 402 | } | ||
| 403 | |||
| 404 | fn output_json( | ||
| 405 | proposals: &[(&nostr::Event, Kind, Vec<String>, Option<String>)], | ||
| 406 | relay_hint: Option<&RelayUrl>, | ||
| 407 | ) -> Result<()> { | ||
| 388 | let json_output: Vec<serde_json::Value> = proposals | 408 | let json_output: Vec<serde_json::Value> = proposals |
| 389 | .iter() | 409 | .iter() |
| 390 | .map(|(proposal, status_kind, proposal_labels, subject_override)| { | 410 | .map(|(proposal, status_kind, proposal_labels, subject_override)| { |
| 391 | let id = proposal.id.to_string(); | 411 | let id = event_id_to_nevent(proposal.id, relay_hint); |
| 392 | let status = status_kind_to_str(*status_kind).to_string(); | 412 | let status = status_kind_to_str(*status_kind).to_string(); |
| 393 | let (title, author, branch) = if let Ok(cl) = event_to_cover_letter(proposal) { | 413 | let (title, author, branch) = if let Ok(cl) = event_to_cover_letter(proposal) { |
| 394 | ( | 414 | ( |
| @@ -460,6 +480,7 @@ fn show_proposal_details( | |||
| 460 | show_comments: bool, | 480 | show_comments: bool, |
| 461 | comment_count: usize, | 481 | comment_count: usize, |
| 462 | comments: &[nostr::Event], | 482 | comments: &[nostr::Event], |
| 483 | relay_hint: Option<&RelayUrl>, | ||
| 463 | ) -> Result<()> { | 484 | ) -> Result<()> { |
| 464 | use nostr::ToBech32; | 485 | use nostr::ToBech32; |
| 465 | 486 | ||
| @@ -484,9 +505,10 @@ fn show_proposal_details( | |||
| 484 | let comments_json: Vec<serde_json::Value> = comments | 505 | let comments_json: Vec<serde_json::Value> = comments |
| 485 | .iter() | 506 | .iter() |
| 486 | .map(|c| { | 507 | .map(|c| { |
| 487 | let reply_to = comment_reply_to(c).map(|id| id.to_string()); | 508 | let reply_to = |
| 509 | comment_reply_to(c).map(|id| event_id_to_nevent(id, relay_hint)); | ||
| 488 | serde_json::json!({ | 510 | serde_json::json!({ |
| 489 | "id": c.id.to_string(), | 511 | "id": event_id_to_nevent(c.id, relay_hint), |
| 490 | "author": c.pubkey.to_bech32().unwrap_or_default(), | 512 | "author": c.pubkey.to_bech32().unwrap_or_default(), |
| 491 | "created_at": c.created_at.as_secs(), | 513 | "created_at": c.created_at.as_secs(), |
| 492 | "reply_to": reply_to, | 514 | "reply_to": reply_to, |
| @@ -495,7 +517,7 @@ fn show_proposal_details( | |||
| 495 | }) | 517 | }) |
| 496 | .collect(); | 518 | .collect(); |
| 497 | serde_json::json!({ | 519 | serde_json::json!({ |
| 498 | "id": proposal.id.to_string(), | 520 | "id": event_id_to_nevent(proposal.id, relay_hint), |
| 499 | "status": status_kind_to_str(*status_kind), | 521 | "status": status_kind_to_str(*status_kind), |
| 500 | "subject": display_title, | 522 | "subject": display_title, |
| 501 | "author": proposal.pubkey.to_bech32().unwrap_or_default(), | 523 | "author": proposal.pubkey.to_bech32().unwrap_or_default(), |
| @@ -507,7 +529,7 @@ fn show_proposal_details( | |||
| 507 | }) | 529 | }) |
| 508 | } else { | 530 | } else { |
| 509 | serde_json::json!({ | 531 | serde_json::json!({ |
| 510 | "id": proposal.id.to_string(), | 532 | "id": event_id_to_nevent(proposal.id, relay_hint), |
| 511 | "status": status_kind_to_str(*status_kind), | 533 | "status": status_kind_to_str(*status_kind), |
| 512 | "subject": display_title, | 534 | "subject": display_title, |
| 513 | "author": proposal.pubkey.to_bech32().unwrap_or_default(), | 535 | "author": proposal.pubkey.to_bech32().unwrap_or_default(), |