vta_cli_common/commands/
notifications.rs1use serde_json::{Value, json};
12use vta_sdk::prelude::*;
13
14use crate::render::{BOLD, CYAN, DIM, GREEN, RESET, is_json_output, print_json};
15
16#[derive(Debug, Clone, Default)]
18pub struct ListOptions {
19 pub kind: Option<String>,
20 pub held_by: Option<String>,
21 pub include_dismissed: bool,
22 pub include_lapsed: bool,
23 pub limit: Option<u32>,
24 pub cursor: Option<String>,
25}
26
27fn line(item: &Value) -> String {
28 let n = &item["notification"];
29 let mut period = String::new();
30 if let Some(until) = n["validUntil"].as_str() {
31 period = format!(" until {until}");
32 }
33 format!(
34 "{CYAN}{}{RESET} {BOLD}{}{RESET} {} at {}{}{} {DIM}from {}{}{RESET}",
35 item["recordId"].as_str().unwrap_or("?"),
36 n["kind"].as_str().unwrap_or("?"),
37 n["subject"]["id"].as_str().unwrap_or("?"),
38 n["subject"]["heldBy"].as_str().unwrap_or("?"),
39 period,
40 match (n["status"].as_str(), item["state"].as_str()) {
41 (Some("withdrawn"), _) => " (withdrawn)",
42 (_, Some("dismissed")) => " (dismissed)",
43 _ => "",
44 },
45 item["originator"].as_str().unwrap_or("?"),
46 match item["relayedBy"].as_array() {
47 Some(r) if !r.is_empty() => format!(
48 ", relayed by {}",
49 r.iter()
50 .filter_map(Value::as_str)
51 .collect::<Vec<_>>()
52 .join(" → ")
53 ),
54 _ => String::new(),
55 },
56 )
57}
58
59pub async fn cmd_notifications_list(
61 client: &VtaClient,
62 context: &str,
63 opts: &ListOptions,
64) -> Result<(), Box<dyn std::error::Error>> {
65 let mut req = json!({ "contextId": context });
66 if let Some(k) = &opts.kind {
67 req["kind"] = json!(k);
68 }
69 if let Some(h) = &opts.held_by {
70 req["heldBy"] = json!(h);
71 }
72 if opts.include_dismissed {
73 req["includeDismissed"] = json!(true);
74 }
75 if opts.include_lapsed {
76 req["includeLapsed"] = json!(true);
77 }
78 if let Some(l) = opts.limit {
79 req["limit"] = json!(l);
80 }
81 if let Some(c) = &opts.cursor {
82 req["cursor"] = json!(c);
83 }
84 let resp = client.notifications_list_json(req).await?;
85 if is_json_output() {
86 print_json(&resp)?;
87 return Ok(());
88 }
89 let items = resp["items"].as_array().cloned().unwrap_or_default();
90 if items.is_empty() {
91 println!("No notifications held in context '{context}'.");
92 } else {
93 println!();
94 println!("{BOLD}Notifications in context '{context}'{RESET}");
95 println!();
96 for item in &items {
97 println!(" {}", line(item));
98 }
99 println!();
100 }
101 if let Some(c) = resp["nextCursor"].as_str() {
102 println!("{DIM}More remain: pass --cursor {c}{RESET}");
103 }
104 Ok(())
105}
106
107pub async fn cmd_notifications_show(
109 client: &VtaClient,
110 context: &str,
111 record_id: &str,
112) -> Result<(), Box<dyn std::error::Error>> {
113 let resp = client
114 .notifications_show_json(json!({ "contextId": context, "recordId": record_id }))
115 .await?;
116 if is_json_output() {
117 print_json(&resp)?;
118 return Ok(());
119 }
120 println!();
121 println!(" {}", line(&resp["item"]));
122 let n = &resp["item"]["notification"];
123 println!(
124 " {DIM}audience {} open with {}{RESET}",
125 n["audience"].as_str().unwrap_or("?"),
126 n["subject"]["openWith"].as_str().unwrap_or("?")
127 );
128 println!();
129 println!("{BOLD}Delivering document, as received{RESET}");
130 println!("{}", serde_json::to_string_pretty(&resp["document"])?);
131 Ok(())
132}
133
134pub async fn cmd_notifications_dismiss(
136 client: &VtaClient,
137 context: &str,
138 record_id: &str,
139) -> Result<(), Box<dyn std::error::Error>> {
140 let resp = client
141 .notifications_dismiss_json(json!({ "contextId": context, "recordId": record_id }))
142 .await?;
143 if is_json_output() {
144 print_json(&resp)?;
145 return Ok(());
146 }
147 if resp["changed"].as_bool() == Some(true) {
148 println!("{GREEN}\u{2713}{RESET} Dismissed {BOLD}{record_id}{RESET}.");
149 } else {
150 println!("{BOLD}{record_id}{RESET} was already dismissed.");
151 }
152 Ok(())
153}
154
155pub async fn cmd_notifications_deposit(
158 client: &VtaClient,
159 context: &str,
160 file: &str,
161) -> Result<(), Box<dyn std::error::Error>> {
162 let text = if file == "-" {
163 let mut buf = String::new();
164 std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf)?;
165 buf
166 } else {
167 std::fs::read_to_string(file)?
168 };
169 let document: Value = serde_json::from_str(&text)
170 .map_err(|e| format!("{file} does not hold a JSON document: {e}"))?;
171 let resp = client
172 .notifications_deposit_json(json!({ "contextId": context, "document": document }))
173 .await?;
174 if is_json_output() {
175 print_json(&resp)?;
176 return Ok(());
177 }
178 println!(
179 "{GREEN}\u{2713}{RESET} Deposited {BOLD}{}{RESET} revision {}. Whether the VTA holds it \
180 is its acceptance policy's decision: `pnm notifications list --context {context}` shows \
181 what it holds.",
182 resp["notificationId"].as_str().unwrap_or("?"),
183 resp["revision"]
184 );
185 Ok(())
186}