Skip to content

Conversation

@KrishivGubba
Copy link

Closes #189

Plugin API: get-slots and set-slots

This PR implements the get-slots and set-slots methods for the plugin API, allowing external plugins to retrieve and update user availability for events.

Commits

This PR includes the following commits:

  • 4cf5333 - add get-slots event handler
  • ccd51db - add set-slots logic, add plugin utility methods
  • ecaa4b9 - finish get and set slots event handlers

Overview

A message handler (handleMessage) was added to the Event component that routes plugin API requests to the appropriate handler method. Plugins communicate with the frontend via window.postMessage, and responses are sent back using the same mechanism.

Message Handler

The handleMessage method validates incoming messages and routes them to the appropriate handler:

  • Messages with payload.type === "get-slots" are routed to getSlots()
  • Messages with payload.type === "set-slots" are routed to setSlots()

Request Format

All plugin API requests must follow this format:

window.postMessage({
  type: "FILL_CALENDAR_EVENT",
  requestId: "unique-request-id",  // Used to match requests with responses
  payload: {
    type: "get-slots" | "set-slots",
    // ... additional payload fields (see below)
  }
}, "*")

get-slots

Description

Retrieves availability slots for all respondents to an event. Returns slots in the user's local timezone (converted from UTC).

Request Format

{
  type: "FILL_CALENDAR_EVENT",
  requestId: "get-slots-123",
  payload: {
    type: "get-slots"
  }
}

Optional payload fields:

  • None (currently)

Response Format

Success response:

{
  type: "FILL_CALENDAR_EVENT_RESPONSE",
  command: "get-slots",
  requestId: "get-slots-123",
  ok: true,
  payload: {
    slots: {
      "userId1": {
        name: "John Doe",
        email: "john@example.com",
        availability: ["2026-01-07T09:00:00", "2026-01-07T09:15:00", ...],
        ifNeeded: ["2026-01-07T14:00:00", ...]
      },
      "guestName": {
        name: "guestName",  // Guest name as stored in localStorage
        email: "",  // May be empty for guests
        availability: [...],
        ifNeeded: [...]
      }
    },
    timeIncrement: 15  // Time increment in minutes (15, 30, or 60)
  }
}

Error response:

{
  type: "FILL_CALENDAR_EVENT_RESPONSE",
  command: "get-slots",
  requestId: "get-slots-123",
  ok: false,
  error: {
    message: "Error message here"
  }
}

Example Request

window.postMessage({
  type: "FILL_CALENDAR_EVENT",
  requestId: "test-get-slots-" + Date.now(),
  payload: {
    type: "get-slots"
  }
}, "*")

Timezone Conversion

  • Slots are stored in UTC in the backend
  • get-slots converts UTC timestamps to the user's local timezone before returning
  • Timezone is determined by (in priority order):
    1. localStorage["timezone"].value (if set)
    2. Browser's local timezone (Intl.DateTimeFormat().resolvedOptions().timeZone)
  • Returned timestamps are in ISO format without timezone (e.g., "2026-01-07T09:00:00")

Limitations

  • Privacy settings not enforced - When an event creator enables "Hides responses from respondents", responses should be hidden from non-creator respondents. However, the backend endpoint /events/{eventId}/responses?timeMin={timestamp}&timeMax={timestamp} currently returns all respondents regardless of this setting. As a result, get-slots may expose all respondents' details and availability even when privacy is enabled. I'll raise an issue to fix this soon, but I think it's a fix to be made on the backend and not to this API.

set-slots

Description

Sets availability slots for the current user (logged-in user or guest). Converts timestamps from the user's timezone to UTC before storing in the backend. Overwrites existing availability (does not merge).

Request Format

{
  type: "FILL_CALENDAR_EVENT",
  requestId: "set-slots-123",
  payload: {
    type: "set-slots",
    timezone: "America/Los_Angeles",  // Optional: IANA timezone name
    slots: [
      {
        start: "2026-01-07T09:00:00",  // ISO format without timezone
        end: "2026-01-07T12:00:00",
        status: "available" | "if-needed"
      },
      {
        start: "2026-01-07T14:00:00",
        end: "2026-01-07T16:00:00",
        status: "if-needed"
      }
    ]
  }
}

