@@ -37,6 +37,57 @@ type ComposioV2EmailPayload = {
3737 } ;
3838} ;
3939
40+ // Composio V2 webhook format for calendar triggers
41+ // Trigger types from Composio:
42+ // - googlecalendar_event_starting_soon: Event is within configured minutes from starting
43+ // - googlecalendar_event_created: New event created
44+ // - googlecalendar_event_updated: Existing event modified
45+ // - googlecalendar_event_canceled_or_deleted: Event cancelled/deleted
46+ // - googlecalendar_attendee_response_changed: RSVP changed
47+ // - googlecalendar_calendar_event_sync: Full event data sync
48+ type ComposioV2CalendarPayload = {
49+ type :
50+ | "googlecalendar_event_starting_soon"
51+ | "googlecalendar_event_created"
52+ | "googlecalendar_event_updated"
53+ | "googlecalendar_event_canceled_or_deleted"
54+ | "googlecalendar_attendee_response_changed"
55+ | "googlecalendar_calendar_event_sync" ;
56+ timestamp : string ;
57+ log_id : string ;
58+ data : {
59+ id : string ;
60+ event_id : string ;
61+ calendar_id ?: string ;
62+ summary : string ;
63+ description ?: string ;
64+ start ?: {
65+ dateTime ?: string ;
66+ date ?: string ;
67+ timeZone ?: string ;
68+ } ;
69+ end ?: {
70+ dateTime ?: string ;
71+ date ?: string ;
72+ timeZone ?: string ;
73+ } ;
74+ attendees ?: Array < {
75+ email : string ;
76+ displayName ?: string ;
77+ responseStatus ?: string ;
78+ } > ;
79+ location ?: string ;
80+ // Event Starting Soon specific fields
81+ countdown_minutes ?: number ;
82+ start_time ?: string ;
83+ connection_id : string ;
84+ connection_nano_id : string ;
85+ trigger_nano_id : string ;
86+ trigger_id : string ;
87+ user_id : string ; // This is our Poppy userId
88+ } ;
89+ } ;
90+
4091// Legacy email trigger format
4192type EmailTriggerPayload = {
4293 event : "trigger" ;
@@ -73,7 +124,8 @@ type OAuthCallbackPayload = {
73124type ComposioWebhookPayload =
74125 | OAuthCallbackPayload
75126 | EmailTriggerPayload
76- | ComposioV2EmailPayload ;
127+ | ComposioV2EmailPayload
128+ | ComposioV2CalendarPayload ;
77129
78130type EmailInfo = {
79131 messageId : string ;
@@ -98,6 +150,20 @@ const isV2EmailPayload = (
98150 return "type" in payload && payload . type === "gmail_new_gmail_message" ;
99151} ;
100152
153+ const isV2CalendarPayload = (
154+ payload : ComposioWebhookPayload ,
155+ ) : payload is ComposioV2CalendarPayload => {
156+ return (
157+ "type" in payload &&
158+ ( payload . type === "googlecalendar_event_starting_soon" ||
159+ payload . type === "googlecalendar_event_created" ||
160+ payload . type === "googlecalendar_event_updated" ||
161+ payload . type === "googlecalendar_event_canceled_or_deleted" ||
162+ payload . type === "googlecalendar_attendee_response_changed" ||
163+ payload . type === "googlecalendar_calendar_event_sync" )
164+ ) ;
165+ } ;
166+
101167const isOAuthCallback = (
102168 payload : ComposioWebhookPayload ,
103169) : payload is OAuthCallbackPayload => {
@@ -116,6 +182,10 @@ export const handleComposioWebhook = async (
116182 return handleV2EmailTrigger ( payload , env ) ;
117183 }
118184
185+ if ( isV2CalendarPayload ( payload ) ) {
186+ return handleV2CalendarTrigger ( payload , env ) ;
187+ }
188+
119189 if ( isEmailTrigger ( payload ) ) {
120190 return handleEmailTrigger ( payload , env ) ;
121191 }
@@ -419,3 +489,128 @@ Notify the user about this email. Keep it brief but informative.`;
419489 return { success : false , message : "Failed to notify user" } ;
420490 }
421491} ;
492+
493+ const handleV2CalendarTrigger = async (
494+ payload : ComposioV2CalendarPayload ,
495+ env : WorkerEnv ,
496+ ) : Promise < { success : boolean ; message : string } > => {
497+ const calendarLogger = logger . withTags ( { module : "v2-calendar-trigger" } ) ;
498+
499+ calendarLogger . info ( "Processing V2 calendar trigger" , {
500+ type : payload . type ,
501+ userId : payload . data . user_id ,
502+ eventSummary : payload . data . summary ,
503+ } ) ;
504+
505+ const userId = payload . data . user_id ;
506+
507+ // Only notify for "Event Starting Soon" trigger
508+ // Other events are informational - user initiated them or we don't need to notify
509+ if ( payload . type !== "googlecalendar_event_starting_soon" ) {
510+ calendarLogger . info ( "Calendar event logged (non-reminder)" , {
511+ type : payload . type ,
512+ eventSummary : payload . data . summary ,
513+ } ) ;
514+ return { success : true , message : "Calendar event logged" } ;
515+ }
516+
517+ // Notify user about upcoming event
518+ const eventInfo = {
519+ eventId : payload . data . event_id ,
520+ summary : payload . data . summary ,
521+ description : payload . data . description ,
522+ start :
523+ payload . data . start_time ||
524+ payload . data . start ?. dateTime ||
525+ payload . data . start ?. date ,
526+ location : payload . data . location ,
527+ attendees : payload . data . attendees ?. map ( ( a ) => a . email ) . join ( ", " ) ,
528+ countdownMinutes : payload . data . countdown_minutes ,
529+ } ;
530+
531+ const db = getDb ( env . HYPERDRIVE . connectionString ) ;
532+ return notifyUserAboutCalendarEvent ( userId , eventInfo , db , env ) ;
533+ } ;
534+
535+ const notifyUserAboutCalendarEvent = async (
536+ userId : string ,
537+ eventInfo : {
538+ eventId : string ;
539+ summary : string ;
540+ description ?: string ;
541+ start ?: string ;
542+ location ?: string ;
543+ attendees ?: string ;
544+ countdownMinutes ?: number ;
545+ } ,
546+ db : Database ,
547+ env : WorkerEnv ,
548+ ) : Promise < { success : boolean ; message : string } > => {
549+ const notifyLogger = logger . withTags ( { module : "calendar-notify" } ) ;
550+
551+ // Find user's conversation
552+ const userConversation = await db
553+ . select ( { conversationId : conversationParticipants . conversationId } )
554+ . from ( conversationParticipants )
555+ . innerJoin (
556+ conversations ,
557+ eq ( conversations . id , conversationParticipants . conversationId ) ,
558+ )
559+ . where ( eq ( conversationParticipants . userId , userId ) )
560+ . orderBy ( desc ( conversations . updatedAt ) )
561+ . limit ( 1 ) ;
562+
563+ if ( userConversation . length === 0 ) {
564+ notifyLogger . warn ( "No conversation found" , { userId } ) ;
565+ return { success : false , message : "No conversation found" } ;
566+ }
567+
568+ const conversationId = userConversation [ 0 ] . conversationId ;
569+
570+ const interactionAgent = await db . query . agents . findFirst ( {
571+ where : eq ( agents . conversationId , conversationId ) ,
572+ } ) ;
573+
574+ if ( ! interactionAgent ) {
575+ notifyLogger . warn ( "No interaction agent found" , { conversationId } ) ;
576+ return { success : false , message : "No interaction agent found" } ;
577+ }
578+
579+ const countdownText = eventInfo . countdownMinutes
580+ ? `Starting in ${ eventInfo . countdownMinutes } minutes`
581+ : `Time: ${ eventInfo . start || "Not specified" } ` ;
582+
583+ const taskDescription = `[CALENDAR REMINDER] Upcoming event:
584+ Event: ${ eventInfo . summary }
585+ ${ countdownText }
586+ ${ eventInfo . location ? `Location: ${ eventInfo . location } ` : "" }
587+ ${ eventInfo . attendees ? `Attendees: ${ eventInfo . attendees } ` : "" }
588+ ${ eventInfo . description ? `Description: ${ eventInfo . description } ` : "" }
589+
590+ Notify the user about this upcoming calendar event. Keep it brief but informative.` ;
591+
592+ const executionAgentId = env . EXECUTION_AGENT . idFromName ( interactionAgent . id ) ;
593+ const executionAgent = env . EXECUTION_AGENT . get ( executionAgentId ) ;
594+
595+ try {
596+ await executionAgent . executeTask ( {
597+ agentId : interactionAgent . id ,
598+ conversationId,
599+ taskDescription,
600+ userId,
601+ } ) ;
602+
603+ notifyLogger . info ( "Calendar notification dispatched" , {
604+ userId,
605+ eventSummary : eventInfo . summary ,
606+ } ) ;
607+
608+ return { success : true , message : "User notified" } ;
609+ } catch ( error ) {
610+ notifyLogger . error ( "Failed to dispatch calendar notification" , {
611+ error : error instanceof Error ? error . message : String ( error ) ,
612+ userId,
613+ } ) ;
614+ return { success : false , message : "Failed to notify user" } ;
615+ }
616+ } ;
0 commit comments