feat: 11642 app shimmer video (#12159)

* 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 <peer@cal.com>
Co-authored-by: Vik <vsreed@stanford.edu>
Co-authored-by: pathaksarvesh <sarvesh@incrediblevisibility.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
This commit is contained in:
vikcodes 2023-11-09 03:33:50 -05:00 committed by GitHub
parent 09fc7e1a4c
commit 6848362683
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 291 additions and 2 deletions

View File

@ -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";
@ -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,

View File

@ -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";
@ -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,

View File

@ -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";
@ -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,

View File

@ -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"),

View File

@ -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";
@ -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,

View File

@ -37,6 +37,7 @@ const appStore = {
"zoho-bigin": () => import("./zoho-bigin"),
basecamp3: () => import("./basecamp3"),
telegramvideo: () => import("./telegram"),
shimmervideo: () => import("./shimmervideo"),
};
export default appStore;

View File

@ -0,0 +1,7 @@
---
items:
- 1.jpeg
- 2.jpeg
---
{DESCRIPTION}

View File

@ -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;

View File

@ -0,0 +1 @@
export { default as add } from "./add";

View File

@ -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": "support@shimmer.care",
"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"
}
}
}

View File

@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";

View File

@ -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;

View File

@ -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);
};

View File

@ -0,0 +1 @@
export { default as VideoApiAdapter } from "./VideoApiAdapter";

View File

@ -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 not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -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({});

View File

@ -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
@ -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 {