diff --git a/.changeset/issue-461-calendar-meet.md b/.changeset/issue-461-calendar-meet.md new file mode 100644 index 00000000..5e566462 --- /dev/null +++ b/.changeset/issue-461-calendar-meet.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": minor +--- + +feat: support google meet video conferencing in calendar +insert diff --git a/Cargo.lock b/Cargo.lock index 0d55ea8d..a999567d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,29 +457,19 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.20.11" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", @@ -489,37 +479,13 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.117", ] @@ -555,7 +521,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -920,6 +886,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "uuid", "yup-oauth2", "zeroize", ] @@ -1264,11 +1231,11 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.11" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" dependencies = [ - "darling 0.23.0", + "darling", "indoc", "proc-macro2", "quote", @@ -2979,9 +2946,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.2.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" diff --git a/Cargo.toml b/Cargo.toml index 24bc253b..2f5c7b12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ ratatui = "0.30.0" crossterm = "0.29.0" chrono = "0.4.44" chrono-tz = "0.10" +uuid = { version = "1.8", features = ["v4", "v5"] } iana-time-zone = "0.1" async-trait = "0.1.89" serde_yaml = "0.9.34" diff --git a/src/helpers/calendar.rs b/src/helpers/calendar.rs index 8ac00a50..c9d00bc9 100644 --- a/src/helpers/calendar.rs +++ b/src/helpers/calendar.rs @@ -80,14 +80,21 @@ impl Helper for CalendarHelper { .value_name("EMAIL") .action(ArgAction::Append), ) + .arg( + Arg::new("meet") + .long("meet") + .help("Add a Google Meet video conference link") + .action(ArgAction::SetTrue), + ) .after_help("\ EXAMPLES: gws calendar +insert --summary 'Standup' --start '2026-06-17T09:00:00-07:00' --end '2026-06-17T09:30:00-07:00' gws calendar +insert --summary 'Review' --start ... --end ... --attendee alice@example.com + gws calendar +insert --summary 'Meet' --start ... --end ... --meet TIPS: Use RFC3339 format for times (e.g. 2026-06-17T09:00:00-07:00). - For recurring events or conference links, use the raw API instead."), + The --meet flag automatically adds a Google Meet link to the event."), ); cmd = cmd.subcommand( Command::new("+agenda") @@ -453,13 +460,28 @@ fn build_insert_request( body["attendees"] = json!(attendees_list); } + let mut params = json!({ + "calendarId": calendar_id + }); + + if matches.get_flag("meet") { + let namespace = uuid::Uuid::NAMESPACE_DNS; + let seed_data = format!("{}:{}:{}", summary, start, end); + let request_id = uuid::Uuid::new_v5(&namespace, seed_data.as_bytes()).to_string(); + + body["conferenceData"] = json!({ + "createRequest": { + "requestId": request_id, + "conferenceSolutionKey": { "type": "hangoutsMeet" } + } + }); + params["conferenceDataVersion"] = json!(1); + } + let body_str = body.to_string(); let scopes: Vec = insert_method.scopes.iter().map(|s| s.to_string()).collect(); // events.insert requires 'calendarId' path parameter - let params = json!({ - "calendarId": calendar_id - }); let params_str = params.to_string(); Ok((params_str, body_str, scopes)) @@ -497,7 +519,8 @@ mod tests { Arg::new("attendee") .long("attendee") .action(ArgAction::Append), - ); + ) + .arg(Arg::new("meet").long("meet").action(ArgAction::SetTrue)); cmd.try_get_matches_from(args).unwrap() } @@ -521,6 +544,59 @@ mod tests { assert_eq!(scopes[0], "https://scope"); } + #[test] + fn test_build_insert_request_with_meet() { + let doc = make_mock_doc(); + let matches = make_matches_insert(&[ + "test", + "--summary", + "Meeting", + "--start", + "2024-01-01T10:00:00Z", + "--end", + "2024-01-01T11:00:00Z", + "--meet", + ]); + let (params, body, _) = build_insert_request(&matches, &doc).unwrap(); + + let params_json: serde_json::Value = serde_json::from_str(¶ms).unwrap(); + assert_eq!(params_json["conferenceDataVersion"], 1); + + let body_json: serde_json::Value = serde_json::from_str(&body).unwrap(); + let create_req = &body_json["conferenceData"]["createRequest"]; + assert_eq!(create_req["conferenceSolutionKey"]["type"], "hangoutsMeet"); + assert!(uuid::Uuid::parse_str(create_req["requestId"].as_str().unwrap()).is_ok()); + } + + #[test] + fn test_build_insert_request_with_meet_is_idempotent() { + let doc = make_mock_doc(); + let args = &[ + "test", + "--summary", + "Idempotent Meeting", + "--start", + "2024-01-01T10:00:00Z", + "--end", + "2024-01-01T11:00:00Z", + "--meet", + ]; + let matches1 = make_matches_insert(args); + let (_, body1, _) = build_insert_request(&matches1, &doc).unwrap(); + + let matches2 = make_matches_insert(args); + let (_, body2, _) = build_insert_request(&matches2, &doc).unwrap(); + + let b1: serde_json::Value = serde_json::from_str(&body1).unwrap(); + let b2: serde_json::Value = serde_json::from_str(&body2).unwrap(); + + assert_eq!( + b1["conferenceData"]["createRequest"]["requestId"], + b2["conferenceData"]["createRequest"]["requestId"], + "requestId should be deterministic for the same event details" + ); + } + #[test] fn test_build_insert_request_with_optional_fields() { let doc = make_mock_doc();