From 8ebf58fe738129309eafa5ff84a5c6e1c42143ea Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Wed, 24 Jan 2024 18:53:30 +0100 Subject: [PATCH] feat: Add recurring GCal meeting series --- .../mutations/helpers/createGcalEvent.ts | 77 +++++++++++++++---- .../public/mutations/startRetrospective.ts | 11 ++- .../mutations/updateRecurrenceSettings.ts | 12 +++ ...06021181176_addGCalEventToMeetingSeries.ts | 22 ++++++ 4 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts diff --git a/packages/server/graphql/mutations/helpers/createGcalEvent.ts b/packages/server/graphql/mutations/helpers/createGcalEvent.ts index f8a056d7c6d..c0da25f13fb 100644 --- a/packages/server/graphql/mutations/helpers/createGcalEvent.ts +++ b/packages/server/graphql/mutations/helpers/createGcalEvent.ts @@ -3,11 +3,23 @@ import makeAppURL from 'parabol-client/utils/makeAppURL' import appOrigin from '../../../appOrigin' import {DataLoaderWorker} from '../../graphql' import standardError from '../../../utils/standardError' -import {CreateGcalEventInput} from '../../public/resolverTypes' +import {CreateGcalEventInput, StandardMutationError} from '../../public/resolverTypes' const emailRemindMinsBeforeMeeting = 24 * 60 const popupRemindMinsBeforeMeeting = 10 +const convertRruleToGcal = (rrule: string | null | undefined) => { + const recurrence = rrule + ? [ + rrule + .split('\n') + .filter((line) => !line.startsWith('DTSTART') && !line.startsWith('DTEND')) + .join('\n') + ] + : [] + return recurrence +} + type Input = { gcalInput?: CreateGcalEventInput | null meetingId: string @@ -17,10 +29,12 @@ type Input = { dataLoader: DataLoaderWorker } -const createGcalEvent = async (input: Input) => { +const createGcalEvent = async ( + input: Input +): Promise<{gcalSeriesId?: string; error?: StandardMutationError}> => { const {gcalInput, meetingId, viewerId, dataLoader, teamId, rrule} = input if (!gcalInput) { - return {error: null} + return {} } const {startTimestamp, endTimestamp, title, timeZone, invitees, videoType} = gcalInput @@ -58,15 +72,7 @@ const createGcalEvent = async (input: Input) => { } } : undefined - const recurrence = rrule - ? [ - rrule - .split('\n') - .filter((line) => !line.startsWith('DTSTART') && !line.startsWith('DTEND')) - .join('\n') - ] - : undefined - console.log('GEORG RECURRENCE', recurrence) + const recurrence = convertRruleToGcal(rrule) const eventInput = { summary: title, @@ -97,13 +103,54 @@ const createGcalEvent = async (input: Input) => { requestBody: eventInput, conferenceDataVersion: 1 }) - console.log('GEORG EVENT', event) + return {gcalSeriesId: event.data.id ?? undefined} } catch (err) { const error = err instanceof Error ? err : new Error('Unable to create event in gcal') return standardError(error, {userId: viewerId}) } - return { - error: null +} + +export type UpdateGcalSeriesInput = { + gcalSeriesId: string + title?: string + rrule: string | null + userId: string + teamId: string + dataLoader: DataLoaderWorker +} +export const updateGcalSeries = async (input: UpdateGcalSeriesInput) => { + const {gcalSeriesId, title, rrule, userId, teamId, dataLoader} = input + + const gcalAuth = await dataLoader.get('freshGcalAuth').load({teamId, userId}) + if (!gcalAuth) { + return standardError(new Error('Could not retrieve Google Calendar auth'), {userId}) + } + const {accessToken: access_token, refreshToken: refresh_token, expiresAt} = gcalAuth + const CLIENT_ID = process.env.GOOGLE_OAUTH_CLIENT_ID + const CLIENT_SECRET = process.env.GOOGLE_OAUTH_CLIENT_SECRET + const REDIRECT_URI = appOrigin + + const expiry_date = expiresAt ? expiresAt.getTime() : undefined + + const oauth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI) + oauth2Client.setCredentials({access_token, refresh_token, expiry_date}) + const calendar = google.calendar({version: 'v3', auth: oauth2Client}) + const recurrence = convertRruleToGcal(rrule) + + try { + const event = await calendar.events.patch({ + calendarId: 'primary', + eventId: gcalSeriesId, + requestBody: { + recurrence, + summary: title + }, + conferenceDataVersion: 1 + }) + return {gcalSeriesId: event.data.id ?? undefined} + } catch (err) { + const error = err instanceof Error ? err : new Error('Unable to create event in gcal') + return standardError(error, {userId}) } } diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index ac10b542ef4..82f9f4a3a03 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -16,6 +16,7 @@ import {IntegrationNotifier} from '../../mutations/helpers/notifications/Integra import {startNewMeetingSeries} from './updateRecurrenceSettings' import safeCreateRetrospective from '../../mutations/helpers/safeCreateRetrospective' import {createMeetingSeriesTitle} from '../../mutations/helpers/createMeetingSeriesTitle' +import getKysely from '../../../postgres/getKysely' const startRetrospective: MutationResolvers['startRetrospective'] = async ( _source, @@ -118,7 +119,7 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( } IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting, template) - const {error} = await createGcalEvent({ + const {error, gcalSeriesId} = await createGcalEvent({ gcalInput, meetingId, teamId, @@ -126,6 +127,14 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( rrule: meetingSeries?.recurrenceRule, dataLoader }) + if (meetingSeries && gcalSeriesId) { + const pg = getKysely() + await pg + .updateTable('MeetingSeries') + .set({gcalSeriesId}) + .where('id', '=', meetingSeries.id) + .execute() + } const data = {teamId, meetingId, hasGcalError: !!error?.message} publish(SubscriptionChannel.TEAM, teamId, 'StartRetrospectiveSuccess', data, subOptions) return data diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 335e08a1e84..590f85e7a27 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -12,6 +12,7 @@ import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' import {MutationResolvers} from '../resolverTypes' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {updateGcalSeries} from '../../mutations/helpers/createGcalEvent' export const startNewMeetingSeries = async ( meeting: { @@ -138,6 +139,7 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = if (meeting.meetingSeriesId) { const meetingSeries = await dataLoader.get('meetingSeries').loadNonNull(meeting.meetingSeriesId) + const {gcalSeriesId, teamId, facilitatorId} = meetingSeries if (!recurrenceSettings.rrule) { await stopMeetingSeries(meetingSeries) @@ -146,6 +148,16 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = await updateMeetingSeries(meetingSeries, recurrenceSettings.rrule) analytics.recurrenceStarted(viewer, meetingSeries) } + if (gcalSeriesId) { + await updateGcalSeries({ + gcalSeriesId, + title: recurrenceSettings.name ?? undefined, + rrule: recurrenceSettings.rrule?.toString() ?? null, + teamId, + userId: facilitatorId, + dataLoader + }) + } if (recurrenceSettings.name) { await updateMeetingSeriesQuery({title: recurrenceSettings.name}, meetingSeries.id) diff --git a/packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts b/packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts new file mode 100644 index 00000000000..919dda62a87 --- /dev/null +++ b/packages/server/postgres/migrations/1706021181176_addGCalEventToMeetingSeries.ts @@ -0,0 +1,22 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "MeetingSeries" + ADD COLUMN IF NOT EXISTS "gcalSeriesId" VARCHAR(100); + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "MeetingSeries" + DROP COLUMN IF EXISTS "gcalSeriesId"; + `) + await client.end() +}