mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
feat(gcal-invite): added google calendar invite tool (#457)
* feat(gcal-invite): added google calendar invite * fix: addressed comments
This commit is contained in:
@@ -2,9 +2,9 @@ import { GoogleCalendarIcon } from '@/components/icons'
|
||||
import type {
|
||||
GoogleCalendarCreateResponse,
|
||||
GoogleCalendarGetResponse,
|
||||
GoogleCalendarInviteResponse,
|
||||
GoogleCalendarListResponse,
|
||||
GoogleCalendarQuickAddResponse,
|
||||
GoogleCalendarUpdateResponse,
|
||||
} from '@/tools/google_calendar/types'
|
||||
import type { BlockConfig } from '../types'
|
||||
|
||||
@@ -13,14 +13,14 @@ type GoogleCalendarResponse =
|
||||
| GoogleCalendarListResponse
|
||||
| GoogleCalendarGetResponse
|
||||
| GoogleCalendarQuickAddResponse
|
||||
| GoogleCalendarUpdateResponse
|
||||
| GoogleCalendarInviteResponse
|
||||
|
||||
export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
type: 'google_calendar',
|
||||
name: 'Google Calendar',
|
||||
description: 'Manage Google Calendar events',
|
||||
longDescription:
|
||||
'Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication.',
|
||||
"Integrate Google Calendar functionality to create, read, update, and list calendar events within your workflow. Automate scheduling, check availability, and manage events using OAuth authentication. Email invitations are sent asynchronously and delivery depends on recipients' Google Calendar settings.",
|
||||
docsLink: 'https://docs.simstudio.ai/tools/google-calendar',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
@@ -36,6 +36,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
{ label: 'List Events', id: 'list' },
|
||||
{ label: 'Get Event', id: 'get' },
|
||||
{ label: 'Quick Add (Natural Language)', id: 'quick_add' },
|
||||
{ label: 'Invite Attendees', id: 'invite' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -133,66 +134,29 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
title: 'Event ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Event ID to retrieve',
|
||||
condition: { field: 'operation', value: 'get' },
|
||||
placeholder: 'Event ID',
|
||||
condition: { field: 'operation', value: ['get', 'invite'] },
|
||||
},
|
||||
|
||||
// Update Event Fields
|
||||
{
|
||||
id: 'eventId',
|
||||
title: 'Event ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Event ID to update',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'summary',
|
||||
title: 'Event Title',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Updated meeting title',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
title: 'Description',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Updated description',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
title: 'Location',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Updated location',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'startDateTime',
|
||||
title: 'Start Date & Time',
|
||||
type: 'short-input',
|
||||
layout: 'half',
|
||||
placeholder: '2025-06-03T10:00:00-08:00',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
{
|
||||
id: 'endDateTime',
|
||||
title: 'End Date & Time',
|
||||
type: 'short-input',
|
||||
layout: 'half',
|
||||
placeholder: '2025-06-03T11:00:00-08:00',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
// Invite Attendees Fields
|
||||
{
|
||||
id: 'attendees',
|
||||
title: 'Attendees (comma-separated emails)',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'john@example.com, jane@example.com',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
condition: { field: 'operation', value: 'invite' },
|
||||
},
|
||||
{
|
||||
id: 'replaceExisting',
|
||||
title: 'Replace Existing Attendees',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
condition: { field: 'operation', value: 'invite' },
|
||||
options: [
|
||||
{ label: 'Add to existing attendees', id: 'false' },
|
||||
{ label: 'Replace all attendees', id: 'true' },
|
||||
],
|
||||
},
|
||||
|
||||
// Quick Add Fields
|
||||
@@ -213,7 +177,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
condition: { field: 'operation', value: 'quick_add' },
|
||||
},
|
||||
|
||||
// Notification setting (for create, update, quick_add)
|
||||
// Notification setting (for create, quick_add, invite)
|
||||
{
|
||||
id: 'sendUpdates',
|
||||
title: 'Send Email Notifications',
|
||||
@@ -221,12 +185,12 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
layout: 'full',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create', 'update', 'quick_add'],
|
||||
value: ['create', 'quick_add', 'invite'],
|
||||
},
|
||||
options: [
|
||||
{ label: 'All', id: 'all' },
|
||||
{ label: 'External Only', id: 'externalOnly' },
|
||||
{ label: 'None', id: 'none' },
|
||||
{ label: 'All attendees (recommended)', id: 'all' },
|
||||
{ label: 'External attendees only', id: 'externalOnly' },
|
||||
{ label: 'None (no emails sent)', id: 'none' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -236,6 +200,7 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
'google_calendar_list',
|
||||
'google_calendar_get',
|
||||
'google_calendar_quick_add',
|
||||
'google_calendar_invite',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
@@ -248,12 +213,14 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
return 'google_calendar_get'
|
||||
case 'quick_add':
|
||||
return 'google_calendar_quick_add'
|
||||
case 'invite':
|
||||
return 'google_calendar_invite'
|
||||
default:
|
||||
throw new Error(`Invalid Google Calendar operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { credential, operation, attendees, ...rest } = params
|
||||
const { credential, operation, attendees, replaceExisting, ...rest } = params
|
||||
|
||||
const processedParams = { ...rest }
|
||||
|
||||
@@ -270,8 +237,13 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert replaceExisting string to boolean for invite operation
|
||||
if (operation === 'invite' && replaceExisting !== undefined) {
|
||||
processedParams.replaceExisting = replaceExisting === 'true'
|
||||
}
|
||||
|
||||
// Set default sendUpdates to 'all' if not specified for operations that support it
|
||||
if (['create', 'update', 'quick_add'].includes(operation) && !processedParams.sendUpdates) {
|
||||
if (['create', 'quick_add', 'invite'].includes(operation) && !processedParams.sendUpdates) {
|
||||
processedParams.sendUpdates = 'all'
|
||||
}
|
||||
|
||||
@@ -299,12 +271,15 @@ export const GoogleCalendarBlock: BlockConfig<GoogleCalendarResponse> = {
|
||||
timeMin: { type: 'string', required: false },
|
||||
timeMax: { type: 'string', required: false },
|
||||
|
||||
// Get/Update operation inputs
|
||||
// Get/Invite operation inputs
|
||||
eventId: { type: 'string', required: false },
|
||||
|
||||
// Quick add inputs
|
||||
text: { type: 'string', required: false },
|
||||
|
||||
// Invite specific inputs
|
||||
replaceExisting: { type: 'string', required: false },
|
||||
|
||||
// Common inputs
|
||||
sendUpdates: { type: 'string', required: false },
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createTool } from './create'
|
||||
import { getTool } from './get'
|
||||
import { inviteTool } from './invite'
|
||||
import { listTool } from './list'
|
||||
import { quickAddTool } from './quick_add'
|
||||
|
||||
export const googleCalendarCreateTool = createTool
|
||||
export const googleCalendarGetTool = getTool
|
||||
export const googleCalendarInviteTool = inviteTool
|
||||
export const googleCalendarListTool = listTool
|
||||
export const googleCalendarQuickAddTool = quickAddTool
|
||||
|
||||
270
apps/sim/tools/google_calendar/invite.ts
Normal file
270
apps/sim/tools/google_calendar/invite.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import type { ToolConfig } from '../types'
|
||||
import {
|
||||
CALENDAR_API_BASE,
|
||||
type GoogleCalendarInviteParams,
|
||||
type GoogleCalendarInviteResponse,
|
||||
} from './types'
|
||||
|
||||
export const inviteTool: ToolConfig<GoogleCalendarInviteParams, GoogleCalendarInviteResponse> = {
|
||||
id: 'google_calendar_invite',
|
||||
name: 'Google Calendar Invite Attendees',
|
||||
description: 'Invite attendees to an existing Google Calendar event',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-calendar',
|
||||
additionalScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Access token for Google Calendar API',
|
||||
},
|
||||
calendarId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'Calendar ID (defaults to primary)',
|
||||
},
|
||||
eventId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Event ID to invite attendees to',
|
||||
},
|
||||
attendees: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
description: 'Array of attendee email addresses to invite',
|
||||
},
|
||||
sendUpdates: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
description: 'How to send updates to attendees: all, externalOnly, or none',
|
||||
},
|
||||
replaceExisting: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
description: 'Whether to replace existing attendees or add to them (defaults to false)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: GoogleCalendarInviteParams) => {
|
||||
const calendarId = params.calendarId || 'primary'
|
||||
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}`
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params: GoogleCalendarInviteParams) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const existingEvent = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
|
||||
}
|
||||
|
||||
// Validate required fields exist
|
||||
if (!existingEvent.start || !existingEvent.end || !existingEvent.summary) {
|
||||
throw new Error('Existing event is missing required fields (start, end, or summary)')
|
||||
}
|
||||
|
||||
// Process new attendees - handle both string and array formats
|
||||
let newAttendeeList: string[] = []
|
||||
|
||||
if (params?.attendees) {
|
||||
if (Array.isArray(params.attendees)) {
|
||||
// Already an array from block processing
|
||||
newAttendeeList = params.attendees.filter(
|
||||
(email: string) => email && email.trim().length > 0
|
||||
)
|
||||
} else if (
|
||||
typeof (params.attendees as any) === 'string' &&
|
||||
(params.attendees as any).trim().length > 0
|
||||
) {
|
||||
// Fallback: process comma-separated string if block didn't convert it
|
||||
newAttendeeList = (params.attendees as any)
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate final attendees list
|
||||
const existingAttendees = existingEvent.attendees || []
|
||||
let finalAttendees: Array<any> = []
|
||||
|
||||
// Handle replaceExisting properly - check for both boolean true and string "true"
|
||||
const shouldReplace =
|
||||
params?.replaceExisting === true || (params?.replaceExisting as any) === 'true'
|
||||
|
||||
if (shouldReplace) {
|
||||
// Replace all attendees with just the new ones
|
||||
finalAttendees = newAttendeeList.map((email: string) => ({
|
||||
email,
|
||||
responseStatus: 'needsAction',
|
||||
}))
|
||||
} else {
|
||||
// Add to existing attendees (preserve all existing ones)
|
||||
|
||||
// Start with ALL existing attendees - preserve them completely
|
||||
finalAttendees = [...existingAttendees]
|
||||
|
||||
// Get set of existing emails for duplicate checking (case-insensitive)
|
||||
const existingEmails = new Set(
|
||||
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
|
||||
)
|
||||
|
||||
// Add only new attendees that don't already exist
|
||||
for (const newEmail of newAttendeeList) {
|
||||
const emailLower = newEmail.toLowerCase()
|
||||
if (!existingEmails.has(emailLower)) {
|
||||
finalAttendees.push({
|
||||
email: newEmail,
|
||||
responseStatus: 'needsAction',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the complete existing event object and only modify the attendees field
|
||||
// This is crucial because the Google Calendar API update method "does not support patch semantics
|
||||
// and always updates the entire event resource" according to the documentation
|
||||
const updatedEvent = {
|
||||
...existingEvent, // Start with the complete existing event to preserve all fields
|
||||
attendees: finalAttendees, // Only modify the attendees field
|
||||
}
|
||||
|
||||
// Remove read-only fields that shouldn't be included in updates
|
||||
const readOnlyFields = [
|
||||
'id',
|
||||
'etag',
|
||||
'kind',
|
||||
'created',
|
||||
'updated',
|
||||
'htmlLink',
|
||||
'iCalUID',
|
||||
'sequence',
|
||||
'creator',
|
||||
'organizer',
|
||||
]
|
||||
readOnlyFields.forEach((field) => {
|
||||
delete updatedEvent[field]
|
||||
})
|
||||
|
||||
// Construct PUT URL with query parameters
|
||||
const calendarId = params?.calendarId || 'primary'
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.sendUpdates !== undefined) {
|
||||
queryParams.append('sendUpdates', params.sendUpdates)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
// Send PUT request to update the event
|
||||
const putResponse = await fetch(putUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedEvent),
|
||||
})
|
||||
|
||||
// Handle the PUT response
|
||||
if (!putResponse.ok) {
|
||||
const errorData = await putResponse.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to invite attendees to calendar event')
|
||||
}
|
||||
|
||||
const data = await putResponse.json()
|
||||
const totalAttendees = data.attendees?.length || 0
|
||||
|
||||
// Calculate how many new attendees were actually added
|
||||
let newAttendeesAdded = 0
|
||||
|
||||
if (shouldReplace) {
|
||||
newAttendeesAdded = newAttendeeList.length
|
||||
} else {
|
||||
// Count how many of the new emails weren't already in the existing list
|
||||
const existingEmails = new Set(
|
||||
existingAttendees.map((attendee: any) => attendee.email?.toLowerCase() || '')
|
||||
)
|
||||
newAttendeesAdded = newAttendeeList.filter(
|
||||
(email) => !existingEmails.has(email.toLowerCase())
|
||||
).length
|
||||
}
|
||||
|
||||
// Improved messaging about email delivery
|
||||
let baseMessage: string
|
||||
if (shouldReplace) {
|
||||
baseMessage = `Successfully updated event "${data.summary}" with ${totalAttendees} attendee${totalAttendees !== 1 ? 's' : ''}`
|
||||
} else {
|
||||
if (newAttendeesAdded > 0) {
|
||||
baseMessage = `Successfully added ${newAttendeesAdded} new attendee${newAttendeesAdded !== 1 ? 's' : ''} to event "${data.summary}" (total: ${totalAttendees})`
|
||||
} else {
|
||||
baseMessage = `No new attendees added to event "${data.summary}" - all specified attendees were already invited (total: ${totalAttendees})`
|
||||
}
|
||||
}
|
||||
|
||||
const emailNote =
|
||||
params?.sendUpdates !== 'none'
|
||||
? ` Email invitations are being sent asynchronously - delivery may take a few minutes and depends on recipients' Google Calendar settings.`
|
||||
: ` No email notifications will be sent as requested.`
|
||||
|
||||
const content = baseMessage + emailNote
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content,
|
||||
metadata: {
|
||||
id: data.id,
|
||||
htmlLink: data.htmlLink,
|
||||
status: data.status,
|
||||
summary: data.summary,
|
||||
description: data.description,
|
||||
location: data.location,
|
||||
start: data.start,
|
||||
end: data.end,
|
||||
attendees: data.attendees,
|
||||
creator: data.creator,
|
||||
organizer: data.organizer,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
transformError: (error) => {
|
||||
if (error.error?.message) {
|
||||
if (error.error.message.includes('invalid authentication credentials')) {
|
||||
return 'Invalid or expired access token. Please reauthenticate.'
|
||||
}
|
||||
if (error.error.message.includes('quota')) {
|
||||
return 'Google Calendar API quota exceeded. Please try again later.'
|
||||
}
|
||||
if (error.error.message.includes('Calendar not found')) {
|
||||
return 'Calendar not found. Please check the calendar ID.'
|
||||
}
|
||||
if (
|
||||
error.error.message.includes('Event not found') ||
|
||||
error.error.message.includes('Not Found')
|
||||
) {
|
||||
return 'Event not found. Please check the event ID.'
|
||||
}
|
||||
if (error.error.message.includes('Failed to fetch existing event')) {
|
||||
return `Unable to retrieve existing event details: ${error.error.message}`
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return (
|
||||
error.message || 'An unexpected error occurred while inviting attendees to the calendar event'
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -2,6 +2,20 @@ import type { ToolResponse } from '../types'
|
||||
|
||||
export const CALENDAR_API_BASE = 'https://www.googleapis.com/calendar/v3'
|
||||
|
||||
// Shared attendee interface that matches Google Calendar API specification
|
||||
export interface CalendarAttendee {
|
||||
id?: string
|
||||
email: string
|
||||
displayName?: string
|
||||
organizer?: boolean
|
||||
self?: boolean
|
||||
resource?: boolean
|
||||
optional?: boolean
|
||||
responseStatus: string
|
||||
comment?: string
|
||||
additionalGuests?: number
|
||||
}
|
||||
|
||||
interface BaseGoogleCalendarParams {
|
||||
accessToken: string
|
||||
calendarId?: string // defaults to 'primary' if not provided
|
||||
@@ -54,6 +68,13 @@ export interface GoogleCalendarQuickAddParams extends BaseGoogleCalendarParams {
|
||||
sendUpdates?: 'all' | 'externalOnly' | 'none'
|
||||
}
|
||||
|
||||
export interface GoogleCalendarInviteParams extends BaseGoogleCalendarParams {
|
||||
eventId: string
|
||||
attendees: string[] // Array of email addresses to invite
|
||||
sendUpdates?: 'all' | 'externalOnly' | 'none'
|
||||
replaceExisting?: boolean // Whether to replace existing attendees or add to them
|
||||
}
|
||||
|
||||
export type GoogleCalendarToolParams =
|
||||
| GoogleCalendarCreateParams
|
||||
| GoogleCalendarListParams
|
||||
@@ -61,6 +82,7 @@ export type GoogleCalendarToolParams =
|
||||
| GoogleCalendarUpdateParams
|
||||
| GoogleCalendarDeleteParams
|
||||
| GoogleCalendarQuickAddParams
|
||||
| GoogleCalendarInviteParams
|
||||
|
||||
interface EventMetadata {
|
||||
id: string
|
||||
@@ -79,11 +101,7 @@ interface EventMetadata {
|
||||
date?: string
|
||||
timeZone?: string
|
||||
}
|
||||
attendees?: Array<{
|
||||
email: string
|
||||
displayName?: string
|
||||
responseStatus: string
|
||||
}>
|
||||
attendees?: CalendarAttendee[]
|
||||
creator?: {
|
||||
email: string
|
||||
displayName?: string
|
||||
@@ -144,6 +162,13 @@ export interface GoogleCalendarUpdateResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleCalendarInviteResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
metadata: EventMetadata
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleCalendarEvent {
|
||||
id: string
|
||||
status: string
|
||||
@@ -163,12 +188,7 @@ export interface GoogleCalendarEvent {
|
||||
date?: string
|
||||
timeZone?: string
|
||||
}
|
||||
attendees?: Array<{
|
||||
email: string
|
||||
displayName?: string
|
||||
responseStatus: string
|
||||
optional?: boolean
|
||||
}>
|
||||
attendees?: CalendarAttendee[]
|
||||
creator?: {
|
||||
email: string
|
||||
displayName?: string
|
||||
@@ -222,12 +242,7 @@ export interface GoogleCalendarApiEventResponse {
|
||||
date?: string
|
||||
timeZone?: string
|
||||
}
|
||||
attendees?: Array<{
|
||||
email: string
|
||||
displayName?: string
|
||||
responseStatus: string
|
||||
optional?: boolean
|
||||
}>
|
||||
attendees?: CalendarAttendee[]
|
||||
creator?: {
|
||||
email: string
|
||||
displayName?: string
|
||||
|
||||
@@ -66,7 +66,7 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
|
||||
attendees: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
description: 'Array of attendee email addresses',
|
||||
description: 'Array of attendee email addresses (replaces all existing attendees)',
|
||||
},
|
||||
sendUpdates: {
|
||||
type: 'string',
|
||||
@@ -78,100 +78,145 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
|
||||
request: {
|
||||
url: (params: GoogleCalendarUpdateParams) => {
|
||||
const calendarId = params.calendarId || 'primary'
|
||||
const queryParams = new URLSearchParams()
|
||||
|
||||
if (params.sendUpdates !== undefined) {
|
||||
queryParams.append('sendUpdates', params.sendUpdates)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}${queryString ? `?${queryString}` : ''}`
|
||||
return `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params.eventId)}`
|
||||
},
|
||||
method: 'PUT',
|
||||
method: 'GET',
|
||||
headers: (params: GoogleCalendarUpdateParams) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: GoogleCalendarUpdateParams): Record<string, any> => {
|
||||
const eventData: any = {}
|
||||
|
||||
// Only include fields that are provided and not empty
|
||||
if (params.summary !== undefined && params.summary !== null && params.summary.trim() !== '') {
|
||||
eventData.summary = params.summary
|
||||
}
|
||||
|
||||
if (
|
||||
params.description !== undefined &&
|
||||
params.description !== null &&
|
||||
params.description.trim() !== ''
|
||||
) {
|
||||
eventData.description = params.description
|
||||
}
|
||||
|
||||
if (
|
||||
params.location !== undefined &&
|
||||
params.location !== null &&
|
||||
params.location.trim() !== ''
|
||||
) {
|
||||
eventData.location = params.location
|
||||
}
|
||||
|
||||
// Only update times if both start and end are provided (Google Calendar requires both)
|
||||
const hasStartTime =
|
||||
params.startDateTime !== undefined &&
|
||||
params.startDateTime !== null &&
|
||||
params.startDateTime.trim() !== ''
|
||||
const hasEndTime =
|
||||
params.endDateTime !== undefined &&
|
||||
params.endDateTime !== null &&
|
||||
params.endDateTime.trim() !== ''
|
||||
|
||||
if (hasStartTime && hasEndTime) {
|
||||
eventData.start = {
|
||||
dateTime: params.startDateTime,
|
||||
}
|
||||
eventData.end = {
|
||||
dateTime: params.endDateTime,
|
||||
}
|
||||
if (params.timeZone) {
|
||||
eventData.start.timeZone = params.timeZone
|
||||
eventData.end.timeZone = params.timeZone
|
||||
}
|
||||
}
|
||||
|
||||
if (params.attendees !== undefined && params.attendees !== null) {
|
||||
// Handle both string and array cases for attendees
|
||||
let attendeeList: string[] = []
|
||||
if (params.attendees) {
|
||||
const attendees = params.attendees as string | string[]
|
||||
if (Array.isArray(attendees)) {
|
||||
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
|
||||
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
|
||||
// Convert comma-separated string to array
|
||||
attendeeList = attendees
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Only update attendees if we have valid entries, otherwise preserve existing
|
||||
if (attendeeList.length > 0) {
|
||||
eventData.attendees = attendeeList.map((email: string) => ({ email }))
|
||||
}
|
||||
}
|
||||
|
||||
return eventData
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
transformResponse: async (response: Response, params) => {
|
||||
const existingEvent = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to update calendar event')
|
||||
throw new Error(existingEvent.error?.message || 'Failed to fetch existing event')
|
||||
}
|
||||
|
||||
// Start with the complete existing event to preserve all fields
|
||||
const updatedEvent = { ...existingEvent }
|
||||
|
||||
// Apply updates only for fields that are provided and not empty
|
||||
if (
|
||||
params?.summary !== undefined &&
|
||||
params?.summary !== null &&
|
||||
params?.summary.trim() !== ''
|
||||
) {
|
||||
updatedEvent.summary = params.summary
|
||||
}
|
||||
|
||||
if (
|
||||
params?.description !== undefined &&
|
||||
params?.description !== null &&
|
||||
params?.description.trim() !== ''
|
||||
) {
|
||||
updatedEvent.description = params.description
|
||||
}
|
||||
|
||||
if (
|
||||
params?.location !== undefined &&
|
||||
params?.location !== null &&
|
||||
params?.location.trim() !== ''
|
||||
) {
|
||||
updatedEvent.location = params.location
|
||||
}
|
||||
|
||||
// Only update times if both start and end are provided (Google Calendar requires both)
|
||||
const hasStartTime =
|
||||
params?.startDateTime !== undefined &&
|
||||
params?.startDateTime !== null &&
|
||||
params?.startDateTime.trim() !== ''
|
||||
const hasEndTime =
|
||||
params?.endDateTime !== undefined &&
|
||||
params?.endDateTime !== null &&
|
||||
params?.endDateTime.trim() !== ''
|
||||
|
||||
if (hasStartTime && hasEndTime) {
|
||||
updatedEvent.start = {
|
||||
dateTime: params.startDateTime,
|
||||
}
|
||||
updatedEvent.end = {
|
||||
dateTime: params.endDateTime,
|
||||
}
|
||||
if (params?.timeZone) {
|
||||
updatedEvent.start.timeZone = params.timeZone
|
||||
updatedEvent.end.timeZone = params.timeZone
|
||||
}
|
||||
}
|
||||
|
||||
// Handle attendees update - this replaces all existing attendees
|
||||
if (params?.attendees !== undefined && params?.attendees !== null) {
|
||||
let attendeeList: string[] = []
|
||||
if (params.attendees) {
|
||||
const attendees = params.attendees as string | string[]
|
||||
if (Array.isArray(attendees)) {
|
||||
attendeeList = attendees.filter((email: string) => email && email.trim().length > 0)
|
||||
} else if (typeof attendees === 'string' && attendees.trim().length > 0) {
|
||||
// Convert comma-separated string to array
|
||||
attendeeList = attendees
|
||||
.split(',')
|
||||
.map((email: string) => email.trim())
|
||||
.filter((email: string) => email.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Replace all attendees with the new list
|
||||
if (attendeeList.length > 0) {
|
||||
updatedEvent.attendees = attendeeList.map((email: string) => ({
|
||||
email,
|
||||
responseStatus: 'needsAction',
|
||||
}))
|
||||
} else {
|
||||
// If empty attendee list is provided, remove all attendees
|
||||
updatedEvent.attendees = []
|
||||
}
|
||||
}
|
||||
|
||||
// Remove read-only fields that shouldn't be included in updates
|
||||
const readOnlyFields = [
|
||||
'id',
|
||||
'etag',
|
||||
'kind',
|
||||
'created',
|
||||
'updated',
|
||||
'htmlLink',
|
||||
'iCalUID',
|
||||
'sequence',
|
||||
'creator',
|
||||
'organizer',
|
||||
]
|
||||
readOnlyFields.forEach((field) => {
|
||||
delete updatedEvent[field]
|
||||
})
|
||||
|
||||
// Construct PUT URL with query parameters
|
||||
const calendarId = params?.calendarId || 'primary'
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params?.sendUpdates !== undefined) {
|
||||
queryParams.append('sendUpdates', params.sendUpdates)
|
||||
}
|
||||
|
||||
const queryString = queryParams.toString()
|
||||
const putUrl = `${CALENDAR_API_BASE}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(params?.eventId || '')}${queryString ? `?${queryString}` : ''}`
|
||||
|
||||
// Send PUT request to update the event
|
||||
const putResponse = await fetch(putUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${params?.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedEvent),
|
||||
})
|
||||
|
||||
if (!putResponse.ok) {
|
||||
const errorData = await putResponse.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to update calendar event')
|
||||
}
|
||||
|
||||
const data = await putResponse.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
@@ -210,6 +255,9 @@ export const updateTool: ToolConfig<GoogleCalendarUpdateParams, GoogleCalendarTo
|
||||
) {
|
||||
return 'Event not found. Please check the event ID.'
|
||||
}
|
||||
if (error.error.message.includes('Failed to fetch existing event')) {
|
||||
return `Unable to retrieve existing event details: ${error.error.message}`
|
||||
}
|
||||
return error.error.message
|
||||
}
|
||||
return error.message || 'An unexpected error occurred while updating the calendar event'
|
||||
|
||||
@@ -30,6 +30,7 @@ import { searchTool as googleSearchTool } from './google'
|
||||
import {
|
||||
googleCalendarCreateTool,
|
||||
googleCalendarGetTool,
|
||||
googleCalendarInviteTool,
|
||||
googleCalendarListTool,
|
||||
googleCalendarQuickAddTool,
|
||||
} from './google_calendar'
|
||||
@@ -211,4 +212,5 @@ export const tools: Record<string, ToolConfig> = {
|
||||
google_calendar_get: googleCalendarGetTool,
|
||||
google_calendar_list: googleCalendarListTool,
|
||||
google_calendar_quick_add: googleCalendarQuickAddTool,
|
||||
google_calendar_invite: googleCalendarInviteTool,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user