Skip to content
Open
Show file tree
Hide file tree
Changes from all 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The seed for the idempotent requestId only includes summary, start, and end. This means that two different events with the same summary and time but different attendees, description, or location will generate the same requestId. To ensure true idempotency and avoid potential collisions, it's better to include all relevant event details in the seed. I recommend including location, description, and a sorted list of attendees in the seed_data if they are provided.

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);
}
Comment on lines +467 to +479
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While this PR successfully adds the --meet flag, I've identified a potential idempotency issue. Using a random v4 UUID for the requestId means that if a user retries a failed command, a new Meet link might be generated, potentially leading to duplicate calendar events with different conference links.

To make this operation idempotent, generating a deterministic v5 UUID based on the event's details would be ideal. This ensures that running the same command multiple times will result in the same requestId, preventing duplicate conference data.

However, to avoid introducing changes outside the primary goal of this pull request and prevent scope creep, I recommend addressing this idempotency improvement in a separate, follow-up PR. This would involve updating the uuid crate features in Cargo.toml to include v5 and macro-diagnostics and implementing the deterministic UUID generation.

References
  1. Avoid introducing changes that are outside the primary goal of a pull request to prevent scope creep.


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