2023-02-16 22:39:57 +00:00
|
|
|
import type { Prisma } from "@prisma/client";
|
2022-02-04 18:30:52 +00:00
|
|
|
|
2022-03-16 23:36:43 +00:00
|
|
|
import { handleErrorsJson, handleErrorsRaw } from "@calcom/lib/errors";
|
2022-05-02 23:13:34 +00:00
|
|
|
import { HttpError } from "@calcom/lib/http-error";
|
2022-03-23 22:00:30 +00:00
|
|
|
import prisma from "@calcom/prisma";
|
|
|
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { CredentialPayload } from "@calcom/types/Credential";
|
2022-03-23 22:00:30 +00:00
|
|
|
import type { PartialReference } from "@calcom/types/EventManager";
|
|
|
|
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
2022-02-04 18:30:52 +00:00
|
|
|
|
2022-05-02 23:13:34 +00:00
|
|
|
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
|
|
|
|
2022-02-04 18:30:52 +00:00
|
|
|
interface TandemToken {
|
|
|
|
expires_in?: number;
|
|
|
|
expiry_date: number;
|
|
|
|
refresh_token: string;
|
|
|
|
token_type: "Bearer";
|
|
|
|
access_token: string;
|
|
|
|
}
|
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
interface ITandemRefreshToken {
|
|
|
|
expires_in?: number;
|
|
|
|
expiry_date?: number;
|
|
|
|
access_token: string;
|
|
|
|
refresh_token: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ITandemCreateMeetingResponse {
|
|
|
|
data: {
|
|
|
|
id: string;
|
|
|
|
event_link: string;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-05-02 23:13:34 +00:00
|
|
|
let client_id = "";
|
|
|
|
let client_secret = "";
|
|
|
|
let base_url = "";
|
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
const tandemAuth = async (credential: CredentialPayload) => {
|
2022-05-02 23:13:34 +00:00
|
|
|
const appKeys = await getAppKeysFromSlug("tandem");
|
|
|
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
|
|
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
|
|
|
if (typeof appKeys.base_url === "string") base_url = appKeys.base_url;
|
|
|
|
if (!client_id) throw new HttpError({ statusCode: 400, message: "Tandem client_id missing." });
|
|
|
|
if (!client_secret) throw new HttpError({ statusCode: 400, message: "Tandem client_secret missing." });
|
|
|
|
if (!base_url) throw new HttpError({ statusCode: 400, message: "Tandem base_url missing." });
|
2022-02-04 18:30:52 +00:00
|
|
|
|
|
|
|
const credentialKey = credential.key as unknown as TandemToken;
|
|
|
|
const isTokenValid = (token: TandemToken) => token && token.access_token && token.expiry_date < Date.now();
|
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
const refreshAccessToken = async (refreshToken: string) => {
|
|
|
|
const result = await fetch(`${base_url}/api/v1/oauth/v2/token`, {
|
2022-02-04 18:30:52 +00:00
|
|
|
method: "POST",
|
|
|
|
body: new URLSearchParams({
|
|
|
|
client_id,
|
|
|
|
client_secret,
|
|
|
|
code: refreshToken,
|
|
|
|
}),
|
2022-10-31 22:06:03 +00:00
|
|
|
});
|
|
|
|
const responseBody = await handleErrorsJson<ITandemRefreshToken>(result);
|
|
|
|
|
|
|
|
// set expiry date as offset from current time.
|
|
|
|
responseBody.expiry_date = Math.round(Date.now() + (responseBody.expires_in || 0) * 1000);
|
|
|
|
delete responseBody.expires_in;
|
|
|
|
// Store new tokens in database.
|
|
|
|
await prisma.credential.update({
|
|
|
|
where: {
|
|
|
|
id: credential.id,
|
|
|
|
},
|
|
|
|
data: {
|
|
|
|
key: responseBody as unknown as Prisma.InputJsonValue,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
credentialKey.expiry_date = responseBody.expiry_date;
|
|
|
|
credentialKey.access_token = responseBody.access_token;
|
|
|
|
credentialKey.refresh_token = responseBody.refresh_token;
|
|
|
|
return credentialKey.access_token;
|
2022-02-04 18:30:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
getToken: () =>
|
|
|
|
!isTokenValid(credentialKey)
|
|
|
|
? Promise.resolve(credentialKey.access_token)
|
|
|
|
: refreshAccessToken(credentialKey.refresh_token),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => {
|
2022-02-04 18:30:52 +00:00
|
|
|
const auth = tandemAuth(credential);
|
|
|
|
|
|
|
|
const _parseDate = (date: string) => {
|
|
|
|
return Date.parse(date) / 1000;
|
|
|
|
};
|
|
|
|
|
|
|
|
const _translateEvent = (event: CalendarEvent, param: string): string => {
|
|
|
|
return JSON.stringify({
|
|
|
|
[param]: {
|
|
|
|
title: event.title,
|
|
|
|
starts_at: _parseDate(event.startTime),
|
|
|
|
ends_at: _parseDate(event.endTime),
|
|
|
|
description: event.description || "",
|
|
|
|
conference_solution: "tandem",
|
|
|
|
type: 3,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
const _translateResult = (result: ITandemCreateMeetingResponse) => {
|
2022-02-04 18:30:52 +00:00
|
|
|
return {
|
|
|
|
type: "tandem_video",
|
|
|
|
id: result.data.id as string,
|
|
|
|
password: "",
|
|
|
|
url: result.data.event_link,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
/** Tandem doesn't need to return busy times, so we return empty */
|
|
|
|
getAvailability: () => {
|
|
|
|
return Promise.resolve([]);
|
|
|
|
},
|
|
|
|
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> => {
|
2022-05-02 23:13:34 +00:00
|
|
|
const accessToken = await (await auth).getToken();
|
2022-02-04 18:30:52 +00:00
|
|
|
|
2022-05-02 23:13:34 +00:00
|
|
|
const result = await fetch(`${base_url}/api/v1/meetings`, {
|
2022-02-04 18:30:52 +00:00
|
|
|
method: "POST",
|
|
|
|
headers: {
|
2023-10-03 18:52:19 +00:00
|
|
|
Authorization: `Bearer ${accessToken}`,
|
2022-02-04 18:30:52 +00:00
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
body: _translateEvent(event, "meeting"),
|
2022-10-31 22:06:03 +00:00
|
|
|
});
|
|
|
|
const responseBody = await handleErrorsJson<ITandemCreateMeetingResponse>(result);
|
2022-02-04 18:30:52 +00:00
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
return Promise.resolve(_translateResult(responseBody));
|
2022-02-04 18:30:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
deleteMeeting: async (uid: string): Promise<void> => {
|
2022-05-02 23:13:34 +00:00
|
|
|
const accessToken = await (await auth).getToken();
|
2022-02-04 18:30:52 +00:00
|
|
|
|
2022-05-02 23:13:34 +00:00
|
|
|
await fetch(`${base_url}/api/v1/meetings/${uid}`, {
|
2022-02-04 18:30:52 +00:00
|
|
|
method: "DELETE",
|
|
|
|
headers: {
|
2023-10-03 18:52:19 +00:00
|
|
|
Authorization: `Bearer ${accessToken}`,
|
2022-02-04 18:30:52 +00:00
|
|
|
},
|
|
|
|
}).then(handleErrorsRaw);
|
|
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
},
|
|
|
|
|
|
|
|
updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> => {
|
2022-05-02 23:13:34 +00:00
|
|
|
const accessToken = await (await auth).getToken();
|
2022-02-04 18:30:52 +00:00
|
|
|
|
2022-05-02 23:13:34 +00:00
|
|
|
const result = await fetch(`${base_url}/api/v1/meetings/${bookingRef.meetingId}`, {
|
2022-02-04 18:30:52 +00:00
|
|
|
method: "PUT",
|
|
|
|
headers: {
|
2023-10-03 18:52:19 +00:00
|
|
|
Authorization: `Bearer ${accessToken}`,
|
2022-02-04 18:30:52 +00:00
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
body: _translateEvent(event, "updates"),
|
2022-10-31 22:06:03 +00:00
|
|
|
});
|
|
|
|
const responseBody = await handleErrorsJson<ITandemCreateMeetingResponse>(result);
|
2022-02-04 18:30:52 +00:00
|
|
|
|
2022-10-31 22:06:03 +00:00
|
|
|
return Promise.resolve(_translateResult(responseBody));
|
2022-02-04 18:30:52 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export default TandemVideoApiAdapter;
|