Required payload fields:

  • slots: Array of slot objects, each with:
    • start: Start time (ISO string without timezone)
    • end: End time (ISO string without timezone)
    • status: Either "available" or "if-needed"

Optional payload fields:

  • timezone: IANA timezone name (e.g., "America/Los_Angeles", "Asia/Kolkata"). If not provided, uses localStorage["timezone"].value or browser's local timezone.

Response Format

Success response:

{
  type: "FILL_CALENDAR_EVENT_RESPONSE",
  command: "set-slots",
  requestId: "set-slots-123",
  ok: true
}

Error response:

{
  type: "FILL_CALENDAR_EVENT_RESPONSE",
  command: "set-slots",
  requestId: "set-slots-123",
  ok: false,
  error: {
    message: "Error message here"
  }
}

Example Request

window.postMessage({
  type: "FILL_CALENDAR_EVENT",
  requestId: "test-set-slots-" + Date.now(),
  payload: {
    type: "set-slots",
    timezone: "Asia/Kolkata",  // IST
    slots: [
      {
        start: "2026-01-07T09:00:00",
        end: "2026-01-07T12:00:00",
        status: "available"
      },
      {
        start: "2026-01-07T12:00:00",
        end: "2026-01-07T16:00:00",
        status: "if-needed"
      }
    ]
  }
}, "*")

