From 033e640f4a748068e4ee7000e2cd58923f5a4c8d Mon Sep 17 00:00:00 2001 From: quu-ack Date: Thu, 20 Nov 2025 14:35:41 -0500 Subject: [PATCH] feat(integrations/googlecalendar): version 2.0 --- .../googlecalendar/definitions/actions.ts | 96 ++++++++++++++++++- .../definitions/configuration.ts | 7 ++ .../definitions/entities/event.ts | 38 ++++++++ integrations/googlecalendar/hub.md | 12 +++ .../googlecalendar/integration.definition.ts | 33 +------ integrations/googlecalendar/package.json | 8 +- .../implementations/check-availability.ts | 90 +++++++++++++++++ .../interfaces/event-create.ts | 6 -- .../interfaces/event-delete.ts | 6 -- .../implementations/interfaces/event-list.ts | 13 --- .../implementations/interfaces/event-read.ts | 6 -- .../interfaces/event-update.ts | 6 -- .../googlecalendar/src/actions/index.ts | 14 +-- .../src/google-api/google-client.ts | 23 +++++ .../src/google-api/mapping/request-mapping.ts | 34 ++++++- .../google-api/mapping/response-mapping.ts | 57 +++++++---- .../src/google-api/oauth-client.ts | 6 +- .../googlecalendar/src/google-api/types.d.ts | 20 ---- .../googlecalendar/src/google-api/types.ts | 34 +++++++ 19 files changed, 374 insertions(+), 135 deletions(-) create mode 100644 integrations/googlecalendar/src/actions/implementations/check-availability.ts delete mode 100644 integrations/googlecalendar/src/actions/implementations/interfaces/event-create.ts delete mode 100644 integrations/googlecalendar/src/actions/implementations/interfaces/event-delete.ts delete mode 100644 integrations/googlecalendar/src/actions/implementations/interfaces/event-list.ts delete mode 100644 integrations/googlecalendar/src/actions/implementations/interfaces/event-read.ts delete mode 100644 integrations/googlecalendar/src/actions/implementations/interfaces/event-update.ts delete mode 100644 integrations/googlecalendar/src/google-api/types.d.ts create mode 100644 integrations/googlecalendar/src/google-api/types.ts diff --git a/integrations/googlecalendar/definitions/actions.ts b/integrations/googlecalendar/definitions/actions.ts index e9533e928a4..e66067d07dd 100644 --- a/integrations/googlecalendar/definitions/actions.ts +++ b/integrations/googlecalendar/definitions/actions.ts @@ -44,7 +44,29 @@ export const actions = { description: 'Creates a new event in the calendar.', input: { schema: entities.Event.schema - .omit({ eventType: true, htmlLink: true, id: true }) + .omit({ eventType: true, htmlLink: true, id: true, conferenceLink: true }) + .extend({ + attendees: z + .array( + z.object({ + email: z.string().email().title('Email').describe('The email address of the attendee.'), + displayName: z + .string() + .title('Display Name') + .optional() + .describe('The name of the attendee. Optional.'), + optional: z + .boolean() + .title('Optional Attendee') + .optional() + .default(false) + .describe('Whether this is an optional attendee. Optional. The default is False.'), + }) + ) + .title('Attendees') + .optional() + .describe('List of attendees for the event. Email invitations will be sent automatically.'), + }) .title('New Event') .describe('The definition of the new event.'), }, @@ -57,8 +79,24 @@ export const actions = { title: 'Update Event', description: 'Updates an existing event in the calendar. Omitted properties are left unchanged.', input: { - schema: entities.Event.schema.omit({ eventType: true, htmlLink: true }).extend({ + schema: entities.Event.schema.omit({ eventType: true, htmlLink: true, conferenceLink: true }).extend({ id: z.string().title('Event ID').describe('The ID of the calendar event to update.'), + attendees: z + .array( + z.object({ + email: z.string().email().title('Email').describe('The email address of the attendee.'), + displayName: z.string().title('Display Name').optional().describe('The name of the attendee. Optional.'), + optional: z + .boolean() + .title('Optional Attendee') + .optional() + .default(false) + .describe('Whether this is an optional attendee. Optional. The default is False.'), + }) + ) + .title('Attendees') + .optional() + .describe('List of attendees for the event.'), }), }, output: { @@ -78,4 +116,58 @@ export const actions = { schema: z.object({}), }, }, + checkAvailability: { + title: 'Check Availability', + description: 'Checks calendar availability and returns free time slots for the specified date range.', + input: { + schema: z.object({ + timeMin: z + .string() + .title('Start Time') + .describe('Start of the time range to check (ISO 8601 format, e.g., "2025-11-05T09:00:00-04:00").'), + timeMax: z + .string() + .title('End Time') + .describe('End of the time range to check (ISO 8601 format, e.g., "2025-11-05T17:00:00-04:00").'), + slotDurationMinutes: z + .number() + .min(15) + .max(480) + .default(45) + .title('Slot Duration (minutes)') + .describe('Duration of each time slot in minutes. Defaults to 45 minutes.'), + timezone: z + .string() + .default('America/Toronto') + .title('Timezone') + .describe('Timezone for formatting output (e.g., "America/Toronto"). Defaults to America/Toronto.'), + }), + }, + output: { + schema: z.object({ + freeSlots: z + .array( + z.object({ + start: z.string().title('Start Time').describe('ISO 8601 formatted start time of the free slot.'), + end: z.string().title('End Time').describe('ISO 8601 formatted end time of the free slot.'), + }) + ) + .title('Free Slots') + .describe('Array of available time slots in ISO format.'), + formattedFreeSlots: z + .array(z.string()) + .title('Formatted Free Slots') + .describe('Array of human-readable formatted free slots (e.g., "9:00 AM – 9:45 AM").'), + busySlots: z + .array( + z.object({ + start: z.string().title('Start Time').describe('ISO 8601 formatted start time of the busy slot.'), + end: z.string().title('End Time').describe('ISO 8601 formatted end time of the busy slot.'), + }) + ) + .title('Busy Slots') + .describe('Array of busy time slots in ISO format.'), + }), + }, + }, } as const satisfies sdk.IntegrationDefinitionProps['actions'] diff --git a/integrations/googlecalendar/definitions/configuration.ts b/integrations/googlecalendar/definitions/configuration.ts index fa604350cbf..b17267c18eb 100644 --- a/integrations/googlecalendar/definitions/configuration.ts +++ b/integrations/googlecalendar/definitions/configuration.ts @@ -33,6 +33,13 @@ export const configurations = { .title('Service account email') .email() .describe('The client email from the Google service account. You can get it from the downloaded JSON file.'), + impersonateEmail: z + .string() + .title('Impersonate email') + .email() + .describe( + "The email of the user to impersonate. This is the email of the user you want to impersonate. It's mandatory to invite people or create meetings." + ), }), }, } as const satisfies sdk.IntegrationDefinitionProps['configurations'] diff --git a/integrations/googlecalendar/definitions/entities/event.ts b/integrations/googlecalendar/definitions/entities/event.ts index 3ebc8ced206..cbf585c4865 100644 --- a/integrations/googlecalendar/definitions/entities/event.ts +++ b/integrations/googlecalendar/definitions/entities/event.ts @@ -72,6 +72,44 @@ export namespace Event { .optional() .default('default') .describe('Visibility of the event. Optional. The default value is "default".'), + enableGoogleMeet: z + .boolean() + .title('Enable Google Meet') + .optional() + .default(false) + .describe('Whether to enable Google Meet for the event. Optional. The default value is false.'), + sendNotifications: z + .boolean() + .title('Send Notifications') + .optional() + .default(true) + .describe('Whether to send notifications to attendees. Optional. The default value is true.'), + attendees: z + .array( + z.object({ + email: z.string().title('Email').describe('The email address of the attendee.'), + displayName: z.string().title('Display Name').optional().describe('The name of the attendee. Optional.'), + optional: z + .boolean() + .title('Optional Attendee') + .optional() + .default(false) + .describe('Whether this is an optional attendee. Optional. The default is False.'), + responseStatus: z + .enum(['needsAction', 'declined', 'tentative', 'accepted']) + .title('Response Status') + .optional() + .describe("The attendee's response status. Read-only when creating events."), + }) + ) + .title('Attendees') + .optional() + .describe('List of attendees for the event. Email invitations will be sent automatically.'), + conferenceLink: z + .string() + .title('Conference Link') + .optional() + .describe('The Google Meet link for the event. This is automatically generated when enableGoogleMeet is true.'), } as const export const schema = z.object(_fields) diff --git a/integrations/googlecalendar/hub.md b/integrations/googlecalendar/hub.md index 30fa98022e8..99c285a009b 100644 --- a/integrations/googlecalendar/hub.md +++ b/integrations/googlecalendar/hub.md @@ -15,6 +15,18 @@ If you are migrating from version `0.x` to `1.x`, please note the following chan > When creating or updating events, the ISO 8601 date-time format is now fully supported and it is no longer necessary to input dates as RFC 3339 strings. +## Migrating from version `1.x` to `2.x` + +If you are migrating from version `1.x` to `2.x`, please note the following changes: + +> The integration has been refactored to remove the generic CRUD interface actions and **de-duplicate redundant actions**. The action names have changed from the previous interface-based naming convention. You will need to update any workflows or code that references the old action names: +> +> - `eventCreate` → `createEvent` +> - `eventDelete` → `deleteEvent` +> - `eventList` → `listEvents` +> - `eventUpdate` → `updateEvent` +> - `eventRead` has been removed (use `listEvents`instead) + ## Configuration ### Automatic configuration with OAuth (recommended) diff --git a/integrations/googlecalendar/integration.definition.ts b/integrations/googlecalendar/integration.definition.ts index 2453a9eb4fe..1a131c449e4 100644 --- a/integrations/googlecalendar/integration.definition.ts +++ b/integrations/googlecalendar/integration.definition.ts @@ -1,14 +1,9 @@ import * as sdk from '@botpress/sdk' -import creatable from './bp_modules/creatable' -import deletable from './bp_modules/deletable' -import listable from './bp_modules/listable' -import readable from './bp_modules/readable' -import updatable from './bp_modules/updatable' import { actions, entities, configuration, configurations, identifier, events, secrets, states } from './definitions' export default new sdk.IntegrationDefinition({ name: 'googlecalendar', - version: '1.0.3', + version: '2.0.0', description: 'Sync with your calendar to manage events, appointments, and schedules directly within the chatbot.', title: 'Google Calendar', readme: 'hub.md', @@ -21,30 +16,4 @@ export default new sdk.IntegrationDefinition({ events, secrets, states, - __advanced: { - useLegacyZuiTransformer: true, - }, }) - .extend(listable, ({ entities }) => ({ - entities: { item: entities.event }, - actions: { list: { name: 'eventList' } }, - })) - .extend(creatable, ({ entities }) => ({ - entities: { item: entities.event }, - actions: { create: { name: 'eventCreate' } }, - events: { created: { name: 'eventCreated' } }, - })) - .extend(readable, ({ entities }) => ({ - entities: { item: entities.event }, - actions: { read: { name: 'eventRead' } }, - })) - .extend(updatable, ({ entities }) => ({ - entities: { item: entities.event }, - actions: { update: { name: 'eventUpdate' } }, - events: { updated: { name: 'eventUpdated' } }, - })) - .extend(deletable, ({ entities }) => ({ - entities: { item: entities.event }, - actions: { delete: { name: 'eventDelete' } }, - events: { deleted: { name: 'eventDeleted' } }, - })) diff --git a/integrations/googlecalendar/package.json b/integrations/googlecalendar/package.json index ba76a79731a..9c8e1ec7e34 100644 --- a/integrations/googlecalendar/package.json +++ b/integrations/googlecalendar/package.json @@ -22,11 +22,5 @@ "@botpresshub/updatable": "workspace:*", "@sentry/cli": "^2.39.1" }, - "bpDependencies": { - "creatable": "../../interfaces/creatable", - "deletable": "../../interfaces/deletable", - "listable": "../../interfaces/listable", - "readable": "../../interfaces/readable", - "updatable": "../../interfaces/updatable" - } + "bpDependencies": {} } diff --git a/integrations/googlecalendar/src/actions/implementations/check-availability.ts b/integrations/googlecalendar/src/actions/implementations/check-availability.ts new file mode 100644 index 00000000000..06ed8a4a97f --- /dev/null +++ b/integrations/googlecalendar/src/actions/implementations/check-availability.ts @@ -0,0 +1,90 @@ +import { wrapAction } from '../action-wrapper' + +type TimeSlot = { + start: Date + end: Date +} + +function _addMinutes(date: Date, minutes: number): Date { + return new Date(date.getTime() + minutes * 60_000) +} + +function _doTimeRangesOverlap(start1: Date, end1: Date, start2: Date, end2: Date): boolean { + return start1 < end2 && end1 > start2 +} + +function _formatTime(date: Date, timezone: string): string { + return date.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: '2-digit', + hour12: true, + timeZone: timezone, + }) +} + +function generateTimeSlots(startTime: Date, endTime: Date, slotDurationMinutes: number): TimeSlot[] { + const slots: TimeSlot[] = [] + let currentSlotStart = new Date(startTime) + + while (currentSlotStart < endTime) { + const currentSlotEnd = _addMinutes(currentSlotStart, slotDurationMinutes) + + if (currentSlotEnd > endTime) { + break + } + + slots.push({ + start: new Date(currentSlotStart), + end: currentSlotEnd, + }) + + currentSlotStart = _addMinutes(currentSlotStart, slotDurationMinutes) + } + + return slots +} + +function filterAvailableSlots(allSlots: TimeSlot[], busyTimes: TimeSlot[]): TimeSlot[] { + return allSlots.filter((slot) => { + const hasConflict = busyTimes.some((busySlot) => + _doTimeRangesOverlap(slot.start, slot.end, busySlot.start, busySlot.end) + ) + return !hasConflict + }) +} + +export const checkAvailability = wrapAction( + { actionName: 'checkAvailability', errorMessageWhenFailed: 'Failed to check calendar availability' }, + async ({ googleClient }, input) => { + const { busySlots } = await googleClient.getBusySlots({ + timeMin: input.timeMin, + timeMax: input.timeMax, + }) + + const searchStartTime = new Date(input.timeMin) + const searchEndTime = new Date(input.timeMax) + + const busyTimeSlots: TimeSlot[] = busySlots.map((slot) => ({ + start: new Date(slot.start), + end: new Date(slot.end), + })) + + const allPossibleSlots = generateTimeSlots(searchStartTime, searchEndTime, input.slotDurationMinutes || 45) + const availableSlots = filterAvailableSlots(allPossibleSlots, busyTimeSlots) + const freeSlotsISO = availableSlots.map((slot) => ({ + start: slot.start.toISOString(), + end: slot.end.toISOString(), + })) + + const freeSlotsHumanReadable = availableSlots.map( + (slot) => + `${_formatTime(slot.start, input.timezone || 'America/Toronto')} – ${_formatTime(slot.end, input.timezone || 'America/Toronto')}` + ) + + return { + freeSlots: freeSlotsISO, + formattedFreeSlots: freeSlotsHumanReadable, + busySlots, + } + } +) diff --git a/integrations/googlecalendar/src/actions/implementations/interfaces/event-create.ts b/integrations/googlecalendar/src/actions/implementations/interfaces/event-create.ts deleted file mode 100644 index e9e87fadd80..00000000000 --- a/integrations/googlecalendar/src/actions/implementations/interfaces/event-create.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { wrapAction } from '../../action-wrapper' - -export const eventCreate = wrapAction( - { actionName: 'eventCreate', errorMessageWhenFailed: 'Failed to create calendar event' }, - async ({ googleClient }, { item }) => ({ item: await googleClient.createEvent({ event: item }) }) -) diff --git a/integrations/googlecalendar/src/actions/implementations/interfaces/event-delete.ts b/integrations/googlecalendar/src/actions/implementations/interfaces/event-delete.ts deleted file mode 100644 index 7f4bc46009c..00000000000 --- a/integrations/googlecalendar/src/actions/implementations/interfaces/event-delete.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { wrapAction } from '../../action-wrapper' - -export const eventDelete = wrapAction( - { actionName: 'eventDelete', errorMessageWhenFailed: 'Failed to delete calendar event' }, - async ({ googleClient }, { id }) => await googleClient.deleteEvent({ eventId: id }) -) diff --git a/integrations/googlecalendar/src/actions/implementations/interfaces/event-list.ts b/integrations/googlecalendar/src/actions/implementations/interfaces/event-list.ts deleted file mode 100644 index 9527eadec6b..00000000000 --- a/integrations/googlecalendar/src/actions/implementations/interfaces/event-list.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { wrapAction } from '../../action-wrapper' - -export const eventList = wrapAction( - { actionName: 'eventList', errorMessageWhenFailed: 'Failed to list calendar events' }, - async ({ googleClient }, { nextToken }) => { - const { events, nextPageToken } = await googleClient.listEvents({ - fetchAmount: 100, - minDate: new Date().toISOString(), - pageToken: nextToken, - }) - return { items: events, meta: { nextToken: nextPageToken } } - } -) diff --git a/integrations/googlecalendar/src/actions/implementations/interfaces/event-read.ts b/integrations/googlecalendar/src/actions/implementations/interfaces/event-read.ts deleted file mode 100644 index cddccf8eb40..00000000000 --- a/integrations/googlecalendar/src/actions/implementations/interfaces/event-read.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { wrapAction } from '../../action-wrapper' - -export const eventRead = wrapAction( - { actionName: 'eventRead', errorMessageWhenFailed: 'Failed to delete calendar event' }, - async ({ googleClient }, { id }) => ({ item: await googleClient.getEvent({ eventId: id }) }) -) diff --git a/integrations/googlecalendar/src/actions/implementations/interfaces/event-update.ts b/integrations/googlecalendar/src/actions/implementations/interfaces/event-update.ts deleted file mode 100644 index 0ce2062d1e1..00000000000 --- a/integrations/googlecalendar/src/actions/implementations/interfaces/event-update.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { wrapAction } from '../../action-wrapper' - -export const eventUpdate = wrapAction( - { actionName: 'eventUpdate', errorMessageWhenFailed: 'Failed to update calendar event' }, - async ({ googleClient }, { item }) => ({ item: await googleClient.updateEvent({ event: item }) }) -) diff --git a/integrations/googlecalendar/src/actions/index.ts b/integrations/googlecalendar/src/actions/index.ts index 8faff4fdfdb..4363515735f 100644 --- a/integrations/googlecalendar/src/actions/index.ts +++ b/integrations/googlecalendar/src/actions/index.ts @@ -1,24 +1,14 @@ +import { checkAvailability } from './implementations/check-availability' import { createEvent } from './implementations/create-event' import { deleteEvent } from './implementations/delete-event' -import { eventCreate } from './implementations/interfaces/event-create' -import { eventDelete } from './implementations/interfaces/event-delete' -import { eventList } from './implementations/interfaces/event-list' -import { eventRead } from './implementations/interfaces/event-read' -import { eventUpdate } from './implementations/interfaces/event-update' - import { listEvents } from './implementations/list-events' import { updateEvent } from './implementations/update-event' import * as bp from '.botpress' export const actions = { - eventCreate, - eventDelete, - eventList, - eventRead, - eventUpdate, - createEvent, deleteEvent, listEvents, updateEvent, + checkAvailability, } as const satisfies bp.IntegrationProps['actions'] diff --git a/integrations/googlecalendar/src/google-api/google-client.ts b/integrations/googlecalendar/src/google-api/google-client.ts index 0f43ef06488..d2964c1c8d0 100644 --- a/integrations/googlecalendar/src/google-api/google-client.ts +++ b/integrations/googlecalendar/src/google-api/google-client.ts @@ -48,6 +48,8 @@ export class GoogleClient { const { data } = await this._calendarClient.events.insert({ calendarId: this._calendarId, requestBody: RequestMapping.mapCreateEvent(event), + conferenceDataVersion: event.enableGoogleMeet ? 1 : undefined, + sendUpdates: event.sendNotifications !== false && event.attendees && event.attendees.length > 0 ? 'all' : 'none', }) return ResponseMapping.mapEvent(data) @@ -59,6 +61,8 @@ export class GoogleClient { calendarId: this._calendarId, eventId: event.id, requestBody: RequestMapping.mapUpdateEvent(event), + conferenceDataVersion: event.enableGoogleMeet ? 1 : undefined, + sendUpdates: event.sendNotifications !== false && event.attendees && event.attendees.length > 0 ? 'all' : 'none', }) return ResponseMapping.mapEvent(data) @@ -104,4 +108,23 @@ export class GoogleClient { nextPageToken: ResponseMapping.mapNextToken(data.nextPageToken), } } + + @handleErrors('Failed to check calendar availability') + public async getBusySlots({ timeMin, timeMax }: { timeMin: string; timeMax: string }) { + const { data } = await this._calendarClient.freebusy.query({ + requestBody: { + timeMin, + timeMax, + }, + }) + + const calendarBusy = data.calendars?.[this._calendarId]?.busy || [] + + return { + busySlots: calendarBusy.map((slot) => ({ + start: slot.start || '', + end: slot.end || '', + })), + } + } } diff --git a/integrations/googlecalendar/src/google-api/mapping/request-mapping.ts b/integrations/googlecalendar/src/google-api/mapping/request-mapping.ts index 65d89d8e714..562c2e00947 100644 --- a/integrations/googlecalendar/src/google-api/mapping/request-mapping.ts +++ b/integrations/googlecalendar/src/google-api/mapping/request-mapping.ts @@ -1,13 +1,37 @@ import type { calendar_v3 } from 'googleapis' +import { randomUUID } from 'node:crypto' import { CreateEventRequest, UpdateEventRequest } from '../types' import { IsoToRFC3339 } from './datetime-utils/iso-to-rfc3339' export namespace RequestMapping { - export const mapCreateEvent = (event: CreateEventRequest): calendar_v3.Schema$Event => ({ - ...event, - start: _mapDateTime(event.startDateTime), - end: _mapDateTime(event.endDateTime), - }) + export const mapCreateEvent = (event: CreateEventRequest): calendar_v3.Schema$Event => { + const mappedEvent: calendar_v3.Schema$Event = { + ...event, + start: _mapDateTime(event.startDateTime), + end: _mapDateTime(event.endDateTime), + } + + if (event.enableGoogleMeet) { + mappedEvent.conferenceData = { + createRequest: { + requestId: randomUUID(), + conferenceSolutionKey: { + type: 'hangoutsMeet', + }, + }, + } + } + + if (event.attendees && event.attendees.length > 0) { + mappedEvent.attendees = event.attendees.map((attendee) => ({ + email: attendee.email, + displayName: attendee.displayName, + optional: attendee.optional ?? false, + })) + } + + return mappedEvent + } export const mapUpdateEvent: (event: UpdateEventRequest) => calendar_v3.Schema$Event = mapCreateEvent diff --git a/integrations/googlecalendar/src/google-api/mapping/response-mapping.ts b/integrations/googlecalendar/src/google-api/mapping/response-mapping.ts index 2ab37c70b2d..de88510347d 100644 --- a/integrations/googlecalendar/src/google-api/mapping/response-mapping.ts +++ b/integrations/googlecalendar/src/google-api/mapping/response-mapping.ts @@ -2,22 +2,47 @@ import type { calendar_v3 } from 'googleapis' import { Event } from '../types' export namespace ResponseMapping { - export const mapEvent = (event: calendar_v3.Schema$Event): Event => ({ - id: event.id ?? '', - description: event.description ?? '', - summary: event.summary ?? '', - location: event.location ?? '', - startDateTime: event.start?.dateTime ?? '', - endDateTime: event.end?.dateTime ?? '', - eventType: _mapEventType(event.eventType), - guestsCanInviteOthers: event.guestsCanInviteOthers ?? true, - guestsCanSeeOtherGuests: event.guestsCanSeeOtherGuests ?? true, - htmlLink: event.htmlLink ?? '', - recurrence: event.recurrence ?? [], - status: _mapEventStatus(event.status), - colorId: event.colorId ?? '', - visibility: _mapEventVisibility(event.visibility), - }) + export const mapEvent = (event: calendar_v3.Schema$Event): Event => { + const conferenceLink = + event.hangoutLink ?? event.conferenceData?.entryPoints?.find((entry) => entry.entryPointType === 'video')?.uri + + const attendees = event.attendees?.map((attendee) => ({ + email: attendee.email ?? '', + displayName: attendee.displayName ?? undefined, + optional: attendee.optional ?? false, + responseStatus: _mapEnum({ + value: attendee.responseStatus, + mapping: { + needsAction: 'needsAction', + declined: 'declined', + tentative: 'tentative', + accepted: 'accepted', + }, + defaultValue: 'needsAction', + }), + })) + + return { + id: event.id ?? '', + description: event.description ?? '', + summary: event.summary ?? '', + location: event.location ?? '', + startDateTime: event.start?.dateTime ?? '', + endDateTime: event.end?.dateTime ?? '', + eventType: _mapEventType(event.eventType), + guestsCanInviteOthers: event.guestsCanInviteOthers ?? true, + guestsCanSeeOtherGuests: event.guestsCanSeeOtherGuests ?? true, + htmlLink: event.htmlLink ?? '', + recurrence: event.recurrence ?? [], + status: _mapEventStatus(event.status), + colorId: event.colorId ?? '', + visibility: _mapEventVisibility(event.visibility), + enableGoogleMeet: !!event.conferenceData, + sendNotifications: true, + conferenceLink: conferenceLink ?? undefined, + attendees: attendees && attendees.length > 0 ? attendees : undefined, + } + } export const mapEvents = (events?: calendar_v3.Schema$Event[]): Event[] => events?.map(mapEvent) ?? [] diff --git a/integrations/googlecalendar/src/google-api/oauth-client.ts b/integrations/googlecalendar/src/google-api/oauth-client.ts index fd53c9c5a32..83bac10819a 100644 --- a/integrations/googlecalendar/src/google-api/oauth-client.ts +++ b/integrations/googlecalendar/src/google-api/oauth-client.ts @@ -4,10 +4,7 @@ import * as bp from '.botpress' type GoogleOAuth2Client = InstanceType<(typeof google.auth)['OAuth2']> -const OAUTH_SCOPES = [ - 'https://www.googleapis.com/auth/calendar.events', - 'https://www.googleapis.com/auth/calendar.readonly', -] +const OAUTH_SCOPES = ['https://www.googleapis.com/auth/calendar.events', 'https://www.googleapis.com/auth/calendar'] const GLOBAL_OAUTH_ENDPOINT = `${process.env.BP_WEBHOOK_URL}/oauth` export const exchangeAuthCodeAndSaveRefreshToken = async ({ @@ -49,6 +46,7 @@ export const getAuthenticatedOAuth2Client = async ({ email: ctx.configuration.clientEmail, key: ctx.configuration.privateKey.split(String.raw`\n`).join('\n'), scopes: OAUTH_SCOPES, + subject: ctx.configuration.impersonateEmail, }) } diff --git a/integrations/googlecalendar/src/google-api/types.d.ts b/integrations/googlecalendar/src/google-api/types.d.ts deleted file mode 100644 index 8d9d6c3dd73..00000000000 --- a/integrations/googlecalendar/src/google-api/types.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { google } from 'googleapis' -import { Event as EventEntity } from 'definitions' - -type GoogleCalendarClient = ReturnType -type GoogleOAuth2Client = InstanceType<(typeof google.auth)['OAuth2']> - -// Entities: -type Event = EventEntity.inferredType -type BareMinimumEvent = PartialExcept - -// Action requests: -type CreateEventRequest = Omit -type UpdateEventRequest = Omit - -// Type utilities: - -/** Like Pick, but each property is required */ -type PickRequired = { [P in K]-?: T[P] } -/** Makes all properties of T optional, except K, which are all required */ -type PartialExcept = Partial> & PickRequired diff --git a/integrations/googlecalendar/src/google-api/types.ts b/integrations/googlecalendar/src/google-api/types.ts new file mode 100644 index 00000000000..8843474201f --- /dev/null +++ b/integrations/googlecalendar/src/google-api/types.ts @@ -0,0 +1,34 @@ +import { Event as EventEntity } from 'definitions' +import { google } from 'googleapis' + +export type GoogleCalendarClient = ReturnType +export type GoogleOAuth2Client = InstanceType<(typeof google.auth)['OAuth2']> + +// Entities: +export type Event = EventEntity.inferredType +type BareMinimumEvent = PartialExcept + +// Action requests: +export type CreateEventRequest = Omit & { + attendees?: Array<{ + email: string + displayName?: string + optional?: boolean + responseStatus?: 'tentative' | 'needsAction' | 'declined' | 'accepted' + }> +} +export type UpdateEventRequest = Omit & { + attendees?: Array<{ + email: string + displayName?: string + optional?: boolean + responseStatus?: 'tentative' | 'needsAction' | 'declined' | 'accepted' + }> +} + +// Type utilities: + +/** Like Pick, but each property is required */ +type PickRequired = { [P in K]-?: T[P] } +/** Makes all properties of T optional, except K, which are all required */ +type PartialExcept = Partial> & PickRequired