upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/bin/ngit/sub_commands/comment.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/ngit/sub_commands/comment.rs')
-rw-r--r--src/bin/ngit/sub_commands/comment.rs182
1 files changed, 182 insertions, 0 deletions
diff --git a/src/bin/ngit/sub_commands/comment.rs b/src/bin/ngit/sub_commands/comment.rs
new file mode 100644
index 0000000..a9b0aa7
--- /dev/null
+++ b/src/bin/ngit/sub_commands/comment.rs
@@ -0,0 +1,182 @@
1use anyhow::{Context, Result, bail};
2use ngit::{
3 client::{
4 Params, get_issues_from_cache, get_proposals_and_revisions_from_cache, send_events,
5 sign_event,
6 },
7 git_events::KIND_COMMENT,
8};
9use nostr::{EventBuilder, Tag, nips::nip19::Nip19};
10use nostr_sdk::{EventId, FromBech32, Kind};
11
12use crate::{
13 client::{Client, Connect, fetching_with_report, get_repo_ref_from_cache},
14 git::{Repo, RepoActions},
15 login,
16 repo_ref::get_repo_coordinates_when_remote_unknown,
17};
18
19fn parse_event_id(id: &str) -> Result<EventId> {
20 if let Ok(nip19) = Nip19::from_bech32(id) {
21 match nip19 {
22 nostr::nips::nip19::Nip19::Event(e) => return Ok(e.event_id),
23 nostr::nips::nip19::Nip19::EventId(event_id) => return Ok(event_id),
24 _ => {}
25 }
26 }
27 if let Ok(event_id) = EventId::from_hex(id) {
28 return Ok(event_id);
29 }
30 bail!("invalid event-id or nevent: {id}")
31}
32
33/// Build and publish a NIP-22 kind-1111 comment on any event.
34///
35/// NIP-22 threading tags:
36/// - uppercase `E` — root event id
37/// - uppercase `K` — root event kind (as string)
38/// - lowercase `e` — parent event id (same as root for top-level comments)
39/// - lowercase `k` — parent event kind
40async fn publish_comment(
41 id: &str,
42 body: &str,
43 offline: bool,
44 root_kind: Kind,
45 entity_name: &str,
46) -> Result<()> {
47 let event_id = parse_event_id(id)?;
48
49 let git_repo = Repo::discover().context("failed to find a git repository")?;
50 let git_repo_path = git_repo.get_path()?;
51
52 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
53 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
54
55 if !offline {
56 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
57 }
58
59 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
60
61 // Login
62 let (signer, user_ref, _) =
63 login::login_or_signup(&Some(&git_repo), &None, &None, Some(&client), true).await?;
64
65 let root_kind_str = root_kind.as_u16().to_string();
66
67 // NIP-22: uppercase E = root event, uppercase K = root kind,
68 // lowercase e = parent event (same as root for top-level),
69 // lowercase k = parent kind
70 let comment_event = sign_event(
71 EventBuilder::new(KIND_COMMENT, body).tags(vec![
72 // Root event (uppercase E)
73 Tag::parse(vec![
74 "E".to_string(),
75 event_id.to_hex(),
76 repo_ref
77 .relays
78 .first()
79 .map(ToString::to_string)
80 .unwrap_or_default(),
81 String::new(), // root marker
82 ])?,
83 // Root kind (uppercase K)
84 Tag::parse(vec!["K".to_string(), root_kind_str.clone()])?,
85 // Parent event (lowercase e, same as root for top-level comment)
86 Tag::parse(vec![
87 "e".to_string(),
88 event_id.to_hex(),
89 repo_ref
90 .relays
91 .first()
92 .map(ToString::to_string)
93 .unwrap_or_default(),
94 "reply".to_string(),
95 ])?,
96 // Parent kind (lowercase k)
97 Tag::parse(vec!["k".to_string(), root_kind_str])?,
98 ]),
99 &signer,
100 format!("comment on {entity_name}"),
101 )
102 .await?;
103
104 let mut client = client;
105 client.set_signer(signer).await;
106
107 send_events(
108 &client,
109 Some(git_repo_path),
110 vec![comment_event],
111 user_ref.relays.write(),
112 repo_ref.relays.clone(),
113 true,
114 false,
115 )
116 .await?;
117
118 println!(
119 "comment posted on {entity_name} {}",
120 &event_id.to_hex()[..8]
121 );
122 Ok(())
123}
124
125pub async fn launch_pr_comment(id: &str, body: &str, offline: bool) -> Result<()> {
126 // Verify the PR exists in cache
127 let event_id = parse_event_id(id)?;
128 let git_repo = Repo::discover().context("failed to find a git repository")?;
129 let git_repo_path = git_repo.get_path()?;
130 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
131 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
132
133 if !offline {
134 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
135 }
136
137 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
138 let proposals =
139 get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()).await?;
140
141 let proposal = proposals
142 .iter()
143 .find(|e| e.id == event_id)
144 .context(format!(
145 "PR with id {} not found in cache",
146 event_id.to_hex()
147 ))?;
148
149 let root_kind = proposal.kind;
150
151 publish_comment(id, body, true /* already fetched */, root_kind, "PR").await
152}
153
154pub async fn launch_issue_comment(id: &str, body: &str, offline: bool) -> Result<()> {
155 // Verify the issue exists in cache
156 let event_id = parse_event_id(id)?;
157 let git_repo = Repo::discover().context("failed to find a git repository")?;
158 let git_repo_path = git_repo.get_path()?;
159 let client = Client::new(Params::with_git_config_relay_defaults(&Some(&git_repo)));
160 let repo_coordinates = get_repo_coordinates_when_remote_unknown(&git_repo, &client).await?;
161
162 if !offline {
163 fetching_with_report(git_repo_path, &client, &repo_coordinates).await?;
164 }
165
166 let repo_ref = get_repo_ref_from_cache(Some(git_repo_path), &repo_coordinates).await?;
167 let issues = get_issues_from_cache(git_repo_path, repo_ref.coordinates()).await?;
168
169 issues.iter().find(|e| e.id == event_id).context(format!(
170 "issue with id {} not found in cache",
171 event_id.to_hex()
172 ))?;
173
174 publish_comment(
175 id,
176 body,
177 true, /* already fetched */
178 Kind::GitIssue,
179 "issue",
180 )
181 .await
182}