Skip to main content

zoom_cli/commands/
meetings.rs

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}