95688b38fa
* checkpoint 1 * checkpoint 2 * update extension header * fix some minor ios ui bugs * fix(companion): restore username fetching for URL fallback display Addresses Cubic AI review feedback (confidence 9/10): The username was hardcoded to 'username' but still used by BasicsTab as a fallback for URL display when bookingUrl is unavailable. This restores the useEffect that fetches the actual username from CalComAPIService.getUsername(). Co-Authored-By: unknown <> * complete devins code change --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1710 lines
46 KiB
TypeScript
1710 lines
46 KiB
TypeScript
import { fetchWithTimeout } from "@/utils/network";
|
|
import { safeLogError, safeLogInfo } from "@/utils/safeLogger";
|
|
|
|
import type {
|
|
AddGuestInput,
|
|
AddGuestsResponse,
|
|
Booking,
|
|
BookingLimitsCount,
|
|
BookingLimitsDuration,
|
|
BookingParticipationResult,
|
|
BookingRecording,
|
|
BookingTranscript,
|
|
ConferencingOption,
|
|
ConferencingSession,
|
|
ConfirmationPolicy,
|
|
CreateEventTypeInput,
|
|
CreatePrivateLinkInput,
|
|
CreateWebhookInput,
|
|
EventType,
|
|
GetConferencingSessionsResponse,
|
|
GetRecordingsResponse,
|
|
GetTranscriptsResponse,
|
|
MarkAbsentRequest,
|
|
MarkAbsentResponse,
|
|
PrivateLink,
|
|
Schedule,
|
|
UpdateLocationResponse,
|
|
UpdatePrivateLinkInput,
|
|
UpdateWebhookInput,
|
|
UserProfile,
|
|
Webhook,
|
|
} from "./types";
|
|
|
|
const API_BASE_URL = "https://api.cal.com/v2";
|
|
|
|
const REQUEST_TIMEOUT_MS = 30000;
|
|
|
|
/**
|
|
* Safely parse JSON response with structure validation.
|
|
* Validates that the input is a non-null object before returning.
|
|
* This prevents prototype pollution and ensures type safety.
|
|
*/
|
|
function safeParseJson(jsonString: string): Record<string, unknown> | null {
|
|
if (typeof jsonString !== "string" || !jsonString.trim()) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsed: unknown = JSON.parse(jsonString);
|
|
|
|
// Validate it's a plain object (not null, array, or primitive)
|
|
if (
|
|
parsed !== null &&
|
|
typeof parsed === "object" &&
|
|
!Array.isArray(parsed) &&
|
|
Object.getPrototypeOf(parsed) === Object.prototype
|
|
) {
|
|
return parsed as Record<string, unknown>;
|
|
}
|
|
|
|
// Also accept arrays as valid JSON responses
|
|
if (Array.isArray(parsed)) {
|
|
return { data: parsed } as Record<string, unknown>;
|
|
}
|
|
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely parse JSON error response with structure validation.
|
|
* Validates the expected error response structure before returning.
|
|
*/
|
|
function safeParseErrorJson(
|
|
jsonString: string
|
|
): { error?: { message?: string }; message?: string } | null {
|
|
if (typeof jsonString !== "string" || !jsonString.trim()) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsed: unknown = JSON.parse(jsonString);
|
|
|
|
// Validate it's a plain object
|
|
if (
|
|
parsed === null ||
|
|
typeof parsed !== "object" ||
|
|
Array.isArray(parsed) ||
|
|
Object.getPrototypeOf(parsed) !== Object.prototype
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const obj = parsed as Record<string, unknown>;
|
|
|
|
// Validate expected error structure - must have error or message property with correct types
|
|
const hasValidErrorProp =
|
|
obj.error === undefined ||
|
|
(typeof obj.error === "object" &&
|
|
obj.error !== null &&
|
|
((obj.error as Record<string, unknown>).message === undefined ||
|
|
typeof (obj.error as Record<string, unknown>).message === "string"));
|
|
|
|
const hasValidMessageProp = obj.message === undefined || typeof obj.message === "string";
|
|
|
|
if (hasValidErrorProp && hasValidMessageProp) {
|
|
return obj as { error?: { message?: string }; message?: string };
|
|
}
|
|
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
BookingLimitsCount,
|
|
BookingLimitsDuration,
|
|
ConfirmationPolicy,
|
|
};
|
|
|
|
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 };
|
|
};
|
|
|
|
// Module-level state (previously private static)
|
|
let _userProfile: UserProfile | null = null;
|
|
// In-flight promise to prevent concurrent /me API calls
|
|
let _userProfilePromise: Promise<UserProfile> | null = null;
|
|
|
|
/**
|
|
* Set OAuth access token for authentication
|
|
*/
|
|
function setAccessToken(accessToken: string, refreshToken?: string): void {
|
|
authConfig.accessToken = accessToken;
|
|
if (refreshToken) {
|
|
authConfig.refreshToken = refreshToken;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set refresh token function for automatic token refresh
|
|
*/
|
|
function setRefreshTokenFunction(
|
|
refreshFn: (refreshToken: string) => Promise<{ accessToken: string; refreshToken?: string }>
|
|
): void {
|
|
refreshTokenFunction = refreshFn;
|
|
}
|
|
|
|
/**
|
|
* Clear all authentication
|
|
*/
|
|
function clearAuth(): void {
|
|
authConfig.accessToken = undefined;
|
|
authConfig.refreshToken = undefined;
|
|
tokenRefreshCallback = null;
|
|
refreshTokenFunction = null;
|
|
}
|
|
|
|
/**
|
|
* Set token refresh callback for OAuth token refresh
|
|
*/
|
|
function setTokenRefreshCallback(
|
|
callback: (accessToken: string, refreshToken?: string) => Promise<void>
|
|
): void {
|
|
tokenRefreshCallback = callback;
|
|
}
|
|
|
|
/**
|
|
* Get current authentication header
|
|
*/
|
|
function 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
|
|
async function getCurrentUser(): Promise<UserProfile> {
|
|
const response = await makeRequest<{ status: string; data: UserProfile }>(
|
|
"/me",
|
|
{
|
|
headers: {
|
|
"cal-api-version": "2024-06-11",
|
|
},
|
|
},
|
|
"2024-06-11"
|
|
);
|
|
return response.data;
|
|
}
|
|
|
|
// Update current user profile
|
|
async function updateUserProfile(updates: {
|
|
email?: string;
|
|
name?: string;
|
|
timeFormat?: number;
|
|
defaultScheduleId?: number;
|
|
weekStart?: string;
|
|
timeZone?: string;
|
|
locale?: string;
|
|
avatarUrl?: string;
|
|
bio?: string;
|
|
metadata?: Record<string, unknown>;
|
|
}): Promise<UserProfile> {
|
|
try {
|
|
const response = await 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?.data) {
|
|
// Update cached profile
|
|
_userProfile = response.data;
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update user profile API");
|
|
} catch (error) {
|
|
console.error("updateUserProfile error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Get and cache user profile with in-flight deduplication
|
|
// This prevents multiple concurrent callers from each making a /me API call
|
|
async function getUserProfile(): Promise<UserProfile> {
|
|
// Return cached profile if available
|
|
if (_userProfile) {
|
|
return _userProfile;
|
|
}
|
|
|
|
// If there's already an in-flight request, wait for it instead of making a new one
|
|
if (_userProfilePromise) {
|
|
return _userProfilePromise;
|
|
}
|
|
|
|
// Create a new request and cache the promise to deduplicate concurrent calls
|
|
_userProfilePromise = getCurrentUser()
|
|
.then((profile) => {
|
|
_userProfile = profile;
|
|
_userProfilePromise = null;
|
|
return profile;
|
|
})
|
|
.catch((error) => {
|
|
_userProfilePromise = null;
|
|
throw error;
|
|
});
|
|
|
|
return _userProfilePromise;
|
|
}
|
|
|
|
// Clear cached profile (useful for logout)
|
|
function clearUserProfile(): void {
|
|
_userProfile = null;
|
|
_userProfilePromise = null;
|
|
}
|
|
|
|
// Test function for bookings API specifically
|
|
async function testRawBookingsAPI(): Promise<void> {
|
|
try {
|
|
const url = `${API_BASE_URL}/bookings?status=upcoming&status=unconfirmed&limit=50`;
|
|
|
|
const response = await fetchWithTimeout(
|
|
url,
|
|
{
|
|
headers: {
|
|
Authorization: getAuthHeader(),
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
},
|
|
REQUEST_TIMEOUT_MS
|
|
);
|
|
|
|
const responseText = await response.text();
|
|
|
|
if (responseText?.trim()) {
|
|
const _responseJson = safeParseJson(responseText);
|
|
if (!_responseJson) {
|
|
safeLogError("[CalComAPIService] Failed to parse bookings response", {
|
|
responseText,
|
|
});
|
|
}
|
|
}
|
|
} catch (_error) {
|
|
safeLogError("[CalComAPIService] testRawBookingsAPI failed", {
|
|
error: _error,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function 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 fetchWithTimeout(
|
|
url,
|
|
{
|
|
...options,
|
|
headers: {
|
|
Authorization: getAuthHeader(),
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": apiVersion,
|
|
...options.headers,
|
|
},
|
|
},
|
|
REQUEST_TIMEOUT_MS
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorBody = await response.text();
|
|
|
|
// Parse error for better user messages
|
|
let errorMessage = response.statusText;
|
|
|
|
const errorJson = safeParseErrorJson(errorBody);
|
|
if (errorJson) {
|
|
errorMessage = errorJson?.error?.message || errorJson?.message || response.statusText;
|
|
} else {
|
|
// 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 makeRequest<T>(endpoint, options, apiVersion, true);
|
|
} catch (refreshError) {
|
|
safeLogError("Token refresh failed:", refreshError);
|
|
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.");
|
|
}
|
|
|
|
// Include status code in error message for graceful error handling downstream
|
|
throw new Error(`API Error: ${response.status} ${errorMessage}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
async function deleteEventType(eventTypeId: number): Promise<void> {
|
|
try {
|
|
await makeRequest(
|
|
`/event-types/${eventTypeId}`,
|
|
{
|
|
method: "DELETE",
|
|
},
|
|
"2024-06-14"
|
|
);
|
|
} catch (error) {
|
|
safeLogError("Delete API error", { error, eventTypeId });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function createEventType(input: CreateEventTypeInput): Promise<EventType> {
|
|
try {
|
|
const sanitizedInput = sanitizePayload(input as unknown as Record<string, unknown>);
|
|
|
|
const response = await makeRequest<{ status: string; data: EventType }>(
|
|
"/event-types",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-06-14",
|
|
},
|
|
body: JSON.stringify(sanitizedInput),
|
|
},
|
|
"2024-06-14"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from create event type API");
|
|
} catch (error) {
|
|
console.error("createEventType error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Cancel a booking
|
|
async function cancelBooking(bookingUid: string, cancellationReason?: string): Promise<void> {
|
|
const body: { cancellationReason?: string } = {};
|
|
if (cancellationReason) {
|
|
body.cancellationReason = cancellationReason;
|
|
}
|
|
|
|
await makeRequest(
|
|
`/bookings/${bookingUid}/cancel`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
body: JSON.stringify(body),
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
}
|
|
|
|
async function rescheduleBooking(
|
|
bookingUid: string,
|
|
input: {
|
|
start: string;
|
|
reschedulingReason?: string;
|
|
}
|
|
): Promise<Booking> {
|
|
try {
|
|
safeLogInfo("[CalComAPIService] rescheduleBooking request:", {
|
|
bookingUid,
|
|
input,
|
|
});
|
|
|
|
const response = await 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?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from reschedule booking API");
|
|
} catch (error) {
|
|
safeLogError("[CalComAPIService] rescheduleBooking error:", error);
|
|
if (error instanceof Error) {
|
|
safeLogError("[CalComAPIService] Error message:", error.message);
|
|
safeLogError("[CalComAPIService] Error stack:", error.stack);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function confirmBooking(bookingUid: string): Promise<Booking> {
|
|
try {
|
|
const response = await makeRequest<{ status: string; data: Booking }>(
|
|
`/bookings/${bookingUid}/confirm`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from confirm booking API");
|
|
} catch (error) {
|
|
safeLogError("confirmBooking error", { error, bookingUid });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function declineBooking(bookingUid: string, reason?: string): Promise<Booking> {
|
|
try {
|
|
const body: { reason?: string } = {};
|
|
if (reason) {
|
|
body.reason = reason;
|
|
}
|
|
|
|
const response = await makeRequest<{ status: string; data: Booking }>(
|
|
`/bookings/${bookingUid}/decline`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
body: JSON.stringify(body),
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from decline booking API");
|
|
} catch (error) {
|
|
console.error("declineBooking error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark an attendee as absent (no-show) for a booking
|
|
* @param bookingUid - The unique identifier of the booking
|
|
* @param attendeeEmail - The email of the attendee to mark as absent
|
|
* @param absent - Whether to mark as absent (true) or undo (false)
|
|
*/
|
|
async function markAbsent(
|
|
bookingUid: string,
|
|
attendeeEmail: string,
|
|
absent: boolean = true
|
|
): Promise<Booking> {
|
|
try {
|
|
const body: MarkAbsentRequest = {
|
|
attendees: [{ email: attendeeEmail, absent }],
|
|
};
|
|
|
|
const response = await makeRequest<MarkAbsentResponse>(
|
|
`/bookings/${bookingUid}/mark-absent`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
body: JSON.stringify(body),
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from mark absent API");
|
|
} catch (error) {
|
|
console.error("markAbsent error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Booking Action API Methods (API v2 2024-08-13)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Add guests to a booking
|
|
* @param bookingUid - The unique identifier of the booking
|
|
* @param guests - Array of guests to add (email required, name optional)
|
|
* @returns Updated booking with new guests
|
|
*/
|
|
async function addGuests(bookingUid: string, guests: AddGuestInput[]): Promise<Booking> {
|
|
try {
|
|
const response = await makeRequest<AddGuestsResponse>(
|
|
`/bookings/${bookingUid}/guests`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
body: JSON.stringify({ guests }),
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from add guests API");
|
|
} catch (error) {
|
|
console.error("addGuests error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the location of a booking (legacy - sends location as string)
|
|
* Note: This updates the booking location but may not update the calendar event
|
|
* @param bookingUid - The unique identifier of the booking
|
|
* @param location - The new location string
|
|
* @returns Updated booking with new location
|
|
* @deprecated Use updateLocationV2 instead for proper typed location updates
|
|
*/
|
|
async function updateLocation(bookingUid: string, location: string): Promise<Booking> {
|
|
try {
|
|
const response = await makeRequest<UpdateLocationResponse>(
|
|
`/bookings/${bookingUid}/location`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
body: JSON.stringify({ location }),
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update location API");
|
|
} catch (error) {
|
|
console.error("updateLocation error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the location of a booking with typed location object
|
|
* Supports: address, link, phone, attendeePhone, attendeeAddress, attendeeDefined
|
|
* Note: Integration-based locations (Cal Video, Google Meet, Zoom) are NOT supported
|
|
* @param bookingUid - The unique identifier of the booking
|
|
* @param location - The location object with type and value
|
|
* @returns Updated booking with new location
|
|
*/
|
|
async function updateLocationV2(
|
|
bookingUid: string,
|
|
location: { type: string; [key: string]: string }
|
|
): Promise<Booking> {
|
|
try {
|
|
const response = await makeRequest<UpdateLocationResponse>(
|
|
`/bookings/${bookingUid}/location`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
body: JSON.stringify({ location }),
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update location API");
|
|
} catch (error) {
|
|
safeLogError("[CalComAPIService] updateLocationV2 error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get recordings for a booking (Cal Video only)
|
|
* Note: Only available for past Cal Video bookings
|
|
* @param bookingUid - The unique identifier of the booking
|
|
* @returns Array of recording objects with download URLs
|
|
*/
|
|
async function getRecordings(bookingUid: string): Promise<BookingRecording[]> {
|
|
try {
|
|
const response = await makeRequest<GetRecordingsResponse>(
|
|
`/bookings/${bookingUid}/recordings`,
|
|
{
|
|
headers: {
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
// Handle 401/403 gracefully - endpoint may require auth in future
|
|
if (error instanceof Error) {
|
|
const statusMatch = error.message.match(/API Error: (\d+)/);
|
|
if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
|
|
console.warn(`Recordings access denied for booking ${bookingUid}`);
|
|
return [];
|
|
}
|
|
if (statusMatch && statusMatch[1] === "404") {
|
|
console.warn(`No recordings found for booking ${bookingUid}`);
|
|
return [];
|
|
}
|
|
}
|
|
console.error("getRecordings error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get conferencing session details for a booking (Cal Video only)
|
|
* Note: This endpoint is PBAC-guarded and requires booking.readRecordings permission
|
|
* @param bookingUid - The unique identifier of the booking
|
|
* @returns Array of conferencing session objects with participant info
|
|
*/
|
|
async function getConferencingSessions(bookingUid: string): Promise<ConferencingSession[]> {
|
|
try {
|
|
const response = await makeRequest<GetConferencingSessionsResponse>(
|
|
`/bookings/${bookingUid}/conferencing-sessions`,
|
|
{
|
|
headers: {
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
// Handle 401/403 gracefully - endpoint is PBAC-guarded
|
|
if (error instanceof Error) {
|
|
const statusMatch = error.message.match(/API Error: (\d+)/);
|
|
if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
|
|
console.warn(`Conferencing sessions access denied for booking ${bookingUid}`);
|
|
return [];
|
|
}
|
|
if (statusMatch && statusMatch[1] === "404") {
|
|
console.warn(`No conferencing sessions found for booking ${bookingUid}`);
|
|
return [];
|
|
}
|
|
}
|
|
console.error("getConferencingSessions error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get transcripts for a booking (Cal Video only)
|
|
* Note: This endpoint may require authentication in future releases
|
|
* @param bookingUid - The unique identifier of the booking
|
|
* @returns Array of transcript objects with download URLs
|
|
*/
|
|
async function getTranscripts(bookingUid: string): Promise<BookingTranscript[]> {
|
|
try {
|
|
const response = await makeRequest<GetTranscriptsResponse>(
|
|
`/bookings/${bookingUid}/transcripts`,
|
|
{
|
|
headers: {
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
// Handle 401/403 gracefully - endpoint may require auth in future
|
|
if (error instanceof Error) {
|
|
const statusMatch = error.message.match(/API Error: (\d+)/);
|
|
if (statusMatch && (statusMatch[1] === "401" || statusMatch[1] === "403")) {
|
|
console.warn(`Transcripts access denied for booking ${bookingUid}`);
|
|
return [];
|
|
}
|
|
if (statusMatch && statusMatch[1] === "404") {
|
|
console.warn(`No transcripts found for booking ${bookingUid}`);
|
|
return [];
|
|
}
|
|
}
|
|
console.error("getTranscripts error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function getEventTypes(): Promise<EventType[]> {
|
|
// Get cached user profile to extract username (uses in-flight deduplication)
|
|
let username: string | undefined;
|
|
try {
|
|
const userProfile = await getUserProfile();
|
|
// Extract username from response
|
|
if (userProfile?.username) {
|
|
username = userProfile.username;
|
|
}
|
|
} catch (_error) {}
|
|
|
|
// Build query string with username and sorting
|
|
const params = new URLSearchParams();
|
|
if (username) {
|
|
params.append("username", username);
|
|
}
|
|
// Sort by creation date descending (newer first) to match main codebase behavior
|
|
// Main codebase uses position: "desc", id: "desc" - since API doesn't expose position,
|
|
// we use sortCreatedAt: "desc" for similar behavior (newer event types first)
|
|
params.append("sortCreatedAt", "desc");
|
|
|
|
const queryString = params.toString();
|
|
const endpoint = `/event-types${queryString ? `?${queryString}` : ""}`;
|
|
|
|
const response = await makeRequest<unknown>(endpoint, {}, "2024-06-14");
|
|
|
|
// Handle different possible response structures
|
|
let eventTypesArray: EventType[] = [];
|
|
|
|
if (Array.isArray(response)) {
|
|
eventTypesArray = response as EventType[];
|
|
} else if (response && typeof response === "object") {
|
|
const resp = response as Record<string, unknown>;
|
|
if (resp.data && Array.isArray(resp.data)) {
|
|
eventTypesArray = resp.data as EventType[];
|
|
} else if (resp.eventTypes && Array.isArray(resp.eventTypes)) {
|
|
eventTypesArray = resp.eventTypes as EventType[];
|
|
} else if (resp.items && Array.isArray(resp.items)) {
|
|
eventTypesArray = resp.items as EventType[];
|
|
} else if (resp.data && typeof resp.data === "object") {
|
|
const dataObj = resp.data as Record<string, unknown>;
|
|
if (dataObj.eventTypes && Array.isArray(dataObj.eventTypes)) {
|
|
eventTypesArray = dataObj.eventTypes as EventType[];
|
|
} else if (dataObj.items && Array.isArray(dataObj.items)) {
|
|
eventTypesArray = dataObj.items as EventType[];
|
|
} else {
|
|
const keys = Object.keys(dataObj);
|
|
if (keys.length > 0) {
|
|
eventTypesArray = Object.values(dataObj).filter((item): item is EventType =>
|
|
Boolean(item && typeof item === "object" && "id" in item)
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
const possibleArrays = Object.values(resp).filter((val) => Array.isArray(val));
|
|
if (possibleArrays.length > 0) {
|
|
eventTypesArray = possibleArrays[0] as EventType[];
|
|
}
|
|
}
|
|
}
|
|
|
|
return eventTypesArray;
|
|
}
|
|
|
|
async function getBookingByUid(bookingUid: string): Promise<Booking> {
|
|
try {
|
|
const response = await makeRequest<{ status: string; data: Booking }>(
|
|
`/bookings/${bookingUid}`,
|
|
{
|
|
headers: {
|
|
"cal-api-version": "2024-08-13",
|
|
},
|
|
},
|
|
"2024-08-13"
|
|
);
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
throw new Error("Invalid response from get booking API");
|
|
} catch (error) {
|
|
console.error("getBookingByUid error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function getBookings(filters?: {
|
|
status?: string[];
|
|
fromDate?: string;
|
|
toDate?: string;
|
|
eventTypeId?: number;
|
|
limit?: number;
|
|
offset?: number;
|
|
}): Promise<Booking[]> {
|
|
// Build query parameters
|
|
const params = new URLSearchParams();
|
|
|
|
if (filters?.status?.length) {
|
|
for (const status of filters.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 makeRequest<unknown>(endpoint);
|
|
|
|
// Handle different possible response structures (same logic as event types)
|
|
let bookingsArray: Booking[] = [];
|
|
|
|
if (Array.isArray(response)) {
|
|
bookingsArray = response as Booking[];
|
|
} else if (response && typeof response === "object") {
|
|
const resp = response as Record<string, unknown>;
|
|
if (resp.data && Array.isArray(resp.data)) {
|
|
bookingsArray = resp.data as Booking[];
|
|
} else if (resp.bookings && Array.isArray(resp.bookings)) {
|
|
bookingsArray = resp.bookings as Booking[];
|
|
} else if (resp.items && Array.isArray(resp.items)) {
|
|
bookingsArray = resp.items as Booking[];
|
|
} else if (resp.data && typeof resp.data === "object") {
|
|
const dataObj = resp.data as Record<string, unknown>;
|
|
if (dataObj.bookings && Array.isArray(dataObj.bookings)) {
|
|
bookingsArray = dataObj.bookings as Booking[];
|
|
} else if (dataObj.items && Array.isArray(dataObj.items)) {
|
|
bookingsArray = dataObj.items as Booking[];
|
|
} else {
|
|
// Convert object values to array as last resort
|
|
const keys = Object.keys(dataObj);
|
|
if (keys.length > 0) {
|
|
bookingsArray = Object.values(dataObj).filter((item): item is Booking =>
|
|
Boolean(item && typeof item === "object" && ("id" in item || "uid" in item))
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Try to extract any arrays from the response
|
|
const possibleArrays = Object.values(resp).filter((val) => Array.isArray(val));
|
|
if (possibleArrays.length > 0) {
|
|
bookingsArray = possibleArrays[0] as Booking[];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get cached user profile to filter bookings (uses in-flight deduplication)
|
|
let userProfile: UserProfile | undefined;
|
|
try {
|
|
userProfile = await getUserProfile();
|
|
} catch (_error) {
|
|
return bookingsArray;
|
|
}
|
|
|
|
// Extract user info from response
|
|
let userId: number | undefined;
|
|
let userEmail: string | undefined;
|
|
|
|
if (userProfile) {
|
|
userId = userProfile.id;
|
|
userEmail = userProfile.email;
|
|
}
|
|
|
|
// Filter bookings to only show ones where the current user is participating
|
|
const userBookings = bookingsArray.filter((booking) => {
|
|
const {
|
|
isOrganizer: _isOrganizer,
|
|
isHost: _isHost,
|
|
isAttendee: _isAttendee,
|
|
isParticipating,
|
|
} = getBookingParticipation(booking, userId, userEmail);
|
|
|
|
return isParticipating;
|
|
});
|
|
|
|
return userBookings;
|
|
}
|
|
|
|
// Get all schedules
|
|
async function getSchedules(): Promise<Schedule[]> {
|
|
const response = await 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?.data && Array.isArray(response.data)) {
|
|
// Fallback: response.data might be array
|
|
schedulesArray = response.data;
|
|
}
|
|
|
|
return schedulesArray;
|
|
}
|
|
|
|
// Create a new schedule
|
|
async function 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 {
|
|
const sanitizedInput = sanitizePayload(input as Record<string, unknown>);
|
|
|
|
const response = await makeRequest<{ status: string; data: Schedule }>(
|
|
"/schedules",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-06-11",
|
|
},
|
|
body: JSON.stringify(sanitizedInput),
|
|
},
|
|
"2024-06-11"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from create schedule API");
|
|
} catch (error) {
|
|
console.error("createSchedule error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function getScheduleById(scheduleId: number): Promise<Schedule | null> {
|
|
try {
|
|
const response = await makeRequest<unknown>(
|
|
`/schedules/${scheduleId}`,
|
|
{
|
|
headers: {
|
|
"cal-api-version": "2024-06-11",
|
|
},
|
|
},
|
|
"2024-06-11"
|
|
);
|
|
|
|
if (response && typeof response === "object") {
|
|
const resp = response as Record<string, unknown>;
|
|
if (resp.data && typeof resp.data === "object") {
|
|
return resp.data as Schedule;
|
|
}
|
|
if (resp.id) {
|
|
return response as Schedule;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error("getScheduleById error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Get conferencing options
|
|
async function getConferencingOptions(): Promise<ConferencingOption[]> {
|
|
try {
|
|
const response = await makeRequest<{
|
|
status: string;
|
|
data: ConferencingOption[];
|
|
}>("/conferencing");
|
|
|
|
if (response?.data && Array.isArray(response.data)) {
|
|
return response.data;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error("getConferencingOptions error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Get a single event type by ID
|
|
async function getEventTypeById(eventTypeId: number): Promise<EventType | null> {
|
|
try {
|
|
const response = await makeRequest<{ status: string; data: EventType }>(
|
|
`/event-types/${eventTypeId}`,
|
|
{},
|
|
"2024-06-14"
|
|
);
|
|
|
|
if (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");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Update an event type
|
|
/**
|
|
* Sanitizes a payload before sending to the API.
|
|
* - Removes keys with null values for array fields (API expects arrays or field to be omitted)
|
|
* - Removes keys with undefined values
|
|
* - Recursively sanitizes nested objects
|
|
*/
|
|
function sanitizePayload(payload: Record<string, unknown>): Record<string, unknown> {
|
|
const sanitized: Record<string, unknown> = {};
|
|
|
|
// Fields that should NEVER be sent as null - API expects array or omit entirely
|
|
const arrayFields = [
|
|
"lengthInMinutesOptions",
|
|
"multipleDuration",
|
|
"locations",
|
|
"bookingFields",
|
|
"hosts",
|
|
"children",
|
|
"customInputs",
|
|
];
|
|
|
|
// Fields that can be null (to clear the value)
|
|
const nullableFields = [
|
|
"description",
|
|
"successRedirectUrl",
|
|
"slotInterval",
|
|
"eventName",
|
|
"timeZone",
|
|
"interfaceLanguage",
|
|
];
|
|
|
|
for (const [key, value] of Object.entries(payload)) {
|
|
// Skip undefined values
|
|
if (value === undefined) continue;
|
|
|
|
// Handle null values
|
|
if (value === null) {
|
|
// For array fields, skip entirely (don't send null)
|
|
if (arrayFields.includes(key)) {
|
|
console.warn(`Skipping null value for array field: ${key}`);
|
|
continue;
|
|
}
|
|
// For nullable fields, allow null
|
|
if (nullableFields.includes(key)) {
|
|
sanitized[key] = null;
|
|
continue;
|
|
}
|
|
// For other fields, skip null to be safe
|
|
console.warn(`Skipping null value for field: ${key}`);
|
|
continue;
|
|
}
|
|
|
|
// Recursively sanitize nested objects (but not arrays)
|
|
if (typeof value === "object" && !Array.isArray(value)) {
|
|
const sanitizedNested = sanitizePayload(value as Record<string, unknown>);
|
|
// Only include if the nested object has values
|
|
if (Object.keys(sanitizedNested).length > 0) {
|
|
sanitized[key] = sanitizedNested;
|
|
}
|
|
} else {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
async function updateEventType(
|
|
eventTypeId: number,
|
|
updates: Partial<CreateEventTypeInput>
|
|
): Promise<EventType> {
|
|
try {
|
|
const sanitizedUpdates = sanitizePayload(updates as Record<string, unknown>);
|
|
|
|
const response = await makeRequest<{ status: string; data: EventType }>(
|
|
`/event-types/${eventTypeId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-06-14",
|
|
},
|
|
body: JSON.stringify(sanitizedUpdates),
|
|
},
|
|
"2024-06-14"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update event type API");
|
|
} catch (error) {
|
|
console.error("updateEventType error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Update a schedule
|
|
async function 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 {
|
|
// Sanitize the updates to remove null values
|
|
const sanitizedUpdates = sanitizePayload(updates as Record<string, unknown>);
|
|
const response = await makeRequest<{ status: string; data: Schedule }>(
|
|
`/schedules/${scheduleId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cal-api-version": "2024-06-11",
|
|
},
|
|
body: JSON.stringify(sanitizedUpdates),
|
|
},
|
|
"2024-06-11"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update schedule API");
|
|
} catch (error) {
|
|
console.error("updateSchedule error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Duplicate a schedule
|
|
async function duplicateSchedule(scheduleId: number): Promise<Schedule> {
|
|
try {
|
|
const response = await makeRequest<{ status: string; data: Schedule }>(
|
|
`/atoms/schedules/${scheduleId}/duplicate`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"cal-api-version": "2024-06-11",
|
|
},
|
|
},
|
|
"2024-06-11"
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from duplicate schedule API");
|
|
} catch (error) {
|
|
console.error("duplicateSchedule error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Delete a schedule
|
|
async function deleteSchedule(scheduleId: number): Promise<void> {
|
|
try {
|
|
await makeRequest<{ status: string }>(
|
|
`/schedules/${scheduleId}`,
|
|
{
|
|
method: "DELETE",
|
|
headers: {
|
|
"cal-api-version": "2024-06-11",
|
|
},
|
|
},
|
|
"2024-06-11"
|
|
);
|
|
} catch (error) {
|
|
console.error("deleteSchedule error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// WEBHOOKS
|
|
// ============================================
|
|
|
|
// Get all global webhooks
|
|
async function getWebhooks(): Promise<Webhook[]> {
|
|
try {
|
|
const response = await makeRequest<{ status: string; data: Webhook[] }>("/webhooks");
|
|
|
|
if (response?.data && Array.isArray(response.data)) {
|
|
return response.data;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error("getWebhooks error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Create a global webhook
|
|
async function createWebhook(input: CreateWebhookInput): Promise<Webhook> {
|
|
try {
|
|
const sanitizedInput = sanitizePayload(input as unknown as Record<string, unknown>);
|
|
const response = await makeRequest<{ status: string; data: Webhook }>("/webhooks", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(sanitizedInput),
|
|
});
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from create webhook API");
|
|
} catch (error) {
|
|
console.error("createWebhook error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Update a global webhook
|
|
async function updateWebhook(webhookId: string, updates: UpdateWebhookInput): Promise<Webhook> {
|
|
try {
|
|
const sanitizedUpdates = sanitizePayload(updates as Record<string, unknown>);
|
|
const response = await makeRequest<{ status: string; data: Webhook }>(
|
|
`/webhooks/${webhookId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(sanitizedUpdates),
|
|
}
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update webhook API");
|
|
} catch (error) {
|
|
console.error("updateWebhook error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Delete a global webhook
|
|
async function deleteWebhook(webhookId: string): Promise<void> {
|
|
try {
|
|
await makeRequest(`/webhooks/${webhookId}`, {
|
|
method: "DELETE",
|
|
});
|
|
} catch (error) {
|
|
console.error("deleteWebhook error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Get webhooks for a specific event type
|
|
async function getEventTypeWebhooks(eventTypeId: number): Promise<Webhook[]> {
|
|
try {
|
|
const response = await makeRequest<{ status: string; data: Webhook[] }>(
|
|
`/event-types/${eventTypeId}/webhooks`
|
|
);
|
|
|
|
if (response?.data && Array.isArray(response.data)) {
|
|
return response.data;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error("getEventTypeWebhooks error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Create a webhook for a specific event type
|
|
async function createEventTypeWebhook(
|
|
eventTypeId: number,
|
|
input: CreateWebhookInput
|
|
): Promise<Webhook> {
|
|
try {
|
|
const sanitizedInput = sanitizePayload(input as unknown as Record<string, unknown>);
|
|
const response = await makeRequest<{ status: string; data: Webhook }>(
|
|
`/event-types/${eventTypeId}/webhooks`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(sanitizedInput),
|
|
}
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from create event type webhook API");
|
|
} catch (error) {
|
|
console.error("createEventTypeWebhook error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Update an event type webhook
|
|
async function updateEventTypeWebhook(
|
|
eventTypeId: number,
|
|
webhookId: string,
|
|
updates: UpdateWebhookInput
|
|
): Promise<Webhook> {
|
|
try {
|
|
const sanitizedUpdates = sanitizePayload(updates as Record<string, unknown>);
|
|
const response = await makeRequest<{ status: string; data: Webhook }>(
|
|
`/event-types/${eventTypeId}/webhooks/${webhookId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(sanitizedUpdates),
|
|
}
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update event type webhook API");
|
|
} catch (error) {
|
|
console.error("updateEventTypeWebhook error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Delete an event type webhook
|
|
async function deleteEventTypeWebhook(eventTypeId: number, webhookId: string): Promise<void> {
|
|
try {
|
|
await makeRequest(`/event-types/${eventTypeId}/webhooks/${webhookId}`, {
|
|
method: "DELETE",
|
|
});
|
|
} catch (error) {
|
|
console.error("deleteEventTypeWebhook error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// PRIVATE LINKS
|
|
// ============================================
|
|
|
|
// Get all private links for an event type
|
|
async function getEventTypePrivateLinks(eventTypeId: number): Promise<PrivateLink[]> {
|
|
try {
|
|
const response = await makeRequest<{ status: string; data: PrivateLink[] }>(
|
|
`/event-types/${eventTypeId}/private-links`
|
|
);
|
|
|
|
if (response?.data && Array.isArray(response.data)) {
|
|
return response.data;
|
|
}
|
|
|
|
return [];
|
|
} catch (error) {
|
|
console.error("getEventTypePrivateLinks error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Create a private link for an event type
|
|
async function createEventTypePrivateLink(
|
|
eventTypeId: number,
|
|
input: CreatePrivateLinkInput = {}
|
|
): Promise<PrivateLink> {
|
|
try {
|
|
const sanitizedInput = sanitizePayload(input as Record<string, unknown>);
|
|
const response = await makeRequest<{ status: string; data: PrivateLink }>(
|
|
`/event-types/${eventTypeId}/private-links`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(sanitizedInput),
|
|
}
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from create private link API");
|
|
} catch (error) {
|
|
console.error("createEventTypePrivateLink error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Update a private link
|
|
async function updateEventTypePrivateLink(
|
|
eventTypeId: number,
|
|
linkId: number,
|
|
updates: UpdatePrivateLinkInput
|
|
): Promise<PrivateLink> {
|
|
try {
|
|
const sanitizedUpdates = sanitizePayload(updates as Record<string, unknown>);
|
|
const response = await makeRequest<{ status: string; data: PrivateLink }>(
|
|
`/event-types/${eventTypeId}/private-links/${linkId}`,
|
|
{
|
|
method: "PATCH",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(sanitizedUpdates),
|
|
}
|
|
);
|
|
|
|
if (response?.data) {
|
|
return response.data;
|
|
}
|
|
|
|
throw new Error("Invalid response from update private link API");
|
|
} catch (error) {
|
|
console.error("updateEventTypePrivateLink error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Delete a private link
|
|
async function deleteEventTypePrivateLink(eventTypeId: number, linkId: number): Promise<void> {
|
|
try {
|
|
await makeRequest(`/event-types/${eventTypeId}/private-links/${linkId}`, {
|
|
method: "DELETE",
|
|
});
|
|
} catch (error) {
|
|
console.error("deleteEventTypePrivateLink error");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Helper to get username
|
|
async function getUsername(): Promise<string> {
|
|
const profile = await getUserProfile();
|
|
return profile.username;
|
|
}
|
|
|
|
// Export as object to satisfy noStaticOnlyClass rule
|
|
export const CalComAPIService = {
|
|
setAccessToken,
|
|
setRefreshTokenFunction,
|
|
clearAuth,
|
|
setTokenRefreshCallback,
|
|
getCurrentUser,
|
|
updateUserProfile,
|
|
getUserProfile,
|
|
getUsername,
|
|
clearUserProfile,
|
|
testRawBookingsAPI,
|
|
deleteEventType,
|
|
createEventType,
|
|
cancelBooking,
|
|
rescheduleBooking,
|
|
confirmBooking,
|
|
declineBooking,
|
|
markAbsent,
|
|
addGuests,
|
|
updateLocation,
|
|
updateLocationV2,
|
|
getRecordings,
|
|
getConferencingSessions,
|
|
getTranscripts,
|
|
getEventTypes,
|
|
getBookingByUid,
|
|
getBookings,
|
|
getSchedules,
|
|
createSchedule,
|
|
getScheduleById,
|
|
getConferencingOptions,
|
|
getEventTypeById,
|
|
updateEventType,
|
|
updateSchedule,
|
|
duplicateSchedule,
|
|
deleteSchedule,
|
|
getWebhooks,
|
|
createWebhook,
|
|
updateWebhook,
|
|
deleteWebhook,
|
|
getEventTypeWebhooks,
|
|
createEventTypeWebhook,
|
|
updateEventTypeWebhook,
|
|
deleteEventTypeWebhook,
|
|
getEventTypePrivateLinks,
|
|
createEventTypePrivateLink,
|
|
updateEventTypePrivateLink,
|
|
deleteEventTypePrivateLink,
|
|
};
|