//native
/**
* Update Following Instances
* Change one occurrence of a recurring event and all later ones ("this and following events"). Like Google Calendar, this splits the series: the original is cut off just before `instance_id` (from List Event Instances) and a new recurring event, with your changes, starts at that occurrence. Only the fields you set change; `start`/`end` (`YYYY-MM-DD` or RFC 3339) move the first new occurrence, `recurrence` (e.g. `["RRULE:FREQ=WEEKLY;BYDAY=TU"]`) replaces the rule. A COUNT limit is carried over as the occurrences that remain. Returns the trimmed original and the new series.
*/
export async function main(
auth: RT.Gcal,
instance_id: string,
calendar_id: string = "primary",
summary: string | undefined,
description: string | undefined,
location: string | undefined,
start: string | undefined,
end: string | undefined,
time_zone: string | undefined,
attendees: string[] | undefined,
recurrence: string[] | undefined,
send_updates: "all" | "externalOnly" | "none" = "all"
) {
const base = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendar_id || "primary")}/events`
const call = async (path: string, method = "GET", body?: object) => {
const response = await fetch(`${base}${path}`, {
method,
headers: {
Authorization: `Bearer ${auth.token}`,
...(body ? { "Content-Type": "application/json" } : {}),
Accept: "application/json",
},
body: body ? JSON.stringify(body) : undefined,
})
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`)
}
return await response.json()
}
const instance = await call(`/${instance_id}`)
if (!instance.recurringEventId) {
throw new Error(`${instance_id} is not an instance of a recurring event`)
}
const series = await call(`/${instance.recurringEventId}`)
const rules: string[] = series.recurrence ?? []
// The original series ends just before this occurrence (UNTIL is inclusive).
const original = instance.originalStartTime
let until: string
if (original.date) {
const d = new Date(`${original.date}T00:00:00Z`)
d.setUTCDate(d.getUTCDate() - 1)
until = d.toISOString().slice(0, 10).replace(/-/g, "")
} else {
const d = new Date(new Date(original.dateTime).getTime() - 1000)
until = d.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "")
}
// COUNT counts from the series start, so the new series gets what remains.
let remaining: number | undefined
const count = rules.join(";").match(/;COUNT=(\d+)/)
if (count) {
const before = await call(
`/${series.id}/instances?showDeleted=true&maxResults=2500&timeMax=${encodeURIComponent(original.dateTime ?? `${original.date}T00:00:00Z`)}`
)
remaining = Number(count[1]) - before.items.length
}
const trimmed = await call(
`/${series.id}?sendUpdates=${send_updates || "all"}`,
"PATCH",
{
recurrence: rules.map((r) =>
r.startsWith("RRULE:")
? `${r.replace(/;(UNTIL|COUNT)=[^;]+/g, "")};UNTIL=${until}`
: r
),
}
)
const when = (v: string | undefined, current: any) =>
!v
? { ...current, timeZone: time_zone || current.timeZone }
: /^\d{4}-\d{2}-\d{2}$/.test(v)
? { date: v }
: { dateTime: v, timeZone: time_zone || current.timeZone }
const created = await call(`?sendUpdates=${send_updates || "all"}`, "POST", {
summary: summary || series.summary,
description: description || series.description,
location: location || series.location,
colorId: series.colorId,
transparency: series.transparency,
visibility: series.visibility,
reminders: series.reminders,
guestsCanModify: series.guestsCanModify,
guestsCanInviteOthers: series.guestsCanInviteOthers,
guestsCanSeeOtherGuests: series.guestsCanSeeOtherGuests,
attendees: attendees?.length
? attendees.map((email) => ({ email }))
: series.attendees?.map((a: any) => ({
email: a.email,
optional: a.optional,
})),
start: when(start, instance.start),
end: when(end, instance.end),
recurrence:
(recurrence?.length ? recurrence : undefined) ??
rules.map((r) =>
r.startsWith("RRULE:") && remaining != null
? r.replace(/;COUNT=\d+/, `;COUNT=${remaining}`)
: r
),
})
return { original: trimmed, new_series: created }
}
Submitted by hugo989 6 hours ago