diff --git a/packages/server/graphql/mutations/helpers/createGcalEvent.ts b/packages/server/graphql/mutations/helpers/createGcalEvent.ts index 6e936a13cab..c0da25f13fb 100644 --- a/packages/server/graphql/mutations/helpers/createGcalEvent.ts +++ b/packages/server/graphql/mutations/helpers/createGcalEvent.ts @@ -3,23 +3,38 @@ 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 viewerId: string teamId: string + rrule?: string | null dataLoader: DataLoaderWorker } -const createGcalEvent = async (input: Input) => { - const {gcalInput, meetingId, viewerId, dataLoader, teamId} = 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 @@ -57,8 +72,9 @@ const createGcalEvent = async (input: Input) => { } } : undefined + const recurrence = convertRruleToGcal(rrule) - const event = { + const eventInput = { summary: title, description, start: { @@ -69,6 +85,7 @@ const createGcalEvent = async (input: Input) => { dateTime: endDateTime, timeZone }, + recurrence, attendees: attendeesWithEmailObjects, reminders: { useDefault: false, @@ -81,17 +98,59 @@ const createGcalEvent = async (input: Input) => { } try { - await calendar.events.insert({ + const event = await calendar.events.insert({ calendarId: 'primary', - requestBody: event, + requestBody: eventInput, 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: 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 29583f3d755..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,22 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( } IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting, template) - const {error} = await createGcalEvent({gcalInput, meetingId, teamId, viewerId, dataLoader}) + const {error, gcalSeriesId} = await createGcalEvent({ + gcalInput, + meetingId, + teamId, + viewerId, + 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() +}