Skip to content

Commit

Permalink
feat: 11642 app shimmer video (calcom#12159)
Browse files Browse the repository at this point in the history
* added initial app

* created basic functionality for Shimmer Video app with tracking of Daily rooms

* changed the type config value in the shimmer video config.json

* re-fixed update to shimmer-video config type

* updated static images for shimmer video app

* fixed tracking Shimmer video event parameter

* Add zod files

* Allow query for "conferencing" apps

* Move to shimmer video

* Redirect to shimmer app

* Remove console.logs

* Remove legacy use of seed-app-store.

---------

Co-authored-by: Peer Richelsen <[email protected]>
Co-authored-by: Vik <[email protected]>
Co-authored-by: pathaksarvesh <[email protected]>
Co-authored-by: Joe Au-Yeung <[email protected]>
Co-authored-by: Joe Au-Yeung <[email protected]>
Co-authored-by: Hariom <[email protected]>
  • Loading branch information
7 people authored Nov 9, 2023
1 parent 09fc7e1 commit 6848362
Show file tree
Hide file tree
Showing 21 changed files with 291 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/app-store/apps.keys-schemas.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { appKeysSchema as plausible_zod_ts } from "./plausible/zod";
import { appKeysSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appKeysSchema as routing_forms_zod_ts } from "./routing-forms/zod";
import { appKeysSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appKeysSchema as shimmervideo_zod_ts } from "./shimmervideo/zod";
import { appKeysSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
Expand Down Expand Up @@ -58,6 +59,7 @@ export const appKeysSchemas = {
qr_code: qr_code_zod_ts,
"routing-forms": routing_forms_zod_ts,
salesforce: salesforce_zod_ts,
shimmervideo: shimmervideo_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
"booking-pages-tag": booking_pages_tag_zod_ts,
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/apps.metadata.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import riverside_config_json from "./riverside/config.json";
import routing_forms_config_json from "./routing-forms/config.json";
import salesforce_config_json from "./salesforce/config.json";
import sendgrid_config_json from "./sendgrid/config.json";
import shimmervideo_config_json from "./shimmervideo/config.json";
import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json";
import skiff_config_json from "./skiff/config.json";
Expand Down Expand Up @@ -121,6 +122,7 @@ export const appStoreMetadata = {
"routing-forms": routing_forms_config_json,
salesforce: salesforce_config_json,
sendgrid: sendgrid_config_json,
shimmervideo: shimmervideo_config_json,
signal: signal_config_json,
sirius_video: sirius_video_config_json,
skiff: skiff_config_json,
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/apps.schemas.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { appDataSchema as plausible_zod_ts } from "./plausible/zod";
import { appDataSchema as qr_code_zod_ts } from "./qr_code/zod";
import { appDataSchema as routing_forms_zod_ts } from "./routing-forms/zod";
import { appDataSchema as salesforce_zod_ts } from "./salesforce/zod";
import { appDataSchema as shimmervideo_zod_ts } from "./shimmervideo/zod";
import { appDataSchema as stripepayment_zod_ts } from "./stripepayment/zod";
import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
Expand Down Expand Up @@ -58,6 +59,7 @@ export const appDataSchemas = {
qr_code: qr_code_zod_ts,
"routing-forms": routing_forms_zod_ts,
salesforce: salesforce_zod_ts,
shimmervideo: shimmervideo_zod_ts,
stripe: stripepayment_zod_ts,
tandemvideo: tandemvideo_zod_ts,
"booking-pages-tag": booking_pages_tag_zod_ts,
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/apps.server.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const apiHandlers = {
"routing-forms": import("./routing-forms/api"),
salesforce: import("./salesforce/api"),
sendgrid: import("./sendgrid/api"),
shimmervideo: import("./shimmervideo/api"),
signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"),
skiff: import("./skiff/api"),
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/bookerApps.metadata.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import office365video_config_json from "./office365video/config.json";
import ping_config_json from "./ping/config.json";
import plausible_config_json from "./plausible/config.json";
import riverside_config_json from "./riverside/config.json";
import shimmervideo_config_json from "./shimmervideo/config.json";
import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json";
import sylapsvideo_config_json from "./sylapsvideo/config.json";
Expand Down Expand Up @@ -53,6 +54,7 @@ export const appStoreMetadata = {
ping: ping_config_json,
plausible: plausible_config_json,
riverside: riverside_config_json,
shimmervideo: shimmervideo_config_json,
signal: signal_config_json,
sirius_video: sirius_video_config_json,
sylapsvideo: sylapsvideo_config_json,
Expand Down
1 change: 1 addition & 0 deletions packages/app-store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const appStore = {
"zoho-bigin": () => import("./zoho-bigin"),
basecamp3: () => import("./basecamp3"),
telegramvideo: () => import("./telegram"),
shimmervideo: () => import("./shimmervideo"),
};

export default appStore;
7 changes: 7 additions & 0 deletions packages/app-store/shimmervideo/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
items:
- 1.jpeg
- 2.jpeg
---

{DESCRIPTION}
16 changes: 16 additions & 0 deletions packages/app-store/shimmervideo/api/add.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";

import appConfig from "../config.json";

const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};

export default handler;
1 change: 1 addition & 0 deletions packages/app-store/shimmervideo/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as add } from "./add";
Empty file.
23 changes: 23 additions & 0 deletions packages/app-store/shimmervideo/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Shimmer Video",
"slug": "shimmervideo",
"type": "shimmer_video",
"logo": "icon.png",
"url": "https://shimmer.care",
"variant": "conferencing",
"categories": ["conferencing"],
"publisher": "Shimmer.care",
"email": "[email protected]",
"description": "The #1 Expert ADHD Coach. Weekly calls and in-app support so that you can reach your full potential",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"appData": {
"location": {
"linkType": "dynamic",
"type": "integrations:shimmer_video",
"label": "Shimmer Video"
}
}
}
2 changes: 2 additions & 0 deletions packages/app-store/shimmervideo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";
195 changes: 195 additions & 0 deletions packages/app-store/shimmervideo/lib/VideoApiAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { z } from "zod";

import { handleErrorsJson } from "@calcom/lib/errors";
import type { GetRecordingsResponseSchema, GetAccessLinkResponseSchema } from "@calcom/prisma/zod-utils";
import { getRecordingsResponseSchema, getAccessLinkResponseSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";

import { getShimmerAppKeys } from "./getShimmerAppKeys";

/** Shimmer Video app type in the config.json
* changed to 'shimmer_video' to support video conferencing
*/

/** @link https://docs.daily.co/reference/rest-api/rooms/create-room */
const dailyReturnTypeSchema = z.object({
/** Long UID string ie: 987b5eb5-d116-4a4e-8e2c-14fcb5710966 */
id: z.string(),
/** Not a real name, just a random generated string ie: "ePR84NQ1bPigp79dDezz" */
name: z.string(),
api_created: z.boolean(),
privacy: z.union([z.literal("private"), z.literal("public")]),
/** https://api-demo.daily.co/ePR84NQ1bPigp79dDezz */
url: z.string(),
created_at: z.string(),
config: z.object({
enable_prejoin_ui: z.boolean(),
enable_people_ui: z.boolean(),
enable_emoji_reactions: z.boolean(),
enable_pip_ui: z.boolean(),
enable_hand_raising: z.boolean(),
enable_network_ui: z.boolean(),
enable_video_processing_ui: z.boolean(),
enable_noise_cancellation_ui: z.boolean(),
enable_advanced_chat: z.boolean(),
//above flags are for prebuilt daily
enable_chat: z.boolean(),
enable_knocking: z.boolean(),
}),
});

export interface DailyEventResult {
id: string;
name: string;
api_created: boolean;
privacy: string;
url: string;
created_at: string;
config: Record<string, unknown>;
}

export interface DailyVideoCallData {
type: string;
id: string;
password: string;
url: string;
}

export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
const { api_key } = await getShimmerAppKeys();
const response = await fetch(`https://api.daily.co/v1${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
}).then(handleErrorsJson);
return response;
};

export const fetcherShimmer = async (endpoint: string, init?: RequestInit | undefined) => {
const { api_key, api_route } = await getShimmerAppKeys();

if (!api_route) {
//if no api_route, then we wont push to shimmer
return Promise.resolve([]);
}

const response = await fetch(`${api_route}${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
});

return response;
};

export const postToShimmerAPI = async (
event: CalendarEvent,
endpoint: string,
body: Record<string, unknown>
) => {
return fetcherShimmer(endpoint, {
method: "POST",
body: JSON.stringify({
cal: event,
daily: body,
}),
});
};

function postToDailyAPI(endpoint: string, body: Record<string, unknown>) {
return fetcher(endpoint, {
method: "POST",
body: JSON.stringify(body),
});
}

const ShimmerDailyVideoApiAdapter = (): VideoApiAdapter => {
async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise<VideoCallData> {
if (!event.uid) {
throw new Error("We need need the booking uid to create the Daily reference in DB");
}
const body = await translateEvent();
const dailyEvent = await postToDailyAPI(endpoint, body).then(dailyReturnTypeSchema.parse);
// const meetingToken = await postToDailyAPI("/meeting-tokens", {
// properties: { room_name: dailyEvent.name, exp: dailyEvent.config.exp, is_owner: true },
// }).then(meetingTokenSchema.parse);
await postToShimmerAPI(event, "trackDailyRoom", dailyEvent);

return Promise.resolve({
type: "shimmer_video",
id: dailyEvent.name,
password: "",
// password: meetingToken.token,
url: `https://app.shimmer.care?videoId=${dailyEvent.name}`,
});
}

const translateEvent = async () => {
return {
privacy: "private",
properties: {
enable_prejoin_ui: true,
enable_people_ui: true,
enable_emoji_reactions: true,
enable_pip_ui: true,
enable_hand_raising: true,
enable_network_ui: true,
enable_video_processing_ui: true,
enable_noise_cancellation_ui: true,
enable_advanced_chat: true,
//above flags are for prebuilt daily
enable_knocking: true,
enable_screenshare: true,
enable_chat: true,
},
};
};

return {
/** Daily doesn't need to return busy times, so we return empty */
getAvailability: () => {
return Promise.resolve([]);
},
createMeeting: async (event: CalendarEvent): Promise<VideoCallData> =>
createOrUpdateMeeting("/rooms", event),
deleteMeeting: async (uid: string): Promise<void> => {
await fetcher(`/rooms/${uid}`, { method: "DELETE" });
return Promise.resolve();
},
updateMeeting: (bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData> =>
createOrUpdateMeeting(`/rooms/${bookingRef.uid}`, event),
getRecordings: async (roomName: string): Promise<GetRecordingsResponseSchema> => {
try {
const res = await fetcher(`/recordings?room_name=${roomName}`).then(
getRecordingsResponseSchema.parse
);
return Promise.resolve(res);
} catch (err) {
throw new Error("Something went wrong! Unable to get recording");
}
},
getRecordingDownloadLink: async (recordingId: string): Promise<GetAccessLinkResponseSchema> => {
try {
const res = await fetcher(`/recordings/${recordingId}/access-link?valid_for_secs=172800`).then(
getAccessLinkResponseSchema.parse
);
return Promise.resolve(res);
} catch (err) {
console.log("err", err);
throw new Error("Something went wrong! Unable to get recording access link");
}
},
};
};

export default ShimmerDailyVideoApiAdapter;
13 changes: 13 additions & 0 deletions packages/app-store/shimmervideo/lib/getShimmerAppKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";

import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";

const shimmerAppKeysSchema = z.object({
api_key: z.string(),
api_route: z.string(),
});

export const getShimmerAppKeys = async () => {
const appKeys = await getAppKeysFromSlug("shimmer-video");
return shimmerAppKeysSchema.parse(appKeys);
};
1 change: 1 addition & 0 deletions packages/app-store/shimmervideo/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as VideoApiAdapter } from "./VideoApiAdapter";
14 changes: 14 additions & 0 deletions packages/app-store/shimmervideo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/shimmer-video",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "The #1 Expert ADHD Coach. Weekly calls and in-app support so that you can reach your full potential"
}
Binary file added packages/app-store/shimmervideo/static/1.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app-store/shimmervideo/static/2.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app-store/shimmervideo/static/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/app-store/shimmervideo/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from "zod";

export const appKeysSchema = z.object({
api_key: z.string(),
api_route: z.string(),
});

export const appDataSchema = z.object({});
3 changes: 1 addition & 2 deletions packages/core/EventManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default class EventManager {
// (type closecom_other_calendar)
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
this.videoCredentials = appCredentials
.filter((cred) => cred.type.endsWith("_video"))
.filter((cred) => cred.type.endsWith("_video") || cred.type.endsWith("_conferencing"))
// Whenever a new video connection is added, latest credentials are added with the highest ID.
// Because you can't rely on having them in the highest first order here, ensure this by sorting in DESC order
// We also don't have updatedAt or createdAt dates on credentials so this is the best we can do
Expand Down Expand Up @@ -654,7 +654,6 @@ export default class EventManager {
*/
private async createVideoEvent(event: CalendarEvent) {
const credential = this.getVideoCredential(event);

if (credential) {
return createMeeting(credential, event);
} else {
Expand Down

0 comments on commit 6848362

Please sign in to comment.