vta_cli_common/commands/
collection.rs1use serde_json::{Value, json};
10use vta_sdk::prelude::*;
11
12use crate::render::{BOLD, CYAN, DIM, GREEN, RESET, YELLOW, is_json_output, print_json};
13
14#[derive(Debug, Clone, Default)]
16pub struct ListOptions {
17 pub identifier: Option<String>,
18 pub collector: Option<String>,
19 pub limit: Option<u32>,
20 pub cursor: Option<String>,
21}
22
23fn refused_as(e: &VtaError, local: &str) -> bool {
26 e.rejection_code()
27 .is_some_and(|c| c == local || c.ends_with(&format!(":{local}")))
28}
29
30fn explain(
32 e: VtaError,
33 context: &str,
34 identifier: &str,
35 claiming: bool,
36) -> Box<dyn std::error::Error> {
37 let hint = if refused_as(&e, "claimHeld") {
38 format!(
39 "Another client holds a collection claim for {identifier}. Release it — it then \
40 ends at its stated end, because that client may still be connected — and claim \
41 again once it has ended:\n pnm collection release --context {context} \
42 --identifier {identifier}\n pnm collection list --context {context} --identifier \
43 {identifier}"
44 )
45 } else if refused_as(&e, "claimNotRenewable") {
46 format!(
47 "Your claim for {identifier} was released by another party and cannot be renewed. \
48 Stop collecting before its end (`pnm collection list --context {context} \
49 --identifier {identifier}` shows it), and claim again once it has ended."
50 )
51 } else if refused_as(&e, "notCollectable") {
52 format!(
53 "{identifier} is not an identifier this VTA collects for in context '{context}': it \
54 holds no messaging keys for it there, its DID document advertises no mediated \
55 transport, or it is the VTA's own identity, another node's or one provisioned for \
56 another party, which its holder collects for. `pnm collection list --context \
57 {context}` shows the identifiers it does.{}",
58 if claiming {
59 ""
60 } else {
61 " There is no claim to release."
62 }
63 )
64 } else {
65 return Box::new(e);
66 };
67 format!("{e}\n\n{hint}").into()
68}
69
70pub async fn cmd_collection_claim(
72 client: &VtaClient,
73 context: &str,
74 identifier: &str,
75 duration_seconds: Option<i64>,
76) -> Result<(), Box<dyn std::error::Error>> {
77 let mut req = json!({ "contextId": context, "identifier": identifier });
78 if let Some(d) = duration_seconds {
79 req["durationSeconds"] = json!(d);
80 }
81 let resp = client
82 .collection_claim_json(req)
83 .await
84 .map_err(|e| explain(e, context, identifier, true))?;
85 if is_json_output() {
86 print_json(&resp)?;
87 return Ok(());
88 }
89 let verb = if resp["renewed"].as_bool() == Some(true) {
90 "Renewed"
91 } else {
92 "Claimed"
93 };
94 println!(
95 "{GREEN}\u{2713}{RESET} {verb} {BOLD}{identifier}{RESET} until {}. The VTA has stopped \
96 collecting for it; stop collecting before that end unless you renew.",
97 resp["endsAt"].as_str().unwrap_or("?")
98 );
99 Ok(())
100}
101
102pub async fn cmd_collection_release(
104 client: &VtaClient,
105 context: &str,
106 identifier: &str,
107) -> Result<(), Box<dyn std::error::Error>> {
108 let resp = client
109 .collection_release_json(json!({ "contextId": context, "identifier": identifier }))
110 .await
111 .map_err(|e| explain(e, context, identifier, false))?;
112 if is_json_output() {
113 print_json(&resp)?;
114 return Ok(());
115 }
116 match resp["endsAt"].as_str() {
117 Some(at) => println!(
118 "{GREEN}\u{2713}{RESET} Released the claim for {BOLD}{identifier}{RESET}; it ends \
119 at {at}."
120 ),
121 None => println!("No claim for {BOLD}{identifier}{RESET} was in force; nothing changed."),
122 }
123 Ok(())
124}
125
126fn line(item: &Value) -> String {
127 let id = item["identifier"].as_str().unwrap_or("?");
128 match item["collector"].as_str() {
129 Some("client") => {
130 let c = &item["claim"];
131 format!(
132 "{CYAN}{id}{RESET} {BOLD}client{RESET} {} {DIM}until {}{}{RESET}",
133 c["claimant"].as_str().unwrap_or("?"),
134 c["endsAt"].as_str().unwrap_or("?"),
135 if c["renewable"].as_bool() == Some(false) {
136 ", not renewable"
137 } else {
138 ""
139 }
140 )
141 }
142 Some("none") => format!(
143 "{CYAN}{id}{RESET} {YELLOW}nobody{RESET} {DIM}({}){RESET}",
144 match item["notCollectedReason"].as_str() {
145 Some("disabled") => "collection by default is not enabled on this VTA",
146 Some("limit") => "the VTA is at its bound on collected identifiers",
147 Some("conflict") =>
148 "another connection kept evicting the VTA's; claim it from that client",
149 Some(other) => other,
150 None => "?",
151 }
152 ),
153 Some(c) => format!("{CYAN}{id}{RESET} {BOLD}{c}{RESET}"),
154 None => format!("{CYAN}{id}{RESET} ?"),
155 }
156}
157
158pub async fn cmd_collection_list(
160 client: &VtaClient,
161 context: &str,
162 opts: &ListOptions,
163) -> Result<(), Box<dyn std::error::Error>> {
164 let mut req = json!({ "contextId": context });
165 if let Some(i) = &opts.identifier {
166 req["identifier"] = json!(i);
167 }
168 if let Some(c) = &opts.collector {
169 req["collector"] = json!(c);
170 }
171 if let Some(l) = opts.limit {
172 req["limit"] = json!(l);
173 }
174 if let Some(c) = &opts.cursor {
175 req["cursor"] = json!(c);
176 }
177 let resp = client.collection_list_json(req).await?;
178 if is_json_output() {
179 print_json(&resp)?;
180 return Ok(());
181 }
182 let items = resp["items"].as_array().cloned().unwrap_or_default();
183 if items.is_empty() {
184 println!("No identifiers in context '{context}' that this VTA would collect for.");
185 } else {
186 println!();
187 println!("{BOLD}Collection in context '{context}'{RESET}");
188 println!();
189 for item in &items {
190 println!(" {}", line(item));
191 }
192 println!();
193 }
194 if let Some(c) = resp["nextCursor"].as_str() {
195 println!("{DIM}More remain: pass --cursor {c}{RESET}");
196 }
197 Ok(())
198}