1use crate::api::types::{CreateMeetingRequest, UpdateMeetingRequest};
2use crate::api::{ApiError, ZoomClient};
3use crate::output::{self, OutputConfig};
4
5pub async fn list(
6 client: &mut ZoomClient,
7 out: &OutputConfig,
8 user: &str,
9 meeting_type: Option<&str>,
10) -> Result<(), ApiError> {
11 let result = client.list_meetings(user, meeting_type).await?;
12
13 if out.json {
14 out.print_data(&serde_json::to_string_pretty(&result).expect("serialize"));
15 } else {
16 if result.meetings.is_empty() {
17 out.print_message("No meetings found.");
18 return Ok(());
19 }
20 let rows: Vec<Vec<String>> = result
21 .meetings
22 .iter()
23 .map(|m| {
24 vec![
25 m.id.to_string(),
26 m.topic.clone(),
27 m.start_time
28 .as_deref()
29 .map(output::format_timestamp)
30 .unwrap_or_else(|| "-".into()),
31 m.duration
32 .map(|d| format!("{d} min"))
33 .unwrap_or_else(|| "-".into()),
34 ]
35 })
36 .collect();
37 out.print_data(&output::table(
38 &["ID", "TOPIC", "START TIME", "DURATION"],
39 &rows,
40 ));
41 if let Some(total) = result.total_records {
42 out.print_message(&format!("{total} meeting(s) total"));
43 }
44 }
45 Ok(())
46}
47
48pub async fn get(
49 client: &mut ZoomClient,
50 out: &OutputConfig,
51 meeting_id: u64,
52) -> Result<(), ApiError> {
53 let meeting = client.get_meeting(meeting_id).await?;
54
55 if out.json {
56 out.print_data(&serde_json::to_string_pretty(&meeting).expect("serialize"));
57 } else {
58 let join_url = meeting.join_url.clone().unwrap_or_else(|| "-".into());
59 out.print_data(&output::kv_block(&[
60 ("id", meeting.id.to_string()),
61 ("topic", meeting.topic.clone()),
62 (
63 "start_time",
64 meeting
65 .start_time
66 .as_deref()
67 .map(output::format_timestamp)
68 .unwrap_or_else(|| "-".into()),
69 ),
70 (
71 "duration",
72 meeting
73 .duration
74 .map(|d| format!("{d} min"))
75 .unwrap_or_else(|| "-".into()),
76 ),
77 (
78 "status",
79 meeting.status.clone().unwrap_or_else(|| "-".into()),
80 ),
81 ("join_url", output::hyperlink(&join_url)),
82 ]));
83 }
84 Ok(())
85}
86
87pub async fn create(
88 client: &mut ZoomClient,
89 out: &OutputConfig,
90 topic: String,
91 duration: Option<u32>,
92 start: Option<String>,
93 password: Option<String>,
94) -> Result<(), ApiError> {
95 let meeting_type = if start.is_some() { 2 } else { 1 };
96 let req = CreateMeetingRequest {
97 topic,
98 start_time: start,
99 duration,
100 password,
101 meeting_type,
102 };
103 let meeting = client.create_meeting("me", req).await?;
104
105 if out.json {
106 out.print_data(&serde_json::to_string_pretty(&meeting).expect("serialize"));
107 } else {
108 let join_url = meeting.join_url.clone().unwrap_or_else(|| "-".into());
109 out.print_result(
110 &serde_json::json!({}),
111 &format!(
112 "Meeting created: {} (ID: {})\nJoin URL: {}",
113 meeting.topic,
114 meeting.id,
115 output::hyperlink(&join_url)
116 ),
117 );
118 }
119 Ok(())
120}
121
122pub async fn update(
123 client: &mut ZoomClient,
124 out: &OutputConfig,
125 meeting_id: u64,
126 topic: Option<String>,
127 duration: Option<u32>,
128 start: Option<String>,
129) -> Result<(), ApiError> {
130 let req = UpdateMeetingRequest {
131 topic,
132 duration,
133 start_time: start,
134 };
135 client.update_meeting(meeting_id, req).await?;
136
137 out.print_result(
138 &serde_json::json!({"updated": true, "id": meeting_id}),
139 &format!("Meeting {meeting_id} updated."),
140 );
141 Ok(())
142}
143
144pub async fn delete(
145 client: &mut ZoomClient,
146 out: &OutputConfig,
147 meeting_id: u64,
148) -> Result<(), ApiError> {
149 client.delete_meeting(meeting_id).await?;
150
151 out.print_result(
152 &serde_json::json!({"deleted": true, "id": meeting_id}),
153 &format!("Meeting {meeting_id} deleted."),
154 );
155 Ok(())
156}
157
158pub async fn end(
159 client: &mut ZoomClient,
160 out: &OutputConfig,
161 meeting_id: u64,
162) -> Result<(), ApiError> {
163 client.end_meeting(meeting_id).await?;
164 out.print_result(
165 &serde_json::json!({"ended": true, "id": meeting_id}),
166 &format!("Meeting {meeting_id} ended."),
167 );
168 Ok(())
169}
170
171pub async fn participants(
172 client: &mut ZoomClient,
173 out: &OutputConfig,
174 meeting_id: &str,
175) -> Result<(), ApiError> {
176 let result = client.list_past_meeting_participants(meeting_id).await?;
177
178 if out.json {
179 out.print_data(&serde_json::to_string_pretty(&result).expect("serialize"));
180 } else {
181 if result.participants.is_empty() {
182 out.print_message("No participants found.");
183 return Ok(());
184 }
185 let rows: Vec<Vec<String>> = result
186 .participants
187 .iter()
188 .map(|p| {
189 vec![
190 p.name.clone().unwrap_or_default(),
191 p.user_email.clone().unwrap_or_else(|| "-".into()),
192 p.join_time
193 .as_deref()
194 .map(output::format_timestamp)
195 .unwrap_or_else(|| "-".into()),
196 p.leave_time
197 .as_deref()
198 .map(output::format_timestamp)
199 .unwrap_or_else(|| "-".into()),
200 p.duration
201 .map(|s| format!("{} min", s / 60))
202 .unwrap_or_else(|| "-".into()),
203 ]
204 })
205 .collect();
206 out.print_data(&output::table(
207 &["NAME", "EMAIL", "JOIN TIME", "LEAVE TIME", "DURATION"],
208 &rows,
209 ));
210 if let Some(total) = result.total_records {
211 out.print_message(&format!("{total} participant(s) total"));
212 }
213 }
214 Ok(())
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::api::ZoomClient;
221 use wiremock::matchers::{method, path};
222 use wiremock::{Mock, MockServer, ResponseTemplate};
223
224 fn test_out() -> OutputConfig {
225 OutputConfig {
226 json: true,
227 quiet: true,
228 }
229 }
230
231 #[tokio::test]
232 async fn meetings_list_empty_is_ok() {
233 let server = MockServer::start().await;
234 Mock::given(method("GET"))
235 .and(path("/v2/users/me/meetings"))
236 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
237 "meetings": [], "total_records": 0
238 })))
239 .mount(&server)
240 .await;
241
242 let mut client =
243 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
244 list(&mut client, &test_out(), "me", None).await.unwrap();
245 }
246
247 #[tokio::test]
248 async fn meetings_create_returns_meeting() {
249 let server = MockServer::start().await;
250 Mock::given(method("POST"))
251 .and(path("/v2/users/me/meetings"))
252 .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({
253 "id": 123456789,
254 "topic": "New Meeting",
255 "join_url": "https://zoom.us/j/123456789",
256 "start_url": "https://zoom.us/s/123456789"
257 })))
258 .mount(&server)
259 .await;
260
261 let mut client =
262 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
263 create(
264 &mut client,
265 &test_out(),
266 "New Meeting".into(),
267 Some(30),
268 None,
269 None,
270 )
271 .await
272 .unwrap();
273 }
274
275 #[tokio::test]
276 async fn meetings_delete_succeeds_on_204() {
277 let server = MockServer::start().await;
278 Mock::given(method("DELETE"))
279 .and(path("/v2/meetings/111222333"))
280 .respond_with(ResponseTemplate::new(204))
281 .mount(&server)
282 .await;
283
284 let mut client =
285 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
286 delete(&mut client, &test_out(), 111222333).await.unwrap();
287 }
288
289 #[tokio::test]
290 async fn meetings_get_not_found_propagates() {
291 let server = MockServer::start().await;
292 Mock::given(method("GET"))
293 .and(path("/v2/meetings/999"))
294 .respond_with(ResponseTemplate::new(404).set_body_string("not found"))
295 .mount(&server)
296 .await;
297
298 let mut client =
299 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
300 let err = get(&mut client, &test_out(), 999).await.unwrap_err();
301 assert!(matches!(err, ApiError::NotFound(_)));
302 }
303
304 #[tokio::test]
305 async fn meetings_update_sends_patch_and_returns_ok() {
306 let server = MockServer::start().await;
307 Mock::given(method("PATCH"))
308 .and(path("/v2/meetings/123"))
309 .respond_with(ResponseTemplate::new(204))
310 .mount(&server)
311 .await;
312
313 let mut client =
314 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
315 update(
316 &mut client,
317 &test_out(),
318 123,
319 Some("Updated".into()),
320 None,
321 None,
322 )
323 .await
324 .unwrap();
325 }
326
327 #[tokio::test]
328 async fn meetings_end_sends_put_and_returns_ok() {
329 let server = MockServer::start().await;
330 Mock::given(method("PUT"))
331 .and(path("/v2/meetings/555666777/status"))
332 .respond_with(ResponseTemplate::new(204))
333 .mount(&server)
334 .await;
335 let mut client =
336 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
337 end(&mut client, &test_out(), 555666777).await.unwrap();
338 }
339
340 #[tokio::test]
341 async fn meetings_participants_returns_table_data() {
342 let server = MockServer::start().await;
343 Mock::given(method("GET"))
344 .and(path("/v2/past_meetings/abc123/participants"))
345 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
346 "participants": [
347 {
348 "name": "Alice",
349 "user_email": "alice@example.com",
350 "join_time": "2026-04-01T10:00:00Z",
351 "leave_time": "2026-04-01T10:45:00Z",
352 "duration": 2700
353 }
354 ],
355 "total_records": 1
356 })))
357 .mount(&server)
358 .await;
359 let mut client =
360 ZoomClient::new_for_test(format!("{}/v2", server.uri()), server.uri(), "tok".into());
361 participants(&mut client, &test_out(), "abc123").await.unwrap();
362 }
363}