diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 5117b121e4..94c0ae6188 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -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, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 272d5f2aec..66023a079f 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -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, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 08d223e02d..73acac0dc6 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -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, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 5698485a62..763f43b86f 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -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"), diff --git a/packages/app-store/bookerApps.metadata.generated.ts b/packages/app-store/bookerApps.metadata.generated.ts index 9ac85a9b1e..4ecb096206 100644 --- a/packages/app-store/bookerApps.metadata.generated.ts +++ b/packages/app-store/bookerApps.metadata.generated.ts @@ -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, diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index d75d91c7ef..2a90955403 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -37,6 +37,7 @@ const appStore = { "zoho-bigin": () => import("./zoho-bigin"), basecamp3: () => import("./basecamp3"), telegramvideo: () => import("./telegram"), + shimmervideo: () => import("./shimmervideo"), }; export default appStore; diff --git a/packages/app-store/shimmervideo/DESCRIPTION.md b/packages/app-store/shimmervideo/DESCRIPTION.md new file mode 100644 index 0000000000..5546ed6f62 --- /dev/null +++ b/packages/app-store/shimmervideo/DESCRIPTION.md @@ -0,0 +1,7 @@ +--- +items: + - 1.jpeg + - 2.jpeg +--- + +{DESCRIPTION} diff --git a/packages/app-store/shimmervideo/api/add.ts b/packages/app-store/shimmervideo/api/add.ts new file mode 100644 index 0000000000..6ab3106577 --- /dev/null +++ b/packages/app-store/shimmervideo/api/add.ts @@ -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; diff --git a/packages/app-store/shimmervideo/api/index.ts b/packages/app-store/shimmervideo/api/index.ts new file mode 100644 index 0000000000..4c0d2ead01 --- /dev/null +++ b/packages/app-store/shimmervideo/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; diff --git a/packages/app-store/shimmervideo/components/.gitkeep b/packages/app-store/shimmervideo/components/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/app-store/shimmervideo/config.json b/packages/app-store/shimmervideo/config.json new file mode 100644 index 0000000000..fe80dbaf5e --- /dev/null +++ b/packages/app-store/shimmervideo/config.json @@ -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" + } + } +} diff --git a/packages/app-store/shimmervideo/index.ts b/packages/app-store/shimmervideo/index.ts new file mode 100644 index 0000000000..e2e9d7b029 --- /dev/null +++ b/packages/app-store/shimmervideo/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export * as lib from "./lib"; diff --git a/packages/app-store/shimmervideo/lib/VideoApiAdapter.ts b/packages/app-store/shimmervideo/lib/VideoApiAdapter.ts new file mode 100644 index 0000000000..0d2c60b6a0 --- /dev/null +++ b/packages/app-store/shimmervideo/lib/VideoApiAdapter.ts @@ -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; +} + +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 +) => { + return fetcherShimmer(endpoint, { + method: "POST", + body: JSON.stringify({ + cal: event, + daily: body, + }), + }); +}; + +function postToDailyAPI(endpoint: string, body: Record) { + return fetcher(endpoint, { + method: "POST", + body: JSON.stringify(body), + }); +} + +const ShimmerDailyVideoApiAdapter = (): VideoApiAdapter => { + async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise { + 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 => + createOrUpdateMeeting("/rooms", event), + deleteMeeting: async (uid: string): Promise => { + await fetcher(`/rooms/${uid}`, { method: "DELETE" }); + return Promise.resolve(); + }, + updateMeeting: (bookingRef: PartialReference, event: CalendarEvent): Promise => + createOrUpdateMeeting(`/rooms/${bookingRef.uid}`, event), + getRecordings: async (roomName: string): Promise => { + 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 => { + 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; diff --git a/packages/app-store/shimmervideo/lib/getShimmerAppKeys.ts b/packages/app-store/shimmervideo/lib/getShimmerAppKeys.ts new file mode 100644 index 0000000000..e265df3a55 --- /dev/null +++ b/packages/app-store/shimmervideo/lib/getShimmerAppKeys.ts @@ -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); +}; diff --git a/packages/app-store/shimmervideo/lib/index.ts b/packages/app-store/shimmervideo/lib/index.ts new file mode 100644 index 0000000000..dc61768d60 --- /dev/null +++ b/packages/app-store/shimmervideo/lib/index.ts @@ -0,0 +1 @@ +export { default as VideoApiAdapter } from "./VideoApiAdapter"; diff --git a/packages/app-store/shimmervideo/package.json b/packages/app-store/shimmervideo/package.json new file mode 100644 index 0000000000..8ac82378af --- /dev/null +++ b/packages/app-store/shimmervideo/package.json @@ -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" +} diff --git a/packages/app-store/shimmervideo/static/1.jpeg b/packages/app-store/shimmervideo/static/1.jpeg new file mode 100644 index 0000000000..bf34495b3e Binary files /dev/null and b/packages/app-store/shimmervideo/static/1.jpeg differ diff --git a/packages/app-store/shimmervideo/static/2.jpeg b/packages/app-store/shimmervideo/static/2.jpeg new file mode 100644 index 0000000000..8cc893abfe Binary files /dev/null and b/packages/app-store/shimmervideo/static/2.jpeg differ diff --git a/packages/app-store/shimmervideo/static/icon.png b/packages/app-store/shimmervideo/static/icon.png new file mode 100644 index 0000000000..ca159c6d47 Binary files /dev/null and b/packages/app-store/shimmervideo/static/icon.png differ diff --git a/packages/app-store/shimmervideo/zod.ts b/packages/app-store/shimmervideo/zod.ts new file mode 100644 index 0000000000..3228dd3e24 --- /dev/null +++ b/packages/app-store/shimmervideo/zod.ts @@ -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({}); diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index ff6e836b5d..6c41d76ca2 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -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 {