Files
cal.diy/companion/services/calcom.ts
T
88ce7868e8 feat: companion expo and chrome (#25022)
* feat: companion expo and chrome

* fix: api-v2-fetch-bookings (#25023)

* migrated to app expo router

* feat(companion): added availability screen and fix event type fetch (#25025)

* fix(companion): add host check in bookings (#25036)

* using liquid glass bottom nav experimental

* added action sheet

* added icons to event type tabs

* added action bar for event-type details

* long press for edit event types

* added limits

* added more limits

* added more limits

* added username to event types

* feat(companion): config to set and show all the available meeting options a user has stalled (#25081)

* feat: Add tailwind support using nativewind. (#25028)

* init nativewind

* replace styles with tailwind classes

* changes

* feat(companion): all locations options, icons, and apply nativewind to remaining files (#25143)

* feat: all locations options and icons and apply nativwind to reamaining files

* icon bug fix

* cal video icon

* other location frontend

* other location api

* address cubics comment about correctly detects SVGs

* feat(companion): edit availability (#25149)

* feat: companion-edit-availability

* date override and use correct config for patch api endpoint

* Android Alert supports a maximum of 3 buttons

* feat: companion-ui-release-1 (#25150)

* feat(companion): Add event type & schedule creation flows with API expansion (#25187)

* feat(companion): event schedule creation and apis

* profile section

* limits, advanced, recurring tab

* Clean Folder Structure

* addressed cubics comments

* fix cubiics comments

* replaced text with cal.com svg logo

* added correct more button for event-types

* added copy link button

* added extension popup

* added chrome extension sidebar

* achieved liquid glass buttons

* nit

* more progress

* fixed ios bug, added icon

* added cal icon to gmail

* nit

* nit

* added dialog for one-off meeting

* added one-time dialog

* feat(companion): gmail plugin (#25327)

* Update Cal.com Companion to version 1.7.0 with enhanced background and content scripts. The background script now handles fetching event types from the Cal.com API and improved error handling. The content script has been updated to manage sidebar visibility and respond to messages more effectively. Removed deprecated dev background script and updated manifest permissions for better functionality.

* chore: update companion .gitignore to exclude build outputs and env files

* feat: insert Cal.com event link directly at cursor position in Gmail compose

- Add insertTextAtCursor function to insert booking link where user is typing
- Automatically positions cursor after inserted link for seamless typing
- Fallback to clipboard copy if compose field not found
- Improved UX: no need to manually paste the link

* feat(companion): improve notification UI and add event types caching

- Update notification to Cal.com brand style (black bg, check icon, smooth fade)
- Change notification text to 'Link inserted' for subtlety
- Move notification to bottom-right position (matching Cal.com toasts)
- Add event types caching (5min duration) to reduce API calls
- Cache automatically refreshes on page reload
- Fix TypeScript error with composeBody.focus() type assertion

* Notification toast  border

* conditional description

* conditional description

* feat(companion): Enhance UI with action buttons, tooltips, and Gmail extension improvements (#25353)

* feat: companion-and-gmail-ui-upgrade

* feat: companion-and-gmail-ui-upgrade

* feat: companion-and-gmail-ui-upgrade

* feat: companion-and-gmail-ui-upgrade

* feat(companion): one-off links (#25355)

* added one off prototype

* resolve merge conflicts

---------

Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>

* fixed sidebar iframe

* fix: enable pointer events on sidebar while maintaining click-through on transparent area

- Added pointerEvents: 'auto' to sidebar View component in _layout.tsx
- Changed iframe width from 100% to 400px to match sidebar width
- Changed iframe pointerEvents from 'none' to 'auto' to enable clicking
- This allows sidebar items to be clickable while transparent area remains click-through

* feat(companion): center all modals on full screen in browser extension (#25380)

* fix: companion-full-width-iframe

* center the popups on extension view

* feat(companion): center all modals on full screen in browser extension

- Created FullScreenModal component that expands iframe to full width
- Updated content script to handle instant iframe expansion/collapse
- Converted 17 modals across 6 files to use FullScreenModal
- All modals now appear centered on full browser viewport
- Removed animations for instant modal appearance
- Mobile app behavior remains unchanged

Files updated:
- companion/components/FullScreenModal.tsx (new)
- companion/extension/entrypoints/content.ts
- companion/app/(tabs)/event-types.tsx (5 modals)
- companion/app/(tabs)/bookings.tsx (2 modals)
- companion/app/(tabs)/availability.tsx (3 modals)
- companion/app/booking-detail.tsx (2 modals)
- companion/app/availability-detail.tsx (4 modals)
- companion/components/Header.tsx (1 modal)

* security(companion): validate postMessage origin in content script

Add origin and source validation to postMessage listener to prevent
malicious scripts on host pages from manipulating the companion iframe.

- Verify event.source matches iframe.contentWindow
- Verify event.origin matches iframe origin (localhost:8081)
- Reject all unauthorized messages before processing

This prevents external scripts from:
- Forcing full-screen overlays
- Intercepting pointer events
- Manipulating the companion UI

* fix(companion): center 'Add new event type' modal on full screen

Replace KeyboardAvoidingView with TouchableOpacity pattern in the
create event type modal to ensure it appears centered on the full
screen like all other modals.

This was the last remaining modal that wasn't properly centered.

* fix(companion): resolve modal transition timing issue

Fixed the 'Add new event type' modal not centering properly by:

- Added 100ms delay when transitioning between modals
- Prevents conflicting expand/collapse messages
- Removed redundant postMessage calls from modal handlers
- Added max-height constraint to modal content
- Cleaned up debug console logs

The issue was that when clicking 'New Event Type' from the 'New' menu,
both modals were transitioning simultaneously, causing the create modal
to collapse immediately. The delay ensures proper sequencing.

All 17 modals now work correctly and appear centered on full screen.

* added working date range

* removed all of one-off event code

* nit

* feat(companion): Suggest `Cal.com` links and Embed using Gemini Ai in Gmail (#25406)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* add companion build on CI

* feat: oauth companion (#25526)

* first implemenation of oauth

* fix logout for web

* revert app name

* remove isUsingOAuth

* clean up log out button

* remove logs

* clean up authcontext

* simplify code generation

* code clean up

* clean up oauthService.ts

* oauth browser extension

* add redirect uri to .env.example

* remove api key support

* clean up _layout file

* add identity

* fix + clean up

* improvements to oauth flow

* code clean up

* remove comments

* clean up comments

* add log out dialog

* add confirm modal

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

---------

Co-authored-by: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com>
Co-authored-by: Abhijeet Singh <asingh9829@gmail.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
Co-authored-by: Volnei Munhoz <volnei.munhoz@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-12-03 08:46:04 -03:00

1147 lines
33 KiB
TypeScript

import type {
EventType,
CreateEventTypeInput,
GetEventTypesResponse,
Booking,
GetBookingsResponse,
BookingParticipationResult,
Schedule,
GetSchedulesResponse,
GetScheduleResponse,
UserProfile,
ConferencingOption,
Webhook,
CreateWebhookInput,
UpdateWebhookInput,
PrivateLink,
CreatePrivateLinkInput,
UpdatePrivateLinkInput,
} from "./types";
const API_BASE_URL = "https://api.cal.com/v2";
// Authentication configuration
interface AuthConfig {
accessToken?: string;
refreshToken?: string;
}
// Global auth state
const authConfig: AuthConfig = {};
// Token refresh callback - will be set by AuthContext
let tokenRefreshCallback: ((accessToken: string, refreshToken?: string) => Promise<void>) | null =
null;
// Refresh token function - will be set by AuthContext
let refreshTokenFunction:
| ((refreshToken: string) => Promise<{ accessToken: string; refreshToken?: string }>)
| null = null;
// Re-export types for backward compatibility
export type {
EventType,
CreateEventTypeInput,
Booking,
BookingParticipationResult,
Schedule,
UserProfile,
ConferencingOption,
Webhook,
CreateWebhookInput,
UpdateWebhookInput,
PrivateLink,
CreatePrivateLinkInput,
UpdatePrivateLinkInput,
};
export const getBookingParticipation = (
booking: Booking,
userId?: number,
userEmail?: string
): BookingParticipationResult => {
const normalizedUserEmail = userEmail?.toLowerCase();
const emailEq = (a?: string, b?: string) => a?.toLowerCase() === b?.toLowerCase();
const idEq = (a?: string | number, b?: string | number) =>
a !== undefined && b !== undefined && String(a) === String(b);
const isOrganizer =
!!booking.user &&
(idEq(booking.user.id, userId) || emailEq(booking.user.email, normalizedUserEmail));
const isHost =
Array.isArray(booking.hosts) &&
booking.hosts.some((host) => idEq(host.id, userId) || emailEq(host.email, normalizedUserEmail));
const isAttendee =
Array.isArray(booking.attendees) &&
booking.attendees.some(
(attendee) => idEq(attendee.id, userId) || emailEq(attendee.email, normalizedUserEmail)
);
const isParticipating = isOrganizer || isHost || isAttendee;
return { isOrganizer, isHost, isAttendee, isParticipating };
};
export class CalComAPIService {
private static userProfile: UserProfile | null = null;
/**
* Set OAuth access token for authentication
*/
static setAccessToken(accessToken: string, refreshToken?: string): void {
authConfig.accessToken = accessToken;
if (refreshToken) {
authConfig.refreshToken = refreshToken;
}
}
/**
* Set refresh token function for automatic token refresh
*/
static setRefreshTokenFunction(
refreshFn: (refreshToken: string) => Promise<{ accessToken: string; refreshToken?: string }>
): void {
refreshTokenFunction = refreshFn;
}
/**
* Clear all authentication
*/
static clearAuth(): void {
authConfig.accessToken = undefined;
authConfig.refreshToken = undefined;
tokenRefreshCallback = null;
refreshTokenFunction = null;
}
/**
* Set token refresh callback for OAuth token refresh
*/
static setTokenRefreshCallback(
callback: (accessToken: string, refreshToken?: string) => Promise<void>
): void {
tokenRefreshCallback = callback;
}
/**
* Get current authentication header
*/
private static getAuthHeader(): string {
if (authConfig.accessToken) {
return `Bearer ${authConfig.accessToken}`;
} else {
throw new Error("No authentication configured. Please sign in with OAuth.");
}
}
// Get current user profile
static async getCurrentUser(): Promise<UserProfile> {
try {
const response = await this.makeRequest<{ status: string; data: UserProfile }>(
"/me",
{
headers: {
"cal-api-version": "2024-06-11",
},
},
"2024-06-11"
);
return response.data;
} catch (error) {
throw error;
}
}
// Update current user profile
static async updateUserProfile(updates: {
email?: string;
name?: string;
timeFormat?: number;
defaultScheduleId?: number;
weekStart?: string;
timeZone?: string;
locale?: string;
avatarUrl?: string;
bio?: string;
metadata?: Record<string, any>;
}): Promise<UserProfile> {
try {
const response = await this.makeRequest<{ status: string; data: UserProfile }>(
"/me",
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
"cal-api-version": "2024-06-11",
},
body: JSON.stringify(updates),
},
"2024-06-11"
);
if (response && response.data) {
// Update cached profile
this.userProfile = response.data;
return response.data;
}
throw new Error("Invalid response from update user profile API");
} catch (error) {
console.error("updateUserProfile error:", error);
throw error;
}
}
// Get and cache user profile
static async getUserProfile(): Promise<UserProfile> {
if (this.userProfile) {
return this.userProfile;
}
this.userProfile = await this.getCurrentUser();
return this.userProfile;
}
// Get cached username or fetch if not available
static async getUsername(): Promise<string> {
const profile = await this.getUserProfile();
return profile.username;
}
// Build shareable link for event type
static async buildEventTypeLink(eventTypeSlug: string): Promise<string> {
const username = await this.getUsername();
return `https://cal.com/${username}/${eventTypeSlug}`;
}
// Clear cached profile (useful for logout)
static clearUserProfile(): void {
this.userProfile = null;
}
// Test function for bookings API specifically
static async testRawBookingsAPI(): Promise<void> {
try {
const url = `${API_BASE_URL}/bookings?status=upcoming&status=unconfirmed&limit=50`;
const response = await fetch(url, {
headers: {
Authorization: this.getAuthHeader(),
"Content-Type": "application/json",
"cal-api-version": "2024-08-13",
},
});
const responseText = await response.text();
try {
const responseJson = JSON.parse(responseText);
} catch (parseError) {}
} catch (error) {}
}
private static async makeRequest<T>(
endpoint: string,
options: RequestInit = {},
apiVersion: string = "2024-08-13",
isRetry: boolean = false
): Promise<T> {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
Authorization: this.getAuthHeader(),
"Content-Type": "application/json",
"cal-api-version": apiVersion,
...options.headers,
},
});
if (!response.ok) {
const errorBody = await response.text();
// Parse error for better user messages
let errorMessage = response.statusText;
try {
const errorJson = JSON.parse(errorBody);
errorMessage = errorJson?.error?.message || errorJson?.message || response.statusText;
} catch (parseError) {
// If JSON parsing fails, use the raw error body
errorMessage = errorBody || response.statusText;
}
// Handle specific error cases
if (response.status === 401) {
if (!isRetry && authConfig.refreshToken && refreshTokenFunction && tokenRefreshCallback) {
try {
const newTokens = await refreshTokenFunction(authConfig.refreshToken);
authConfig.accessToken = newTokens.accessToken;
if (newTokens.refreshToken) {
authConfig.refreshToken = newTokens.refreshToken;
}
// Notify AuthContext to update stored tokens
await tokenRefreshCallback(newTokens.accessToken, newTokens.refreshToken);
// Retry the original request with the new token
return this.makeRequest<T>(endpoint, options, apiVersion, true);
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
this.clearAuth();
throw new Error("Authentication failed. Please sign in again.");
}
}
if (errorMessage.includes("expired")) {
throw new Error("Your authentication has expired. Please sign in again.");
}
throw new Error("Authentication failed. Please check your credentials.");
}
throw new Error(`API Error: ${errorMessage}`);
}
return response.json();
}
// Delete an event type
static async deleteEventType(eventTypeId: number): Promise<void> {
try {
console.log(`Deleting event type with ID: ${eventTypeId}`);
await this.makeRequest(
`/event-types/${eventTypeId}`,
{
method: "DELETE",
},
"2024-06-14"
);
console.log("Delete completed");
} catch (error) {
console.error("Delete API error:", error);
throw error;
}
}
// Create an event type
static async createEventType(input: CreateEventTypeInput): Promise<EventType> {
try {
console.log("Creating event type with input:", JSON.stringify(input, null, 2));
const response = await this.makeRequest<{ status: string; data: EventType }>(
"/event-types",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"cal-api-version": "2024-06-14",
},
body: JSON.stringify(input),
},
"2024-06-14"
);
if (response && response.data) {
console.log("Event type created successfully:", response.data);
return response.data;
}
throw new Error("Invalid response from create event type API");
} catch (error) {
console.error("createEventType error:", error);
throw error;
}
}
// Cancel a booking
static async cancelBooking(bookingUid: string, reason?: string): Promise<void> {
try {
const body: { reason?: string } = {};
if (reason) {
body.reason = reason;
}
await this.makeRequest(`/bookings/${bookingUid}/cancel`, {
method: "POST",
body: JSON.stringify(body),
});
} catch (error) {
throw error;
}
}
// Reschedule a booking
static async rescheduleBooking(
bookingUid: string,
input: {
start: string; // ISO 8601 datetime string
reschedulingReason?: string;
}
): Promise<Booking> {
try {
console.log(`Rescheduling booking ${bookingUid} to:`, input.start);
const response = await this.makeRequest<{ status: string; data: Booking }>(
`/bookings/${bookingUid}/reschedule`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"cal-api-version": "2024-08-13",
},
body: JSON.stringify(input),
},
"2024-08-13"
);
if (response && response.data) {
console.log("Booking rescheduled successfully:", response.data);
return response.data;
}
throw new Error("Invalid response from reschedule booking API");
} catch (error) {
console.error("rescheduleBooking error:", error);
throw error;
}
}
static async getEventTypes(): Promise<EventType[]> {
try {
// Get current user to extract username
let username: string | undefined;
try {
const currentUser = await this.getCurrentUser();
// Extract username from response
if (currentUser?.username) {
username = currentUser.username;
}
} catch (error) {}
// Build query string with username if available
const params = new URLSearchParams();
if (username) {
params.append("username", username);
}
const queryString = params.toString();
const endpoint = `/event-types${queryString ? `?${queryString}` : ""}`;
const response = await this.makeRequest<any>(endpoint, {}, "2024-06-14");
// Handle different possible response structures
let eventTypesArray: EventType[] = [];
if (Array.isArray(response)) {
eventTypesArray = response;
} else if (response && typeof response === "object") {
if (response.data && Array.isArray(response.data)) {
eventTypesArray = response.data;
} else if (response.eventTypes && Array.isArray(response.eventTypes)) {
eventTypesArray = response.eventTypes;
} else if (response.items && Array.isArray(response.items)) {
eventTypesArray = response.items;
} else if (response.data && typeof response.data === "object") {
if (response.data.eventTypes && Array.isArray(response.data.eventTypes)) {
eventTypesArray = response.data.eventTypes;
} else if (response.data.items && Array.isArray(response.data.items)) {
eventTypesArray = response.data.items;
} else {
const keys = Object.keys(response.data);
if (keys.length > 0) {
eventTypesArray = Object.values(response.data).filter(
(item): item is EventType => item && typeof item === "object" && "id" in item
) as EventType[];
}
}
} else {
const possibleArrays = Object.values(response).filter((val) => Array.isArray(val));
if (possibleArrays.length > 0) {
eventTypesArray = possibleArrays[0] as EventType[];
}
}
}
return eventTypesArray;
} catch (error) {
throw error;
}
}
static async getBookingByUid(bookingUid: string): Promise<Booking> {
try {
const response = await this.makeRequest<{ status: string; data: Booking }>(
`/bookings/${bookingUid}`,
{
headers: {
"cal-api-version": "2024-08-13",
},
},
"2024-08-13"
);
console.log("getBookingByUid raw response:", JSON.stringify(response, null, 2));
if (response && response.data) {
console.log("getBookingByUid booking data:", JSON.stringify(response.data, null, 2));
console.log("getBookingByUid user field:", response.data.user);
console.log("getBookingByUid hosts field:", response.data.hosts);
console.log("getBookingByUid attendees field:", response.data.attendees);
return response.data;
}
throw new Error("Invalid response from get booking API");
} catch (error) {
console.error("getBookingByUid error:", error);
throw error;
}
}
static async getBookings(filters?: {
status?: string[];
fromDate?: string;
toDate?: string;
eventTypeId?: number;
limit?: number;
offset?: number;
}): Promise<Booking[]> {
try {
// Build query parameters
const params = new URLSearchParams();
if (filters?.status?.length) {
filters.status.forEach((status) => params.append("status", status));
}
if (filters?.fromDate) {
params.append("fromDate", filters.fromDate);
}
if (filters?.toDate) {
params.append("toDate", filters.toDate);
}
if (filters?.eventTypeId) {
params.append("eventTypeId", filters.eventTypeId.toString());
}
if (filters?.limit) {
params.append("limit", filters.limit.toString());
}
if (filters?.offset) {
params.append("offset", filters.offset.toString());
}
const queryString = params.toString();
const endpoint = `/bookings${queryString ? `?${queryString}` : ""}`;
const response = await this.makeRequest<any>(endpoint);
// Handle different possible response structures (same logic as event types)
let bookingsArray: Booking[] = [];
if (Array.isArray(response)) {
bookingsArray = response;
} else if (response && typeof response === "object") {
if (response.data && Array.isArray(response.data)) {
bookingsArray = response.data;
} else if (response.bookings && Array.isArray(response.bookings)) {
bookingsArray = response.bookings;
} else if (response.items && Array.isArray(response.items)) {
bookingsArray = response.items;
} else if (response.data && typeof response.data === "object") {
if (response.data.bookings && Array.isArray(response.data.bookings)) {
bookingsArray = response.data.bookings;
} else if (response.data.items && Array.isArray(response.data.items)) {
bookingsArray = response.data.items;
} else {
// Convert object values to array as last resort
const keys = Object.keys(response.data);
if (keys.length > 0) {
bookingsArray = Object.values(response.data).filter(
(item): item is Booking =>
item && typeof item === "object" && ("id" in item || "uid" in item)
) as Booking[];
}
}
} else {
// Try to extract any arrays from the response
const possibleArrays = Object.values(response).filter((val) => Array.isArray(val));
if (possibleArrays.length > 0) {
bookingsArray = possibleArrays[0] as Booking[];
}
}
}
// Get current user to filter bookings
let currentUser;
try {
currentUser = await this.getCurrentUser();
} catch (error) {
return bookingsArray;
}
// Extract user info from response
let userId: number | undefined;
let userEmail: string | undefined;
if (currentUser) {
userId = currentUser.id;
userEmail = currentUser.email;
}
// Filter bookings to only show ones where the current user is participating
const userBookings = bookingsArray.filter((booking) => {
const { isOrganizer, isHost, isAttendee, isParticipating } = getBookingParticipation(
booking,
userId,
userEmail
);
return isParticipating;
});
return userBookings;
} catch (error) {
throw error;
}
}
// Get all schedules
static async getSchedules(): Promise<Schedule[]> {
try {
const response = await this.makeRequest<{ status: string; data: Schedule[] }>(
"/schedules",
{
headers: {
"cal-api-version": "2024-06-11", // Override version for schedules
},
},
"2024-06-11"
);
let schedulesArray: Schedule[] = [];
// Handle response structure: { status: "success", data: [...] }
if (
response &&
response.status === "success" &&
response.data &&
Array.isArray(response.data)
) {
schedulesArray = response.data;
} else if (Array.isArray(response)) {
// Fallback: response might be array directly
schedulesArray = response;
} else if (response && response.data && Array.isArray(response.data)) {
// Fallback: response.data might be array
schedulesArray = response.data;
}
return schedulesArray;
} catch (error) {
throw error;
}
}
// Create a new schedule
static async createSchedule(input: {
name: string;
timeZone: string;
isDefault?: boolean;
availability?: Array<{
days: string[]; // e.g., ["Monday", "Tuesday"]
startTime: string; // e.g., "09:00"
endTime: string; // e.g., "17:00"
}>;
overrides?: Array<{
date: string; // e.g., "2024-05-20"
startTime: string; // e.g., "12:00"
endTime: string; // e.g., "14:00"
}>;
}): Promise<Schedule> {
try {
console.log("Creating schedule with input:", JSON.stringify(input, null, 2));
const response = await this.makeRequest<{ status: string; data: Schedule }>(
"/schedules",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"cal-api-version": "2024-06-11",
},
body: JSON.stringify(input),
},
"2024-06-11"
);
if (response && response.data) {
console.log("Schedule created successfully:", response.data);
return response.data;
}
throw new Error("Invalid response from create schedule API");
} catch (error) {
console.error("createSchedule error:", error);
throw error;
}
}
// Get specific schedule by ID
static async getScheduleById(scheduleId: number): Promise<Schedule | null> {
try {
const response = await this.makeRequest<any>(
`/schedules/${scheduleId}`,
{
headers: {
"cal-api-version": "2024-06-11", // Override version for schedules
},
},
"2024-06-11"
);
console.log("getScheduleById raw response:", JSON.stringify(response, null, 2));
if (response && response.data) {
console.log("Returning schedule data:", response.data);
return response.data;
}
// Sometimes the response might be the schedule directly
if (response && response.id) {
console.log("Returning schedule directly:", response);
return response;
}
console.log("No schedule data found in response");
return null;
} catch (error) {
console.error("getScheduleById error:", error);
throw error;
}
}
// Get conferencing options
static async getConferencingOptions(): Promise<ConferencingOption[]> {
try {
const response = await this.makeRequest<{ status: string; data: ConferencingOption[] }>(
"/conferencing"
);
if (response && response.data && Array.isArray(response.data)) {
return response.data;
}
return [];
} catch (error) {
console.error("getConferencingOptions error:", error);
throw error;
}
}
// Get a single event type by ID
static async getEventTypeById(eventTypeId: number): Promise<EventType | null> {
try {
const response = await this.makeRequest<{ status: string; data: EventType }>(
`/event-types/${eventTypeId}`,
{},
"2024-06-14"
);
if (response && response.data) {
return response.data;
}
return null;
} catch (error) {
// Handle 404 errors gracefully - resource doesn't exist or user doesn't have access
if (error instanceof Error) {
// Check if error message contains 404 status code
const statusMatch = error.message.match(/API Error: (\d+)/);
if (statusMatch && statusMatch[1] === "404") {
console.warn(`Event type ${eventTypeId} not found`);
return null;
}
}
console.error("getEventTypeById error:", error);
throw error;
}
}
// Update an event type
static async updateEventType(
eventTypeId: number,
updates: Partial<CreateEventTypeInput>
): Promise<EventType> {
try {
console.log(`Updating event type ${eventTypeId} with:`, JSON.stringify(updates, null, 2));
const response = await this.makeRequest<{ status: string; data: EventType }>(
`/event-types/${eventTypeId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
"cal-api-version": "2024-06-14",
},
body: JSON.stringify(updates),
},
"2024-06-14"
);
if (response && response.data) {
console.log("Event type updated successfully:", response.data);
return response.data;
}
throw new Error("Invalid response from update event type API");
} catch (error) {
console.error("updateEventType error:", error);
throw error;
}
}
// Update a schedule
static async updateSchedule(
scheduleId: number,
updates: {
isDefault?: boolean;
name?: string;
timeZone?: string;
availability?: Array<{
days: string[]; // Day names like "Monday", "Tuesday"
startTime: string; // Format: "09:00"
endTime: string; // Format: "10:00"
}>;
overrides?: Array<{
date: string; // Format: "2024-05-20"
startTime: string; // Format: "12:00"
endTime: string; // Format: "14:00"
}>;
}
): Promise<Schedule> {
try {
const response = await this.makeRequest<{ status: string; data: Schedule }>(
`/schedules/${scheduleId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
"cal-api-version": "2024-06-11",
},
body: JSON.stringify(updates),
},
"2024-06-11"
);
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from update schedule API");
} catch (error) {
console.error("updateSchedule error:", error);
throw error;
}
}
// Duplicate a schedule
static async duplicateSchedule(scheduleId: number): Promise<Schedule> {
try {
const response = await this.makeRequest<{ status: string; data: Schedule }>(
`/atoms/schedules/${scheduleId}/duplicate`,
{
method: "POST",
headers: {
"cal-api-version": "2024-06-11",
},
},
"2024-06-11"
);
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from duplicate schedule API");
} catch (error) {
console.error("duplicateSchedule error:", error);
throw error;
}
}
// Delete a schedule
static async deleteSchedule(scheduleId: number): Promise<void> {
try {
await this.makeRequest<{ status: string }>(
`/schedules/${scheduleId}`,
{
method: "DELETE",
headers: {
"cal-api-version": "2024-06-11",
},
},
"2024-06-11"
);
} catch (error) {
console.error("deleteSchedule error:", error);
throw error;
}
}
// ============================================
// WEBHOOKS
// ============================================
// Get all global webhooks
static async getWebhooks(): Promise<Webhook[]> {
try {
const response = await this.makeRequest<{ status: string; data: Webhook[] }>("/webhooks");
if (response && response.data && Array.isArray(response.data)) {
return response.data;
}
return [];
} catch (error) {
console.error("getWebhooks error:", error);
throw error;
}
}
// Create a global webhook
static async createWebhook(input: CreateWebhookInput): Promise<Webhook> {
try {
const response = await this.makeRequest<{ status: string; data: Webhook }>("/webhooks", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
});
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from create webhook API");
} catch (error) {
console.error("createWebhook error:", error);
throw error;
}
}
// Update a global webhook
static async updateWebhook(webhookId: string, updates: UpdateWebhookInput): Promise<Webhook> {
try {
const response = await this.makeRequest<{ status: string; data: Webhook }>(
`/webhooks/${webhookId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updates),
}
);
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from update webhook API");
} catch (error) {
console.error("updateWebhook error:", error);
throw error;
}
}
// Delete a global webhook
static async deleteWebhook(webhookId: string): Promise<void> {
try {
await this.makeRequest(`/webhooks/${webhookId}`, {
method: "DELETE",
});
} catch (error) {
console.error("deleteWebhook error:", error);
throw error;
}
}
// Get webhooks for a specific event type
static async getEventTypeWebhooks(eventTypeId: number): Promise<Webhook[]> {
try {
const response = await this.makeRequest<{ status: string; data: Webhook[] }>(
`/event-types/${eventTypeId}/webhooks`
);
if (response && response.data && Array.isArray(response.data)) {
return response.data;
}
return [];
} catch (error) {
console.error("getEventTypeWebhooks error:", error);
throw error;
}
}
// Create a webhook for a specific event type
static async createEventTypeWebhook(
eventTypeId: number,
input: CreateWebhookInput
): Promise<Webhook> {
try {
const response = await this.makeRequest<{ status: string; data: Webhook }>(
`/event-types/${eventTypeId}/webhooks`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}
);
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from create event type webhook API");
} catch (error) {
console.error("createEventTypeWebhook error:", error);
throw error;
}
}
// Update an event type webhook
static async updateEventTypeWebhook(
eventTypeId: number,
webhookId: string,
updates: UpdateWebhookInput
): Promise<Webhook> {
try {
const response = await this.makeRequest<{ status: string; data: Webhook }>(
`/event-types/${eventTypeId}/webhooks/${webhookId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updates),
}
);
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from update event type webhook API");
} catch (error) {
console.error("updateEventTypeWebhook error:", error);
throw error;
}
}
// Delete an event type webhook
static async deleteEventTypeWebhook(eventTypeId: number, webhookId: string): Promise<void> {
try {
await this.makeRequest(`/event-types/${eventTypeId}/webhooks/${webhookId}`, {
method: "DELETE",
});
} catch (error) {
console.error("deleteEventTypeWebhook error:", error);
throw error;
}
}
// ============================================
// PRIVATE LINKS
// ============================================
// Get all private links for an event type
static async getEventTypePrivateLinks(eventTypeId: number): Promise<PrivateLink[]> {
try {
const response = await this.makeRequest<{ status: string; data: PrivateLink[] }>(
`/event-types/${eventTypeId}/private-links`
);
if (response && response.data && Array.isArray(response.data)) {
return response.data;
}
return [];
} catch (error) {
console.error("getEventTypePrivateLinks error:", error);
throw error;
}
}
// Create a private link for an event type
static async createEventTypePrivateLink(
eventTypeId: number,
input: CreatePrivateLinkInput = {}
): Promise<PrivateLink> {
try {
const response = await this.makeRequest<{ status: string; data: PrivateLink }>(
`/event-types/${eventTypeId}/private-links`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}
);
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from create private link API");
} catch (error) {
console.error("createEventTypePrivateLink error:", error);
throw error;
}
}
// Update a private link
static async updateEventTypePrivateLink(
eventTypeId: number,
linkId: number,
updates: UpdatePrivateLinkInput
): Promise<PrivateLink> {
try {
const response = await this.makeRequest<{ status: string; data: PrivateLink }>(
`/event-types/${eventTypeId}/private-links/${linkId}`,
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updates),
}
);
if (response && response.data) {
return response.data;
}
throw new Error("Invalid response from update private link API");
} catch (error) {
console.error("updateEventTypePrivateLink error:", error);
throw error;
}
}
// Delete a private link
static async deleteEventTypePrivateLink(eventTypeId: number, linkId: number): Promise<void> {
try {
await this.makeRequest(`/event-types/${eventTypeId}/private-links/${linkId}`, {
method: "DELETE",
});
} catch (error) {
console.error("deleteEventTypePrivateLink error:", error);
throw error;
}
}
}