Skip to content
Merged
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
96 changes: 94 additions & 2 deletions integrations/googlecalendar/definitions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.'),
},
Expand All @@ -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: {
Expand All @@ -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']
7 changes: 7 additions & 0 deletions integrations/googlecalendar/definitions/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
38 changes: 38 additions & 0 deletions integrations/googlecalendar/definitions/entities/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions integrations/googlecalendar/hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 1 addition & 32 deletions integrations/googlecalendar/integration.definition.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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' } },
}))
8 changes: 1 addition & 7 deletions integrations/googlecalendar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
Original file line number Diff line number Diff line change
@@ -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,
}
}
)

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Loading