-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Google Calendar & Microsoft Outlook Calendar - adding polling sources for upcoming events #18976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| export default { | ||
| "kind": "calendar#event", | ||
| "etag": "\"3442838491454000\"", | ||
| "id": "0dip62r3f3d85o35jjnjcmqbmo", | ||
| "status": "confirmed", | ||
| "htmlLink": "https://www.google.com/calendar/event?eid=MGRpcDYycjNW8zNWgbWljaGVsbGUucGlwZWRyZWFtQG0", | ||
| "created": "2024-07-19T20:00:45.000Z", | ||
| "updated": "2024-07-19T20:00:45.727Z", | ||
| "summary": "Upcoming Meeting", | ||
| "creator": { | ||
| "email": "test@sample.com", | ||
| "self": true | ||
| }, | ||
| "organizer": { | ||
| "email": "test@sample.com", | ||
| "self": true | ||
| }, | ||
| "start": { | ||
| "dateTime": "2024-07-19T16:07:00-04:00", | ||
| "timeZone": "America/Detroit" | ||
| }, | ||
| "end": { | ||
| "dateTime": "2024-07-19T17:07:00-04:00", | ||
| "timeZone": "America/Detroit" | ||
| }, | ||
| "iCalUID": "0dip62r35jjnjcmqbmo@google.com", | ||
| "sequence": 0, | ||
| "reminders": { | ||
| "useDefault": true | ||
| }, | ||
| "eventType": "default" | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import common from "../common/common.mjs"; | ||
| import sampleEmit from "./test-event.mjs"; | ||
|
|
||
| export default { | ||
| ...common, | ||
| key: "google_calendar-upcoming-event-alert-polling", | ||
| name: "New Upcoming Event Alert (Polling)", | ||
| description: "Emit new event based on a time interval before an upcoming event in the calendar. [See the documentation](https://developers.google.com/calendar/api/v3/reference/events/list)", | ||
| version: "0.0.1", | ||
| type: "source", | ||
| dedupe: "unique", | ||
| props: { | ||
| ...common.props, | ||
| db: "$.service.db", | ||
| pollingInfo: { | ||
|
Check warning on line 15 in components/google_calendar/sources/upcoming-event-alert-polling/upcoming-event-alert-polling.mjs
|
||
| type: "alert", | ||
| alertType: "info", | ||
| content: "Since this source executes based on a timer, event emission may be slightly delayed. For example, if the source runs every 5 minutes, the delay may be up to 5 minutes. You can use the `upcoming-event-alert` source for instant event emission.", | ||
| }, | ||
| calendarId: { | ||
| propDefinition: [ | ||
| common.props.googleCalendar, | ||
| "calendarId", | ||
| ], | ||
| }, | ||
| eventTypes: { | ||
| propDefinition: [ | ||
| common.props.googleCalendar, | ||
| "eventTypes", | ||
| ], | ||
| }, | ||
| minutesBefore: { | ||
| type: "integer", | ||
| label: "Minutes Before", | ||
| description: "Number of minutes to trigger before the start of the calendar event.", | ||
| min: 0, | ||
| default: 30, | ||
| }, | ||
| }, | ||
| methods: { | ||
| ...common.methods, | ||
| _getEmittedEvents() { | ||
| return this.db.get("emittedEvents") || {}; | ||
| }, | ||
| _setEmittedEvents(emittedEvents) { | ||
| this.db.set("emittedEvents", emittedEvents); | ||
| }, | ||
| _cleanupEmittedEvents(now) { | ||
| const emittedEvents = this._getEmittedEvents(); | ||
| const cleanedEvents = {}; | ||
| let cleanedCount = 0; | ||
|
|
||
| // Keep only events that haven't passed yet | ||
| for (const [ | ||
| eventId, | ||
| startTime, | ||
| ] of Object.entries(emittedEvents)) { | ||
| if (startTime > now.getTime()) { | ||
| cleanedEvents[eventId] = startTime; | ||
| } else { | ||
| cleanedCount++; | ||
| } | ||
| } | ||
|
|
||
| if (cleanedCount > 0) { | ||
| console.log(`Cleaned up ${cleanedCount} past event(s) from emitted events tracker`); | ||
| this._setEmittedEvents(cleanedEvents); | ||
| } | ||
|
|
||
| return cleanedEvents; | ||
| }, | ||
| getConfig({ now }) { | ||
| // Get events starting from now up to the alert window | ||
| const timeMin = now.toISOString(); | ||
| // Look ahead to find events within our alert window | ||
| const alertWindowMs = this.minutesBefore * 60 * 1000; | ||
| const timeMax = new Date(now.getTime() + alertWindowMs).toISOString(); | ||
|
|
||
| return { | ||
| calendarId: this.calendarId, | ||
| timeMin, | ||
| timeMax, | ||
| eventTypes: this.eventTypes, | ||
| singleEvents: true, | ||
| orderBy: "startTime", | ||
| }; | ||
| }, | ||
| isRelevant(event, { now }) { | ||
| // Skip cancelled events | ||
| if (event.status === "cancelled") { | ||
| return false; | ||
| } | ||
|
|
||
| // Get event start time | ||
| const startTime = event.start | ||
| ? new Date(event.start.dateTime || event.start.date) | ||
| : null; | ||
|
|
||
| if (!startTime) { | ||
| return false; | ||
| } | ||
|
|
||
| // Calculate time remaining until event starts (in milliseconds) | ||
| const timeRemaining = startTime.getTime() - now.getTime(); | ||
|
|
||
| // Skip past events | ||
| if (timeRemaining < 0) { | ||
| return false; | ||
| } | ||
|
|
||
| // Convert minutesBefore to milliseconds | ||
| const alertThresholdMs = this.minutesBefore * 60 * 1000; | ||
|
|
||
| // Clean up old emitted events and get the current list | ||
| const emittedEvents = this._cleanupEmittedEvents(now); | ||
|
|
||
| // Check if we've already emitted this event | ||
| if (emittedEvents[event.id]) { | ||
| return false; | ||
| } | ||
|
|
||
| // Emit if time remaining is less than or equal to the alert threshold | ||
| if (timeRemaining <= alertThresholdMs) { | ||
| // Mark this event as emitted with its start time for future cleanup | ||
| emittedEvents[event.id] = startTime.getTime(); | ||
| this._setEmittedEvents(emittedEvents); | ||
| return true; | ||
| } | ||
|
|
||
| return false; | ||
| }, | ||
| generateMeta(event) { | ||
| const { | ||
| summary, | ||
| id, | ||
| } = event; | ||
| return { | ||
| summary: `Upcoming: ${summary || `Event ID: ${id}`}`, | ||
| id: `${id}-${Date.now()}`, | ||
| ts: Date.now(), | ||
| }; | ||
| }, | ||
| }, | ||
| hooks: { | ||
| async deploy() { | ||
| // On initial deploy, don't emit historical events | ||
| // Just initialize the emitted events tracker | ||
| this._setEmittedEvents({}); | ||
| }, | ||
| }, | ||
| sampleEmit, | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform"; | ||
| import microsoftOutlook from "../../microsoft_outlook_calendar.app.mjs"; | ||
| import sampleEmit from "./test-event.mjs"; | ||
|
|
||
| export default { | ||
| key: "microsoft_outlook_calendar-new-upcoming-event-polling", | ||
| name: "New Upcoming Calendar Event (Polling)", | ||
| description: "Emit new event based on a time interval before an upcoming calendar event. [See the documentation](https://docs.microsoft.com/en-us/graph/api/user-list-events)", | ||
| version: "0.0.1", | ||
| type: "source", | ||
| dedupe: "unique", | ||
| props: { | ||
| microsoftOutlook, | ||
| db: "$.service.db", | ||
| timer: { | ||
| type: "$.interface.timer", | ||
| default: { | ||
| intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL, | ||
| }, | ||
| }, | ||
| pollingInfo: { | ||
|
Check warning on line 21 in components/microsoft_outlook_calendar/sources/new-upcoming-event-polling/new-upcoming-event-polling.mjs
|
||
| type: "alert", | ||
| alertType: "info", | ||
| content: "Since this source executes based on a timer, event emission may be slightly delayed. For example, if the source runs every 5 minutes, the delay may be up to 5 minutes. You can use the `new-upcoming-event` source for instant event emission.", | ||
| }, | ||
| minutesBefore: { | ||
| type: "integer", | ||
| label: "Minutes Before", | ||
| description: "Number of minutes to trigger before the start of the calendar event.", | ||
| min: 0, | ||
| default: 30, | ||
| }, | ||
| }, | ||
| methods: { | ||
| _getEmittedEvents() { | ||
| return this.db.get("emittedEvents") || {}; | ||
| }, | ||
| _setEmittedEvents(emittedEvents) { | ||
| this.db.set("emittedEvents", emittedEvents); | ||
| }, | ||
| _cleanupEmittedEvents(now) { | ||
| const emittedEvents = this._getEmittedEvents(); | ||
| const cleanedEvents = {}; | ||
| let cleanedCount = 0; | ||
|
|
||
| // Keep only events that haven't passed yet | ||
| for (const [ | ||
| eventId, | ||
| startTime, | ||
| ] of Object.entries(emittedEvents)) { | ||
| if (startTime > now.getTime()) { | ||
| cleanedEvents[eventId] = startTime; | ||
| } else { | ||
| cleanedCount++; | ||
| } | ||
| } | ||
|
|
||
| if (cleanedCount > 0) { | ||
| console.log(`Cleaned up ${cleanedCount} past event(s) from emitted events tracker`); | ||
| this._setEmittedEvents(cleanedEvents); | ||
| } | ||
|
|
||
| return cleanedEvents; | ||
| }, | ||
| generateMeta(event) { | ||
| return { | ||
| id: `${event.id}-${Date.now()}`, | ||
| summary: `Upcoming: ${event.subject || `Event ID: ${event.id}`}`, | ||
| ts: Date.now(), | ||
| }; | ||
| }, | ||
| }, | ||
| hooks: { | ||
| async deploy() { | ||
| // On initial deploy, don't emit historical events | ||
| // Just initialize the emitted events tracker | ||
| this._setEmittedEvents({}); | ||
| }, | ||
| }, | ||
| async run() { | ||
| const now = new Date(); | ||
| const alertWindowMs = this.minutesBefore * 60 * 1000; | ||
| const timeMax = new Date(now.getTime() + alertWindowMs).toISOString(); | ||
|
|
||
| // Clean up old emitted events | ||
| const emittedEvents = this._cleanupEmittedEvents(now); | ||
|
|
||
| // Fetch events within the alert window | ||
| const { value: events } = await this.microsoftOutlook.listCalendarView({ | ||
| params: { | ||
| startDateTime: now.toISOString(), | ||
| endDateTime: timeMax, | ||
| $orderby: "start/dateTime", | ||
| }, | ||
| }); | ||
|
|
||
| if (!events || events.length === 0) { | ||
| console.log("No upcoming events found in the alert window"); | ||
| return; | ||
| } | ||
|
|
||
| for (const event of events) { | ||
| // Skip if already emitted | ||
| if (emittedEvents[event.id]) { | ||
| continue; | ||
| } | ||
|
|
||
| const startTime = event.start | ||
| ? new Date(event.start.dateTime) | ||
| : null; | ||
|
|
||
| if (!startTime) { | ||
| continue; | ||
| } | ||
|
|
||
| const timeRemaining = startTime.getTime() - now.getTime(); | ||
| if (timeRemaining < 0) { | ||
| continue; | ||
| } | ||
|
|
||
| const alertThresholdMs = this.minutesBefore * 60 * 1000; | ||
|
|
||
| // Emit if time remaining is less than or equal to the alert threshold | ||
| if (timeRemaining <= alertThresholdMs) { | ||
| emittedEvents[event.id] = startTime.getTime(); | ||
| this._setEmittedEvents(emittedEvents); | ||
|
|
||
| this.$emit(event, this.generateMeta(event)); | ||
| } | ||
| } | ||
| }, | ||
| sampleEmit, | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix timezone handling before computing alert windows
Microsoft Graph returns
event.start.dateTimein the calendar’s local time without an offset, paired withevent.start.timeZone. Parsing it withnew Date(event.start.dateTime)makes Node assume the runtime’s timezone (typically UTC), so the alert window is shifted by the user’s timezone offset. For example, a 9 AM Pacific event is treated as 9 AM UTC, causing the alert to fire ~7 hours early. Please normalize with the provided timezone before comparing againstnow.Also applies to: 108-129
🤖 Prompt for AI Agents