Keep a copy of a calendar current: incremental sync with sync tokens (including the 410 Gone full resync), push notifications (events.watch, channels.stop), and parsing/verifying the webhook Google calls.

Sync loop

  1. First run: set ::sync/sync-events("primary", null) lists every event (deleted ones included) and returns set.next-sync-token.
  2. Later runs: sync-events("primary", saved-token) returns only what changed: apply set.changed, drop set.cancelled-ids, store the new set.next-sync-token.
  3. When Google expires the token (HTTP 410), sync-events does a full sync and sets full-sync: true: replace your copy rather than merging. Pass {resync: false} to get the 410 Err instead.

Filters that cannot be combined with a sync token (q, timeMin, timeMax, orderBy, updatedMin, iCalUID, extended properties) are dropped on incremental calls; keep the other options the same across runs.

Push. watch-events registers an HTTPS webhook. Notifications carry only headers: treat one as "run sync-events now". Channels expire (a week by default; channel-params: {ttl: "<seconds>"} to ask for a lifetime) and are not renewed automatically: renew from a Hot schedule with renew-watch. Set a channel-token and check it with verify-notification.

Scopes. calendar.events.readonly.

Values

SYNC_EXCLUDED

SYNC_EXCLUDED: Vec [
  "iCalUID",
  "orderBy",
  "privateExtendedProperty",
  "q",
  "sharedExtendedProperty",
  "timeMin",
  "timeMax",
  "updatedMin"
]

WATCH_LOCAL_KEYS

WATCH_LOCAL_KEYS: Vec [
  "channel-id",
  "channel-token",
  "expiration",
  "expires-in-ms",
  "channel-params",
  "resync"
]

Functions

build-channel

fn (address: Str, opts: Map, now-ms: Int): Map

Build a watch channel body (pure): {id, type: "web_hook", address, token?, expiration?, params?}. Options: channel-id (default a new UUID; at most 64 chars), channel-token (echoed back as X-Goog-Channel-Token; at most 256 chars; masked in call records), expiration (epoch ms) or expires-in-ms (from now-ms), channel-params (e.g. {ttl: "86400"}). address must be HTTPS with a valid certificate.

fn (headers: Map, name: Str): Str?

is-sync-token-expired

fn (result: Result): Bool

True when a result is the 410 Gone Google returns for an expired sync token.

now-ms

fn (): Int

parse-notification

fn (request: Map): Notification

Parse a webhook request into a Notification (pure). Header names are matched case-insensitively. Does not check the channel token: use verify-notification.

renew-watch

fn (old: Map?, calendar-id: Str, address: Str): Map
fn (old: Map?, calendar-id: Str, address: Str, opts: Map): Map

Replace a channel before it expires: open a new one first, then stop old (a watch response or {id, resourceId}; null to skip). Returns the new channel. Scope: calendar.events.readonly.

stop-channel

fn (channel-id: Str, resource-id: Str): Bool
fn (channel-id: Str, resource-id: Str, opts: Map): Bool

POST /channels/stop - stop notifications for a channel (id and resourceId from the watch response). Returns true. Scope: calendar.events.readonly.

summarize-events

fn (events: Vec, next-sync-token: Str?, full-sync: Bool, pages: Int): EventSet

Build an EventSet from the events of a sync (pure).

sync-events

fn (calendar-id: Str, sync-token: Str?): EventSet
fn (calendar-id: Str, sync-token: Str?, opts: Map): EventSet

Everything that changed in a calendar since sync-token (null for a first, full sync), as an EventSet. An expired token (410) triggers a full sync unless opts.resync is false. Other options are list-events options (e.g. singleEvents: true). Scope: calendar.events.readonly.

::sync ::google::calendar::sync
::store ::hot::store

state ::store/Map({name: "calendar-sync"})

calendar-sync
meta {schedule: "every 10 minutes"}
fn (event) {
  set ::sync/sync-events("primary", ::store/get(state, "sync-token"))
  // set.full-sync: replace the local copy; else apply set.changed and drop set.cancelled-ids
  ::store/put(state, "sync-token", set.next-sync-token)
}

sync-loop

fn (calendar-id: Str, sync-token: Str?, page-token: Str?, opts: Map, acc: Vec, pages: Int): EventSet

sync-params

fn (sync-token: Str?, page-token: Str?, opts: Map): Map

The list parameters for a sync page (pure): maxResults: 2500 unless set, syncToken when given (with the filters it cannot be combined with removed), else showDeleted: true for a full sync, plus pageToken.

verify-channel-token

fn (request: Map, expected: Str?): Bool

True when the request's X-Goog-Channel-Token equals expected (constant-time compare). Fails closed: an empty expected or a missing header is false. expected is masked in call records.

verify-notification

fn (request: Map): Notification
fn (request: Map, expected: Str): Notification

Verify and parse a Calendar webhook: returns the Notification, or an Err when the channel token is missing or wrong. The 1-arity form checks against the google.calendar.channel.token context variable and fails (halts) when it is unset, so an unconfigured endpoint never accepts traffic.

calendar-hook
meta {webhook: {service: "calendar", path: "/hook"}, secret-headers: ["x-goog-channel-token"]}
fn (request) {
  note ::google::calendar::sync/verify-notification(request)
  cond {
    is-err(note) => { ::hot::http/HttpResponse({status: 401, body: "bad channel token"}) }
    note.is-sync => { ::hot::http/HttpResponse({status: 200, body: ""}) }
    => {
      send("calendar:changed", {resource-id: note.resource-id})
      ::hot::http/HttpResponse({status: 200, body: ""})
    }
  }
}
Context Vars: google.calendar.channel.token

watch-events

fn (calendar-id: Str, address: Str): Map
fn (calendar-id: Str, address: Str, opts: Map): Map

POST /calendars/{id}/events/watch - get notified at address when events in the calendar change. Returns the channel (id, resourceId, expiration): keep id and resourceId to stop it. See build-channel for the channel options. Scope: calendar.events.readonly.

channel ::google::calendar::sync/watch-events("primary", "https://api.example.com/calendar/hook", {
  channel-token: ::hot::ctx/get("google.calendar.channel.token")
})

Types

EventSet

EventSet type {
    events: Vec,
    changed: Vec,
    cancelled-ids: Vec,
    next-sync-token: Str?,
    full-sync: Bool,
    pages: Int
}

The result of sync-events.

  • events - every event returned, in order
  • changed - events created or updated (status not cancelled)
  • cancelled-ids - ids of deleted or cancelled events
  • next-sync-token - store it for the next sync
  • full-sync - true when this was a full listing (first sync or a 410 resync): replace your copy
  • pages - how many pages were read

Notification

Notification type {
    channel-id: Str?,
    channel-token: Str?,
    channel-expiration: Str?,
    resource-id: Str?,
    resource-uri: Str?,
    resource-state: Str?,
    message-number: Int?,
    is-sync: Bool
}

A Calendar push notification, parsed from its headers (the body is empty).

  • channel-id, channel-token, channel-expiration
  • resource-id (stable), resource-uri (version specific)
  • resource-state - sync (first message on a new channel), exists (something changed), not_exists
  • message-number - increasing per channel
  • is-sync