Skip to content
Open
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
2 changes: 1 addition & 1 deletion components/google_calendar/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/google_calendar",
"version": "0.5.13",
"version": "0.6.0",
"description": "Pipedream Google_calendar Components",
"main": "google_calendar.app.mjs",
"keywords": [
Expand Down
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

View workflow job for this annotation

GitHub Actions / Lint Code Base

Component prop pollingInfo must have a description. See https://pipedream.com/docs/components/guidelines/#props

Check warning on line 15 in components/google_calendar/sources/upcoming-event-alert-polling/upcoming-event-alert-polling.mjs

View workflow job for this annotation

GitHub Actions / Lint Code Base

Component prop pollingInfo must have a label. See https://pipedream.com/docs/components/guidelines/#props
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,
};

2 changes: 1 addition & 1 deletion components/microsoft_outlook_calendar/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/microsoft_outlook_calendar",
"version": "0.3.4",
"version": "0.4.0",
"description": "Pipedream Microsoft Outlook Calendar Components",
"main": "microsoft_outlook_calendar.app.mjs",
"keywords": [
Expand Down
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";
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix timezone handling before computing alert windows

Microsoft Graph returns event.start.dateTime in the calendar’s local time without an offset, paired with event.start.timeZone. Parsing it with new 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 against now.

@@
-import sampleEmit from "./test-event.mjs";
+import sampleEmit from "./test-event.mjs";
+import { DateTime } from "luxon";
@@
-      const startTime = event.start
-        ? new Date(event.start.dateTime)
-        : null;
-
-      if (!startTime) {
+      const start = event.start?.dateTime ?? event.start?.date;
+      if (!start) {
+        continue;
+      }
+      const zone = event.start?.timeZone
+        || event.originalStartTimeZone
+        || "UTC";
+      const startDateTime = DateTime.fromISO(start, { zone });
+      if (!startDateTime.isValid) {
+        continue;
+      }
+      const startTime = startDateTime.toJSDate();
+
+      if (!startTime) {
         continue;
       }
@@
-        emittedEvents[event.id] = startTime.getTime();
+        emittedEvents[event.id] = startTime.getTime();

Also applies to: 108-129

🤖 Prompt for AI Agents
In
components/microsoft_outlook_calendar/sources/new-upcoming-event-polling/new-upcoming-event-polling.mjs
around lines 1-3 and also apply same change to lines 108-129: the code currently
parses event.start.dateTime with new Date(...) which treats the timestamp as
runtime-local/UTC and ignores event.start.timeZone, shifting alert windows; fix
by parsing the ISO local date with the provided timeZone (e.g., use a
timezone-aware parser such as Luxon or moment-timezone) — create a DateTime by
combining event.start.dateTime and event.start.timeZone, convert that to a
consistent zone (preferably UTC) for comparisons with now, do the same for
event.end.dateTime, and use those normalized UTC timestamps when computing alert
windows and comparisons.


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

View workflow job for this annotation

GitHub Actions / Lint Code Base

Component prop pollingInfo must have a description. See https://pipedream.com/docs/components/guidelines/#props

Check warning on line 21 in components/microsoft_outlook_calendar/sources/new-upcoming-event-polling/new-upcoming-event-polling.mjs

View workflow job for this annotation

GitHub Actions / Lint Code Base

Component prop pollingInfo must have a label. See https://pipedream.com/docs/components/guidelines/#props
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,
};

Loading