1use crate::api::{ApiError, ZoomClient};
2use crate::output::{self, OutputConfig};
3
4#[allow(clippy::too_many_arguments)]
5pub async fn meetings(
6 client: &mut ZoomClient,
7 out: &OutputConfig,
8 user: &str,
9 from: &str,
10 to: Option<&str>,
11 limit: Option<u32>,
12 offset: Option<u32>,
13 fields: Option<&[String]>,
14) -> Result<(), ApiError> {
15 let result = client.list_user_meeting_reports(user, from, to).await?;
16
17 if out.json {
18 let mut items: Vec<serde_json::Value> = result
19 .meetings
20 .iter()
21 .map(|m| serde_json::to_value(m).expect("serialize"))
22 .collect();
23
24 if let Some(field_list) = fields {
25 items = items
26 .into_iter()
27 .map(|mut item| {
28 if let Some(obj) = item.as_object_mut() {
29 obj.retain(|k, _| field_list.iter().any(|f| f == k));
30 }
31 item
32 })
33 .collect();
34 }
35
36 let total = result.total_records.unwrap_or(items.len() as u64);
37 let offset_val = offset.unwrap_or(0) as usize;
38 let limited: Vec<serde_json::Value> = items
39 .into_iter()
40 .skip(offset_val)
41 .take(limit.unwrap_or(u32::MAX) as usize)
42 .collect();
43 let actual_limit = limit.unwrap_or(limited.len() as u32);
44
45 let envelope = serde_json::json!({
46 "items": limited,
47 "total": total,
48 "limit": actual_limit,
49 "offset": offset.unwrap_or(0)
50 });
51 out.print_data(&serde_json::to_string_pretty(&envelope).expect("serialize"));
52 } else {
53 if result.meetings.is_empty() {
54 out.print_message("No meetings found.");
55 return Ok(());
56 }
57 let rows: Vec<Vec<String>> = result
58 .meetings
59 .iter()
60 .map(|m| {
61 vec![
62 m.id.map(|id| id.to_string()).unwrap_or_default(),
63 m.topic.clone().unwrap_or_default(),
64 m.start_time
65 .as_deref()
66 .map(output::format_timestamp)
67 .unwrap_or_else(|| "-".into()),
68 m.duration
69 .map(|d| format!("{d} min"))
70 .unwrap_or_else(|| "-".into()),
71 m.participants_count
72 .map(|c| c.to_string())
73 .unwrap_or_else(|| "-".into()),
74 ]
75 })
76 .collect();
77 out.print_data(&output::table(
78 &["ID", "TOPIC", "START TIME", "DURATION", "PARTICIPANTS"],
79 &rows,
80 ));
81 if let Some(total) = result.total_records {
82 out.print_message(&format!("{total} meeting(s) total"));
83 }
84 }
85 Ok(())
86}
87
88pub async fn participants(
89 client: &mut ZoomClient,
90 out: &OutputConfig,
91 meeting_id: &str,
92) -> Result<(), ApiError> {
93 let result = client.list_meeting_participant_reports(meeting_id).await?;
94
95 if out.json {
96 out.print_data(&serde_json::to_string_pretty(&result).expect("serialize"));
97 } else {
98 if result.participants.is_empty() {
99 out.print_message("No participants found.");
100 return Ok(());
101 }
102 let rows: Vec<Vec<String>> = result
103 .participants
104 .iter()
105 .map(|p| {
106 vec![
107 p.name.clone().unwrap_or_default(),
108 p.user_email.clone().unwrap_or_default(),
109 p.join_time
110 .as_deref()
111 .map(output::format_timestamp)
112 .unwrap_or_else(|| "-".into()),
113 p.duration
114 .map(|d| format!("{} min", d / 60))
115 .unwrap_or_else(|| "-".into()),
116 ]
117 })
118 .collect();
119 out.print_data(&output::table(
120 &["NAME", "EMAIL", "JOIN TIME", "DURATION"],
121 &rows,
122 ));
123 if let Some(total) = result.total_records {
124 out.print_message(&format!("{total} participant(s) total"));
125 }
126 }
127 Ok(())
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::api::ZoomClient;
134 use wiremock::matchers::{method, path, query_param};
135 use wiremock::{Mock, MockServer, ResponseTemplate};
136
137 fn test_out() -> OutputConfig {
138 OutputConfig::for_test()
139 }
140
141 #[tokio::test]
142 async fn reports_meetings_empty_is_ok() {
143 let server = MockServer::start().await;
144 Mock::given(method("GET"))
145 .and(path("/v2/report/users/me/meetings"))
146 .and(query_param("from", "2026-04-01"))
147 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
148 "meetings": [],
149 "total_records": 0,
150 "from": "2026-04-01",
151 "to": "2026-04-30"
152 })))
153 .mount(&server)
154 .await;
155 let mut client =
156 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
157 meetings(
158 &mut client,
159 &test_out(),
160 "me",
161 "2026-04-01",
162 None,
163 None,
164 None,
165 None,
166 )
167 .await
168 .unwrap();
169 }
170
171 #[tokio::test]
172 async fn reports_meetings_renders_topic_and_duration() {
173 let server = MockServer::start().await;
174 Mock::given(method("GET"))
175 .and(path("/v2/report/users/me/meetings"))
176 .and(query_param("from", "2026-04-01"))
177 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
178 "meetings": [
179 {
180 "uuid": "abc==",
181 "id": 123456789,
182 "topic": "Weekly Standup",
183 "start_time": "2026-04-01T09:00:00Z",
184 "duration": 30,
185 "participants_count": 8
186 }
187 ],
188 "total_records": 1,
189 "from": "2026-04-01",
190 "to": "2026-04-30"
191 })))
192 .mount(&server)
193 .await;
194 let mut client =
195 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
196 let (out, buf) = OutputConfig::capturing();
197 meetings(
198 &mut client,
199 &out,
200 "me",
201 "2026-04-01",
202 None,
203 None,
204 None,
205 None,
206 )
207 .await
208 .unwrap();
209 let captured = buf.lock().unwrap().join("\n");
210 assert!(
211 captured.contains("Weekly Standup"),
212 "table must include topic"
213 );
214 assert!(captured.contains("30 min"), "table must include duration");
215 assert!(
216 captured.contains("8"),
217 "table must include participant count"
218 );
219 }
220
221 #[tokio::test]
222 async fn reports_participants_renders_name_email_and_duration() {
223 let server = MockServer::start().await;
224 Mock::given(method("GET"))
225 .and(path("/v2/report/meetings/123456789/participants"))
226 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
227 "participants": [
228 {
229 "name": "Alice",
230 "user_email": "alice@example.com",
231 "join_time": "2026-04-01T09:00:00Z",
232 "duration": 1800
233 }
234 ],
235 "total_records": 1
236 })))
237 .mount(&server)
238 .await;
239
240 let mut client =
241 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
242 let (out, buf) = OutputConfig::capturing();
243 participants(&mut client, &out, "123456789").await.unwrap();
244 let captured = buf.lock().unwrap().join("\n");
245 assert!(
246 captured.contains("Alice"),
247 "table must include participant name"
248 );
249 assert!(
250 captured.contains("alice@example.com"),
251 "table must include email"
252 );
253 assert!(captured.contains("30 min"), "1800s must render as 30 min");
254 }
255}