Skip to content

Commit df05f5f

Browse files
committed
Fix the script
1 parent ce1d0c3 commit df05f5f

File tree

2 files changed

+163
-33
lines changed

2 files changed

+163
-33
lines changed

scripts/sync-working-groups/sync-working-groups.ts

Lines changed: 146 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
#!/usr/bin/env node
22

3-
import { writeFile } from "node:fs/promises"
3+
import { readFile, writeFile } from "node:fs/promises"
44
import { type } from "arktype"
55

66
const CALENDAR_ID =
77
"linuxfoundation.org_ik79t9uuj2p32i3r203dgv5mo8@group.calendar.google.com"
88
const API_KEY = process.env.GOOGLE_CALENDAR_API_KEY
9-
const OUTPUT_FILE = new URL("./upcoming-events.ndjson", import.meta.url)
10-
const MAX_RESULTS = 25
9+
const OUTPUT_FILE = new URL("./working-group-events.ndjson", import.meta.url)
10+
const DAYS_BACK = 30
11+
const DAYS_TO_KEEP = 90
12+
const DAYS_AHEAD = 30
1113
const DATETIME_REGEX =
1214
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/
1315

@@ -31,6 +33,8 @@ const Event = type({
3133

3234
const responseSchema = type({
3335
items: Event.array(),
36+
"nextSyncToken?": "string",
37+
"nextPageToken?": "string",
3438
})
3539

3640
const WorkingGroupMeetingSchema = type({
@@ -54,25 +58,49 @@ async function main() {
5458
process.exit(1)
5559
}
5660

57-
const timeMin = new Date().toISOString()
61+
const now = new Date()
62+
const existingMeetings = await readExistingMeetings()
63+
console.log(`Found ${existingMeetings.length} existing event(s) in file`)
64+
65+
const lastMeeting = existingMeetings.at(-1)
66+
const lastMeetingStart =
67+
lastMeeting?.start.dateTime ??
68+
(lastMeeting?.start.date ? `${lastMeeting.start.date}T00:00:00Z` : null)
69+
const cutoffDate = new Date(
70+
now.getTime() - DAYS_TO_KEEP * 24 * 60 * 60 * 1000,
71+
)
72+
5873
const searchParams = new URLSearchParams({
5974
key: API_KEY,
60-
timeMin,
6175
singleEvents: "true",
62-
orderBy: "startTime",
63-
maxResults: String(MAX_RESULTS),
6476
})
6577

78+
const timeMin =
79+
lastMeetingStart !== null && lastMeetingStart !== undefined
80+
? new Date(Math.min(Date.parse(lastMeetingStart) + 1, now.getTime()))
81+
: new Date(now.getTime() - DAYS_BACK * 24 * 60 * 60 * 1000)
82+
83+
const timeMax = new Date(now.getTime() + DAYS_AHEAD * 24 * 60 * 60 * 1000)
84+
searchParams.set("timeMin", timeMin.toISOString())
85+
searchParams.set("timeMax", timeMax.toISOString())
86+
searchParams.set("orderBy", "startTime")
87+
console.log(
88+
`\nSyncing from: ${timeMin.toLocaleDateString()} (${timeMin.toISOString()})`,
89+
)
90+
console.log(
91+
`Limiting to before: ${timeMax.toLocaleDateString()} (${timeMax.toISOString()})`,
92+
)
93+
6694
const endpoint = new URL(
67-
`${encodeURIComponent(CALENDAR_ID)}/events?${searchParams}`,
68-
"https://www.googleapis.com/calendar/v3/calendars/",
95+
`calendars/${encodeURIComponent(CALENDAR_ID)}/events?${searchParams}`,
96+
"https://www.googleapis.com/calendar/v3/",
6997
)
7098

71-
console.log(`\nFetching events for calendar: ${CALENDAR_ID}`)
72-
console.log(`Filtering from: ${timeMin}`)
99+
console.log(`Fetching events for calendar: ${CALENDAR_ID}`)
73100

74101
const response = await fetch(endpoint)
75102
const body = await response.json()
103+
76104
if (!response.ok) {
77105
const errorDetails = body.error?.message || response.statusText
78106
throw new Error(
@@ -81,20 +109,117 @@ async function main() {
81109
}
82110

83111
const payload = responseSchema.assert(body)
84-
const meetings = payload.items
112+
113+
let allNewMeetings = payload.items
85114
.filter(event => event.status !== "cancelled")
86115
.map(toWorkingGroupMeeting)
87-
.sort((a, b) => {
88-
const aStart = a.start.dateTime ?? a.start.date ?? ""
89-
const bStart = b.start.dateTime ?? b.start.date ?? ""
90-
return aStart.localeCompare(bStart)
91-
})
92-
93-
const ndjson = meetings.map(event => JSON.stringify(event)).join("\n")
94-
const content = meetings.length > 0 ? `${ndjson}\n` : ""
116+
117+
if (payload.nextPageToken) {
118+
let pageToken: string | undefined = payload.nextPageToken
119+
while (pageToken) {
120+
const pageParams = new URLSearchParams(searchParams)
121+
pageParams.set("pageToken", pageToken)
122+
const pageEndpoint = new URL(
123+
`${encodeURIComponent(CALENDAR_ID)}/events?${pageParams}`,
124+
"https://www.googleapis.com/calendar/v3/calendars/",
125+
)
126+
const pageResponse = await fetch(pageEndpoint)
127+
const pageBody = await pageResponse.json()
128+
if (!pageResponse.ok) {
129+
throw new Error(`Page fetch failed: ${pageResponse.status}`)
130+
}
131+
const pagePayload = responseSchema.assert(pageBody)
132+
allNewMeetings = [
133+
...allNewMeetings,
134+
...pagePayload.items
135+
.filter(event => event.status !== "cancelled")
136+
.map(toWorkingGroupMeeting),
137+
]
138+
pageToken = pagePayload.nextPageToken
139+
}
140+
}
141+
142+
const newMeetings = allNewMeetings
143+
const newIds = new Set(newMeetings.map(meeting => meeting.id))
144+
const existingIds = new Set(existingMeetings.map(meeting => meeting.id))
145+
const newCount = Array.from(newIds).filter(id => !existingIds.has(id)).length
146+
console.log(
147+
`Fetched ${newMeetings.length} event(s) from API (${newCount} new)`,
148+
)
149+
150+
const allMeetings = mergeMeetings(existingMeetings, newMeetings)
151+
const netChange = allMeetings.length - existingMeetings.length
152+
153+
if (netChange > 0) {
154+
console.log(`Added ${netChange} new event(s)`)
155+
} else if (netChange < 0) {
156+
console.log(`Removed ${Math.abs(netChange)} event(s)`)
157+
}
158+
const cutoffDateStr = cutoffDate.toISOString().split("T")[0]
159+
const futureLimit = new Date(now.getTime() + DAYS_AHEAD * 24 * 60 * 60 * 1000)
160+
const futureLimitStr = futureLimit.toISOString().split("T")[0]
161+
const filteredMeetings = allMeetings.filter(meeting => {
162+
const start = meeting.start.dateTime ?? meeting.start.date ?? ""
163+
const startDate = start.split("T")[0]
164+
return startDate >= cutoffDateStr && startDate <= futureLimitStr
165+
})
166+
167+
console.log(
168+
`Keeping events from ${cutoffDate.toLocaleDateString()} onwards (${DAYS_TO_KEEP} days)`,
169+
)
170+
console.log(
171+
`Filtered to ${filteredMeetings.length} event(s) after removing old entries`,
172+
)
173+
174+
const sortedMeetings = filteredMeetings.sort((a, b) => {
175+
const aStart = a.start.dateTime ?? a.start.date ?? ""
176+
const bStart = b.start.dateTime ?? b.start.date ?? ""
177+
return aStart.localeCompare(bStart)
178+
})
179+
180+
const ndjson = sortedMeetings.map(event => JSON.stringify(event)).join("\n")
181+
const content = sortedMeetings.length > 0 ? `${ndjson}\n` : ""
95182
await writeFile(OUTPUT_FILE, content, "utf8")
96183

97-
console.log(`Saved ${meetings.length} event(s) to ${OUTPUT_FILE.pathname}`)
184+
console.log(
185+
`Saved ${sortedMeetings.length} event(s) to ${OUTPUT_FILE.pathname}`,
186+
)
187+
}
188+
189+
async function readExistingMeetings(): Promise<WorkingGroupMeeting[]> {
190+
try {
191+
const content = await readFile(OUTPUT_FILE, "utf8")
192+
return content
193+
.trim()
194+
.split("\n")
195+
.filter(line => line.trim())
196+
.map(line => WorkingGroupMeetingSchema.assert(JSON.parse(line)))
197+
} catch (error: any) {
198+
if (error.code === "ENOENT") {
199+
return []
200+
}
201+
throw error
202+
}
203+
}
204+
205+
function mergeMeetings(
206+
existing: WorkingGroupMeeting[],
207+
incoming: WorkingGroupMeeting[],
208+
): WorkingGroupMeeting[] {
209+
const byId = new Map<string, WorkingGroupMeeting>()
210+
211+
for (const meeting of existing) {
212+
byId.set(meeting.id, meeting)
213+
}
214+
215+
for (const meeting of incoming) {
216+
const existing = byId.get(meeting.id)
217+
if (!existing || meeting.updated > existing.updated) {
218+
byId.set(meeting.id, meeting)
219+
}
220+
}
221+
222+
return Array.from(byId.values())
98223
}
99224

100225
function toWorkingGroupMeeting(event: CalendarEvent): WorkingGroupMeeting {

0 commit comments

Comments
 (0)