Timezone Conversion

  • Input timestamps are assumed to be in the specified timezone (or user's local timezone if not specified)
  • set-slots converts these timestamps to UTC before sending to the backend
  • Timezone is determined by (in priority order):
    1. payload.timezone (if provided in the request)
    2. localStorage["timezone"].value (if set)
    3. Browser's local timezone (Intl.DateTimeFormat().resolvedOptions().timeZone)
  • Timestamps are split into intervals based on the event's timeIncrement (15, 30, or 60 minutes)

Validation

  • Validates that all slots fall within the event's date/time range
  • Validates that start < end for each slot
  • Validates that status is either "available" or "if-needed"
  • Returns appropriate error messages if validation fails

Limitations

  • Group events are not supported - returns an error if the event type is GROUP. I can get to developing this too soon.
  • Only works for the current user - If you're logged in and add guest availability through the UI, set-slots will NOT work for those guest accounts. It only works for:
    • Logged-in users (updates their own availability)
    • Guests who have already set their name (see below)
  • Guest name required - For guests who haven't logged in, the guest name must already exist in localStorage[eventId + ".guestName"]. This means guests must add their availability through the UI at least once before using the plugin API. Returns an error if guest name is not found.

How It Works

  1. Determines if current user is logged-in or guest
  2. For guests: Retrieves guest name from localStorage
  3. Converts all slot timestamps from user's timezone to UTC
  4. Validates slots against event's date/time range
  5. Splits slots into intervals based on event.timeIncrement
  6. Sends a single POST request to /events/:eventId/response (overwrites existing availability)
  7. Refreshes the event to update the UI

Implementation Details

Helper Functions Added

  • convertUTCSlotsToLocalISO() - Converts UTC timestamps to user's local timezone (ISO format without timezone)
  • Moved to frontend/src/utils/date_utils.js for reusability

Code Organization

  • Plugin API message handling: frontend/src/views/Event.vue (handleMessage, getSlots, setSlots)
  • Plugin utilities: frontend/src/utils/plugin_utils.js (sendPluginSuccess, sendPluginError, isValidPluginMessage)
  • Date utilities: frontend/src/utils/date_utils.js (convertUTCSlotsToLocalISO, convertToUTC, isTimeWithinEventRange)

Testing

Use the browser console to test (don't forget to add listeners to intercept error/success messages):

Get slots:

// Add listener first
window.addEventListener("message", (e) => {
  if (e.data?.type === "FILL_CALENDAR_EVENT_RESPONSE" && e.data?.command === "get-slots") {
    if (e.data.ok) {
      console.log("Success:", e.data.payload)
    } else {
      console.error("Error:", e.data.error)
    }
  }
})

// Then send request
window.postMessage({
  type: "FILL_CALENDAR_EVENT",
  requestId: "test-" + Date.now(),
  payload: { type: "get-slots" }
}, "*")

Set slots:

// Add listener first
window.addEventListener("message", (e) => {
  if (e.data?.type === "FILL_CALENDAR_EVENT_RESPONSE" && e.data?.command === "set-slots") {
    if (e.data.ok) {
      console.log("Success: Slots updated")
    } else {
      console.error("Error:", e.data.error)
    }
  }
})

// Then send request
window.postMessage({
  type: "FILL_CALENDAR_EVENT",
  requestId: "test-" + Date.now(),
  payload: {
    type: "set-slots",
    timezone: "Asia/Kolkata",
    slots: [
      { start: "2026-01-07T09:00:00", end: "2026-01-07T12:00:00", status: "available" }
    ]
  }
}, "*")

}) => {
const clientId =
"523323684219-jfakov2bgsleeb6den4ktpohq4lcnae2.apps.googleusercontent.com"
"447891895572-fkf9aidtg8cr356u6cbgvrm0gruh4t65.apps.googleusercontent.com"
Copy link
Member

Choose a reason for hiding this comment

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

make sure to change this back before the final commit, fine for testing

Copy link
Author

Choose a reason for hiding this comment

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

yep, will do that in the commit with the docs

<v-btn
v-if="event.startOnMonday ? weekOffset != 1 : weekOffset != 0"
:icon="isPhone"
text
Copy link
Member

Choose a reason for hiding this comment

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

any reason why this was removed?

Copy link
Author

Choose a reason for hiding this comment

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

probably by mistake, added it back for the next commit.

@jonyTF
Copy link
Member

jonyTF commented Jan 6, 2026

Great work so far! Some notes:

  1. Doesn't work for DOW ("days of the week") events, these days use hardcoded dates from 2018 to reference Sunday - Monday, see this for details on what those hardcoded dates are. I think it's fine for the API user to have to use these hardcoded dates to reference Sunday - Monday when inputting times for DOW events, as long as they are properly documented
  2. Question: Any reason why we can't add an additional "guestName" prop to edit guest availability?

@KrishivGubba
Copy link
Author

  1. Doesn't work for DOW ("days of the week") events, these days use hardcoded dates from 2018 to reference Sunday - Monday, see this for details on what those hardcoded dates are. I think it's fine for the API user to have to use these hardcoded dates to reference Sunday - Monday when inputting times for DOW events, as long as they are properly documented

Fixed this. I added validation to ensure that the inputted dates belong to the set of DOW constants in case the event is of type eventTypes.DOW.
+ i'll add docs for the api next.

  1. Question: Any reason why we can't add an additional "guestName" prop to edit guest availability?

valid, added this capability. if a user is not logged in and haven't already added availability as a guest (i.e. localStorage[guestID] is empty) they can now provide a guestName (and guestEmail if the event requires email collection) prop in the set-slots payload.

@jonyTF
Copy link
Member

jonyTF commented Jan 6, 2026

valid, added this capability. if a user is not logged in and haven't already added availability as a guest (i.e. localStorage[guestID] is empty) they can now provide a guestName (and guestEmail if the event requires email collection) prop in the set-slots payload.

I think it's a little awkward to have it so that users who are logged in or added availability as a guest cannot add / edit guest availability.

Is it possible to make it so that if guestName is not provided, it populates the logged in user's availability (or existing guest availability), and otherwise populate the guestName availability?

@KrishivGubba
Copy link
Author

I think it's a little awkward to have it so that users who are logged in or added availability as a guest cannot add / edit guest availability.

Is it possible to make it so that if guestName is not provided, it populates the logged in user's availability (or existing guest availability), and otherwise populate the guestName availability?

@jonyTF , done, updated this. logic is now basically:

  • If guestName is provided in the getSlots payload

    • If a guest with that name already exists, their availability is updated with the new slots
    • If no such guest exists, a new guest is created with that guestName and their availability is added to the event
  • If guestName is not provided

    • It falls back to the old flow and populates/updates availability for:
      • the logged-in user, or
      • existing guest availability (if acting as guest)

@jonyTF
Copy link
Member

jonyTF commented Jan 7, 2026

Yay the new logic works. Last thing:

  • DOW events are still getting this error when trying to use the 2018 dates:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API to interact with Schej

2 participants