d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
import { generateId } from '@sim/utils/id'
|
|
import type { GoogleCalendarEventRequestBody } from '@/tools/google_calendar/types'
|
|
|
|
type EventDateTime = GoogleCalendarEventRequestBody['start']
|
|
|
|
const DEFAULT_TIME_ZONE = 'America/Los_Angeles'
|
|
const TZ_OFFSET_PATTERN = /([+-]\d{2}:?\d{2}|Z)$/
|
|
|
|
/**
|
|
* Build a Google Calendar event date/time object from a user-supplied value.
|
|
*
|
|
* A date-only value (e.g. `2025-06-03`) produces an all-day `{ date }` object.
|
|
* A datetime value produces `{ dateTime, timeZone? }`. An explicitly provided
|
|
* timezone always wins. Otherwise a default zone is attached only for "naive"
|
|
* datetimes that carry no UTC offset — when an offset is present it is authoritative
|
|
* and is never overridden with a guessed zone, which would misalign the time.
|
|
*
|
|
* For recurring events the Calendar API requires a named `timeZone` on start/end;
|
|
* callers should pass the user's timezone explicitly (an RFC3339 offset alone is
|
|
* insufficient to expand a recurrence across DST).
|
|
*/
|
|
export function buildEventDateTime(value: string, timeZone: string | undefined): EventDateTime {
|
|
const isDateOnly = !value.includes('T')
|
|
if (isDateOnly) {
|
|
return { date: value }
|
|
}
|
|
|
|
const hasOffset = TZ_OFFSET_PATTERN.test(value)
|
|
const result: EventDateTime = { dateTime: value }
|
|
if (timeZone) {
|
|
result.timeZone = timeZone
|
|
} else if (!hasOffset) {
|
|
result.timeZone = DEFAULT_TIME_ZONE
|
|
}
|
|
return result
|
|
}
|
|
|
|
/** Normalize a comma/newline-separated string or array of attendee emails into `[{ email }]`. */
|
|
export function normalizeAttendees(
|
|
attendees: string | string[] | undefined
|
|
): Array<{ email: string }> {
|
|
if (!attendees) return []
|
|
|
|
const list = Array.isArray(attendees)
|
|
? attendees
|
|
: attendees.split(',').map((email) => email.trim())
|
|
|
|
return list.filter((email) => email.length > 0).map((email) => ({ email }))
|
|
}
|
|
|
|
/**
|
|
* Recurring events require a named `timeZone` on their timed start/end — the Calendar API
|
|
* rejects them otherwise, and an RFC3339 offset is not a substitute (an IANA zone cannot be
|
|
* derived from a fixed offset). Throws a clear error so we fail fast with guidance instead of
|
|
* silently guessing a zone (which would misalign the recurrence) or sending an invalid request.
|
|
* All-day recurring events (date-only values) do not need a timezone and are allowed.
|
|
*/
|
|
export function assertRecurringTimeZone(
|
|
dateTimes: Array<string | undefined>,
|
|
timeZone: string | undefined
|
|
): void {
|
|
if (timeZone) return
|
|
const hasTimedValue = dateTimes.some((value) => value?.includes('T'))
|
|
if (hasTimedValue) {
|
|
throw new Error(
|
|
'Recurring events require a time zone. Provide the timeZone parameter (an IANA name, e.g. America/New_York).'
|
|
)
|
|
}
|
|
}
|
|
|
|
/** Normalize recurrence rules (single string, newline-separated string, or array) into an array. */
|
|
export function normalizeRecurrence(recurrence: string | string[] | undefined): string[] {
|
|
if (!recurrence) return []
|
|
|
|
const list = Array.isArray(recurrence) ? recurrence : recurrence.split('\n')
|
|
|
|
return list.map((rule) => rule.trim()).filter((rule) => rule.length > 0)
|
|
}
|
|
|
|
/** Build a `conferenceData.createRequest` payload that asks Google to attach a Meet link. */
|
|
export function buildGoogleMeetConferenceData(): GoogleCalendarEventRequestBody['conferenceData'] {
|
|
return {
|
|
createRequest: {
|
|
requestId: generateId(),
|
|
conferenceSolutionKey: { type: 'hangoutsMeet' },
|
|
},
|
|
}
|
|
}
|