Skip to content

calendar +agenda silently returns incomplete results #900

Description

@laudney

Summary

gws calendar +agenda says that it shows events across all calendars, but it can silently omit calendars and events.

At commit a3768d0e82ad83cca2da97724e46bea4ff0e6dbd:

  • The helper reads only the first calendarList page and ignores nextPageToken:
    // 1. List all calendars
    let list_url = "https://www.googleapis.com/calendar/v3/users/me/calendarList";
    let list_resp = client
    .get(list_url)
    .bearer_auth(&token)
    .send()
    .await
    .map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to list calendars: {e}")))?;
    if !list_resp.status().is_success() {
    let err = list_resp.text().await.unwrap_or_default();
    return Err(GwsError::Api {
    code: 0,
    message: err,
    reason: "calendarList_failed".to_string(),
    enable_url: None,
    });
    }
    let list_json: Value = list_resp
    .json()
    .await
    .map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to parse calendar list: {e}")))?;
    let calendars = list_json
    .get("items")
    .and_then(|i| i.as_array())
    .cloned()
    .unwrap_or_default();
  • Each calendar request hard-codes maxResults=50 and ignores the event nextPageToken:
    let mut all_events: Vec<Value> = stream::iter(filtered_calendars)
    .map(|cal| {
    let client = &client;
    let token = &token;
    let time_min = &time_min;
    let time_max = &time_max;
    async move {
    let events_url = format!(
    "https://www.googleapis.com/calendar/v3/calendars/{}/events",
    crate::validate::encode_path_segment(&cal.id),
    );
    let resp = crate::client::send_with_retry(|| {
    client
    .get(&events_url)
    .query(&[
    ("timeMin", time_min.as_str()),
    ("timeMax", time_max.as_str()),
    ("singleEvents", "true"),
    ("orderBy", "startTime"),
    ("maxResults", "50"),
    ])
    .bearer_auth(token)
    })
    .await;
    let resp = match resp {
    Ok(r) if r.status().is_success() => r,
    _ => return vec![],
    };
    let events_json: Value = match resp.json().await {
    Ok(v) => v,
    Err(_) => return vec![],
    };
    let mut events = Vec::new();
    if let Some(items) = events_json.get("items").and_then(|i| i.as_array()) {
  • A request error, non-success response, or JSON decode error is converted to an empty event list:
    let resp = crate::client::send_with_retry(|| {
    client
    .get(&events_url)
    .query(&[
    ("timeMin", time_min.as_str()),
    ("timeMax", time_max.as_str()),
    ("singleEvents", "true"),
    ("orderBy", "startTime"),
    ("maxResults", "50"),
    ])
    .bearer_auth(token)
    })
    .await;
    let resp = match resp {
    Ok(r) if r.status().is_success() => r,
    _ => return vec![],
    };
    let events_json: Value = match resp.json().await {
    Ok(v) => v,
    Err(_) => return vec![],
    };

This makes a partial result indistinguishable from a complete agenda with no events.

Reproduction

  1. Put more than 50 matching events in one calendar within the requested range.
  2. Run gws calendar +agenda for that range.
  3. The output contains at most 50 events from that calendar and reports no truncation.

A provider error for one secondary calendar also produces a successful result that omits that calendar.

Expected behavior

  • Follow nextPageToken for calendarList.list and every events.list request until it is absent.
  • Do not convert per-calendar failures into empty calendars. Return an error, or return an explicit partial-result status with the failed calendar and cause.
  • If the command has an intentional caller limit, expose the limit and report that the result is truncated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions