Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/web/src/app/api/google/channels/calendars/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { handler } from "@repo/api/providers/google-calendar/channel";

export const { POST } = handler();
3 changes: 3 additions & 0 deletions apps/web/src/app/api/google/channels/events/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { handler } from "@repo/api/providers/google-calendar/channel";

export const { POST } = handler();
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
".": "./src/root.ts",
"./trpc": "./src/trpc.ts",
"./providers": "./src/providers/index.ts",
"./providers/google-calendar/channel": "./src/providers/google-calendar/channel.ts",
"./interfaces": "./src/interfaces/index.ts",
"./schemas": "./src/schemas/index.ts"
},
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/interfaces/events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Temporal } from "temporal-polyfill";

// table
export interface CalendarEvent {
id: string;
title?: string;
Expand Down Expand Up @@ -28,6 +29,7 @@ export interface CalendarEvent {
recurringEventId?: string;
}

// conference + entry point : are jsonb in the database
export interface ConferenceEntryPoint {
joinUrl: {
label?: string;
Expand Down Expand Up @@ -66,6 +68,7 @@ export interface Conference {
extra?: Record<string, unknown>;
}

// table
export interface Attendee {
id?: string;
email: string;
Expand All @@ -89,6 +92,7 @@ export type Frequency =
| "MONTHLY"
| "YEARLY";

// table
export interface Recurrence {
freq: Frequency;
interval?: number;
Expand Down
15 changes: 10 additions & 5 deletions packages/api/src/providers/calendars/google-calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export class GoogleCalendarProvider implements CalendarProvider {
const events: CalendarEvent[] =
items?.map((event) =>
parseGoogleCalendarEvent({
calendar,
calendarId: calendar.id,
readOnly: calendar.readOnly,
accountId: this.accountId,
event,
defaultTimeZone: timeZone ?? "UTC",
Expand Down Expand Up @@ -148,7 +149,8 @@ export class GoogleCalendarProvider implements CalendarProvider {
});

return parseGoogleCalendarEvent({
calendar,
calendarId: calendar.id,
readOnly: calendar.readOnly,
accountId: this.accountId,
event,
defaultTimeZone: timeZone ?? "UTC",
Expand All @@ -168,7 +170,8 @@ export class GoogleCalendarProvider implements CalendarProvider {
);

return parseGoogleCalendarEvent({
calendar,
calendarId: calendar.id,
readOnly: calendar.readOnly,
accountId: this.accountId,
event: createdEvent,
});
Expand Down Expand Up @@ -239,7 +242,8 @@ export class GoogleCalendarProvider implements CalendarProvider {
);

return parseGoogleCalendarEvent({
calendar,
calendarId: calendar.id,
readOnly: calendar.readOnly,
accountId: this.accountId,
event: updatedEvent,
});
Expand Down Expand Up @@ -273,7 +277,8 @@ export class GoogleCalendarProvider implements CalendarProvider {
});

return parseGoogleCalendarEvent({
calendar: destinationCalendar,
calendarId: destinationCalendar.id,
readOnly: destinationCalendar.readOnly,
accountId: this.accountId,
event: moved,
});
Expand Down
11 changes: 6 additions & 5 deletions packages/api/src/providers/calendars/google-calendar/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Temporal } from "temporal-polyfill";
import {
Attendee,
AttendeeStatus,
Calendar,
CalendarEvent,
Conference,
Recurrence,
Expand Down Expand Up @@ -117,14 +116,16 @@ function parseRecurrence(
}

interface ParsedGoogleCalendarEventOptions {
calendar: Calendar;
calendarId: string;
readOnly: boolean;
accountId: string;
event: GoogleCalendarEvent;
defaultTimeZone?: string;
}

export function parseGoogleCalendarEvent({
calendar,
calendarId,
readOnly,
accountId,
event,
defaultTimeZone = "UTC",
Expand Down Expand Up @@ -157,9 +158,9 @@ export function parseGoogleCalendarEvent({
etag: event.etag,
providerId: "google",
accountId,
calendarId: calendar.id,
calendarId,
readOnly:
calendar.readOnly ||
readOnly ||
["birthday", "focusTime", "outOfOffice", "workingLocation"].includes(
event.eventType ?? "",
),
Expand Down
21 changes: 15 additions & 6 deletions packages/api/src/providers/calendars/microsoft-calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ export class MicrosoftCalendarProvider implements CalendarProvider {

const events = (response.value as MicrosoftEvent[]).map(
(event: MicrosoftEvent) =>
parseMicrosoftEvent({ event, accountId: this.accountId, calendar }),
parseMicrosoftEvent({
event,
accountId: this.accountId,
calendarId: calendar.id,
readOnly: calendar.readOnly,
}),
);

return { events, recurringMasterEvents: [] };
Expand All @@ -151,7 +156,8 @@ export class MicrosoftCalendarProvider implements CalendarProvider {
return parseMicrosoftEvent({
event,
accountId: this.accountId,
calendar,
calendarId: calendar.id,
readOnly: calendar.readOnly,
});
});
}
Expand All @@ -168,7 +174,8 @@ export class MicrosoftCalendarProvider implements CalendarProvider {
return parseMicrosoftEvent({
event: createdEvent,
accountId: this.accountId,
calendar,
calendarId: calendar.id,
readOnly: calendar.readOnly,
});
});
}
Expand Down Expand Up @@ -211,7 +218,8 @@ export class MicrosoftCalendarProvider implements CalendarProvider {
return parseMicrosoftEvent({
event: updatedEvent,
accountId: this.accountId,
calendar,
calendarId: calendar.id,
readOnly: calendar.readOnly,
});
});
}
Expand All @@ -225,7 +233,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider {
async deleteEvent(
calendarId: string,
eventId: string,
sendUpdate: boolean = true,
_sendUpdate: boolean = true,
): Promise<void> {
await this.withErrorHandler("deleteEvent", async () => {
await this.graphClient
Expand All @@ -245,6 +253,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider {
// This could be implemented by creating a new event in destination and deleting the original,
// preserving fields as needed.
const event = await this.event(sourceCalendar, eventId);

return {
...event,
calendarId: destinationCalendar.id,
Expand All @@ -255,7 +264,7 @@ export class MicrosoftCalendarProvider implements CalendarProvider {
}

async responseToEvent(
calendarId: string,
_calendarId: string,
eventId: string,
response: ResponseToEventInput,
): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { Temporal } from "temporal-polyfill";
import type {
Attendee,
AttendeeStatus,
Calendar,
CalendarEvent,
Conference,
} from "../../../interfaces";
Expand Down Expand Up @@ -69,7 +68,8 @@ function parseDate(date: string) {

interface ParseMicrosoftEventOptions {
accountId: string;
calendar: Calendar;
calendarId: string;
readOnly: boolean;
event: MicrosoftEvent;
}

Expand Down Expand Up @@ -102,7 +102,8 @@ function parseResponseStatus(

export function parseMicrosoftEvent({
accountId,
calendar,
calendarId,
readOnly,
event,
}: ParseMicrosoftEventOptions): CalendarEvent {
const { start, end, isAllDay } = event;
Expand Down Expand Up @@ -132,8 +133,8 @@ export function parseMicrosoftEvent({
etag: event["@odata.etag"],
providerId: "microsoft",
accountId,
calendarId: calendar.id,
readOnly: calendar.readOnly,
calendarId,
readOnly,
conference: parseMicrosoftConference(event),
...(responseStatus && { response: { status: responseStatus } }),
metadata: {
Expand Down
115 changes: 115 additions & 0 deletions packages/api/src/providers/google-calendar/channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Account, auth } from "@repo/auth/server";
import { db } from "@repo/db";

import { handleCalendarListMessage } from "./channels/calendars";
import { handleEventsMessage } from "./channels/events";
import { parseHeaders } from "./channels/headers";

interface FindChannelOptions {
channelId: string;
}

async function findChannel({ channelId }: FindChannelOptions) {
return await db.query.channel.findFirst({
where: (table, { eq }) => eq(table.id, channelId),
});
}

export async function withAccessToken(account: Account) {
const { accessToken } = await auth.api.getAccessToken({
body: {
providerId: account.providerId,
accountId: account.id,
userId: account.userId,
},
});

return {
...account,
accessToken: accessToken ?? account.accessToken!,
};
}

interface FindAccountOptions {
accountId: string;
}

async function findAccount({ accountId }: FindAccountOptions) {
const account = await db.query.account.findFirst({
where: (table, { eq }) => eq(table.id, accountId),
});

if (!account) {
throw new Error(`Account ${accountId} not found`);
}

return await withAccessToken(account);
}

export function handler() {
const POST = async (request: Request) => {
const headers = await parseHeaders({ headers: request.headers });

if (!headers) {
return new Response("Missing or invalid headers", { status: 400 });
}

if (headers.resourceState === "sync") {
return new Response("OK", { status: 200 });
}

const channel = await findChannel({ channelId: headers.id });

if (!channel) {
return new Response("Channel not found", { status: 404 });
}

if (!headers.token || headers.token !== channel.token) {
return new Response("Invalid channel token", { status: 401 });
}

if (headers.resourceId !== channel.resourceId) {
return new Response("Mismatched resource", { status: 400 });
}

const account = await findAccount({ accountId: channel.accountId });

if (!account.accessToken) {
return new Response("Failed to obtain a valid access token", {
status: 500,
});
}

if (channel.type === "google.calendar") {
await handleCalendarListMessage({
account,
});
} else if (channel.type === "google.event") {
const calendarId = extractCalendarId(headers.resourceUri);

if (!calendarId) {
return new Response("Missing calendar id", { status: 400 });
}

await handleEventsMessage({ calendarId, account });
} else {
return new Response("Invalid channel type", { status: 400 });
}

return new Response(null, { status: 204 });
};

return {
POST,
};
}

function extractCalendarId(uri: string): string | null {
const match = /\/calendars\/([^/?#]+)/.exec(uri);

if (!match) {
return null;
}

return decodeURIComponent(match[1]!);
}
Loading