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
- First run:
set ::sync/sync-events("primary", null)lists every event (deleted ones included) and returnsset.next-sync-token. - Later runs:
sync-events("primary", saved-token)returns only what changed: applyset.changed, dropset.cancelled-ids, store the newset.next-sync-token. - When Google expires the token (HTTP 410),
sync-eventsdoes a full sync and setsfull-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.
header
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: ""})
}
}
}
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 orderchanged- events created or updated (status notcancelled)cancelled-ids- ids of deleted or cancelled eventsnext-sync-token- store it for the next syncfull-sync- true when this was a full listing (first sync or a 410 resync): replace your copypages- 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-expirationresource-id(stable),resource-uri(version specific)resource-state-sync(first message on a new channel),exists(something changed),not_existsmessage-number- increasing per channelis-sync