Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/issue-461-calendar-meet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": minor
---

feat: support google meet video conferencing in calendar +insert
65 changes: 16 additions & 49 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
86 changes: 81 additions & 5 deletions src/helpers/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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<String> = 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))
Expand Down Expand Up @@ -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()
}

Expand All @@ -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(&params).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();
Expand Down
Loading