import { listEventsAcrossCalendars } from 'kody:@kody/google/calendar'
import { dayBounds, skippedSection, timeOnly } from '../format.ts'
import { googleCalendarSetupError } from '../google-setup.ts'
import type { BriefingInput, BriefingItem, BriefingSection } from '../types.ts'
type CalendarDateTime = { date?: string; dateTime?: string }
type CalendarEvent = {
summary?: string
start?: CalendarDateTime
end?: CalendarDateTime
htmlLink?: string
recurringEventId?: string
recurrence?: string[]
calendarSummary?: string
calendarId?: string
accountLabel?: string
}
type CalendarListResult = {
items?: CalendarEvent[]
calendars?: unknown[]
failures?: string[]
}
function isRecurring(event: CalendarEvent): boolean {
return Boolean(event.recurringEventId) || Boolean(event.recurrence?.length)
}
function eventSortKey(event: CalendarEvent): string {
return event.start?.dateTime || event.start?.date || ''
}
function formatWhen(event: CalendarEvent, timezone: string): string {
if (event.start?.date) return 'All day'
const start = timeOnly(event.start?.dateTime, timezone)
const end = timeOnly(event.end?.dateTime, timezone)
return end ? `${start}–${end}` : start
}
export async function calendarSection(
input: BriefingInput,
date: string,
timezone: string,
): Promise<BriefingSection> {
const accounts = input.calendar?.accounts?.length
? input.calendar.accounts.map((account) => String(account).trim()).filter(Boolean)
: ['personal']
if (!accounts.length) {
return skippedSection(
'calendar',
'Pass calendar.accounts (for example ["personal"]) after connecting Google OAuth.',
)
}
const { start, end } = dayBounds(date, timezone)
const events: CalendarEvent[] = []
const failures: string[] = []
const results = await Promise.all(
accounts.map(async (account) => {
try {
const result = (await listEventsAcrossCalendars({
account,
timeMin: start,
timeMax: end,
singleEvents: true,
orderBy: 'startTime',
maxResults: 100,
calendarMaxResults: 100,
showHiddenCalendars: true,
})) as CalendarListResult
return { account, result, error: null as unknown }
} catch (error) {
return { account, result: null, error: googleCalendarSetupError(error, account) }
}
}),
)
for (const { account, result, error } of results) {
if (!result) {
failures.push(error instanceof Error ? error.message : String(error))
continue
}
for (const failure of result.failures || []) {
failures.push(`${account}: ${failure}`)
}
for (const event of result.items || []) {
events.push({ ...event, accountLabel: account })
}
}
const unusual = events
.filter((event) => !isRecurring(event))
.sort((a, b) => eventSortKey(a).localeCompare(eventSortKey(b)))
const items: BriefingItem[] = unusual.slice(0, 10).map((event) => ({
title: event.summary || 'Calendar event',
detail: `${event.accountLabel || 'calendar'} · ${event.calendarSummary || 'Primary'} · ${formatWhen(event, timezone)}`,
label: event.start?.date ? 'all-day' : timeOnly(event.start?.dateTime, timezone),
severity: 'notice',
href: event.htmlLink,
}))
for (const failure of failures) {
items.push({
title: 'Calendar source warning',
detail: failure,
severity: 'warning',
})
}
const status = failures.length && !unusual.length
? 'warning'
: unusual.length
? 'ok'
: failures.length
? 'warning'
: 'empty'
return {
id: 'calendar',
title: 'Calendar',
status,
summary: unusual.length
? `${unusual.length} non-recurring event${unusual.length === 1 ? '' : 's'} today`
: failures.length
? 'Calendar auth or listing failed'
: 'No non-recurring events today',
items,
}
}