diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index c9eb0a711e1c07..5fd2ca94e1f681 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -13,6 +13,7 @@ import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appKeysSchema as gtm_zod_ts } from "./gtm/zod"; import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appKeysSchema as intercom_zod_ts } from "./intercom/zod"; +import { appKeysSchema as jelly_zod_ts } from "./jelly/zod"; import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appKeysSchema as make_zod_ts } from "./make/zod"; @@ -53,6 +54,7 @@ export const appKeysSchemas = { gtm: gtm_zod_ts, hubspot: hubspot_zod_ts, intercom: intercom_zod_ts, + jelly: jelly_zod_ts, jitsivideo: jitsivideo_zod_ts, larkcalendar: larkcalendar_zod_ts, make: make_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 110d92e47e6d06..99bfc060dbd714 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -33,6 +33,7 @@ import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata"; import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json"; import intercom_config_json from "./intercom/config.json"; +import jelly_config_json from "./jelly/config.json"; import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata"; import linear_config_json from "./linear/config.json"; @@ -117,6 +118,7 @@ export const appStoreMetadata = { huddle01video: huddle01video__metadata_ts, "ics-feedcalendar": ics_feedcalendar_config_json, intercom: intercom_config_json, + jelly: jelly_config_json, jitsivideo: jitsivideo__metadata_ts, larkcalendar: larkcalendar__metadata_ts, linear: linear_config_json, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index bf3c6dd256f537..cf18cf0624d2ed 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -13,6 +13,7 @@ import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod"; import { appDataSchema as gtm_zod_ts } from "./gtm/zod"; import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod"; import { appDataSchema as intercom_zod_ts } from "./intercom/zod"; +import { appDataSchema as jelly_zod_ts } from "./jelly/zod"; import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appDataSchema as make_zod_ts } from "./make/zod"; @@ -53,6 +54,7 @@ export const appDataSchemas = { gtm: gtm_zod_ts, hubspot: hubspot_zod_ts, intercom: intercom_zod_ts, + jelly: jelly_zod_ts, jitsivideo: jitsivideo_zod_ts, larkcalendar: larkcalendar_zod_ts, make: make_zod_ts, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 972cdc73dea195..12c57a2c733dd7 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -33,6 +33,7 @@ export const apiHandlers = { huddle01video: import("./huddle01video/api"), "ics-feedcalendar": import("./ics-feedcalendar/api"), intercom: import("./intercom/api"), + jelly: import("./jelly/api"), jitsivideo: import("./jitsivideo/api"), larkcalendar: import("./larkcalendar/api"), linear: import("./linear/api"), diff --git a/packages/app-store/bookerApps.metadata.generated.ts b/packages/app-store/bookerApps.metadata.generated.ts index f52b8e1ab829b6..849b63fcbfcf28 100644 --- a/packages/app-store/bookerApps.metadata.generated.ts +++ b/packages/app-store/bookerApps.metadata.generated.ts @@ -15,6 +15,7 @@ import ga4_config_json from "./ga4/config.json"; import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata"; import gtm_config_json from "./gtm/config.json"; import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata"; +import jelly_config_json from "./jelly/config.json"; import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; import matomo_config_json from "./matomo/config.json"; import metapixel_config_json from "./metapixel/config.json"; @@ -52,6 +53,7 @@ export const appStoreMetadata = { googlevideo: googlevideo__metadata_ts, gtm: gtm_config_json, huddle01video: huddle01video__metadata_ts, + jelly: jelly_config_json, jitsivideo: jitsivideo__metadata_ts, matomo: matomo_config_json, metapixel: metapixel_config_json, diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 6a83795e395bad..751243825477ca 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -11,6 +11,7 @@ const appStore = { hubspot: () => import("./hubspot"), huddle01video: () => import("./huddle01video"), "ics-feedcalendar": () => import("./ics-feedcalendar"), + jellyconferencing: () => import("./jelly"), jitsivideo: () => import("./jitsivideo"), larkcalendar: () => import("./larkcalendar"), office365calendar: () => import("./office365calendar"), diff --git a/packages/app-store/jelly/DESCRIPTION.md b/packages/app-store/jelly/DESCRIPTION.md new file mode 100644 index 00000000000000..a4b15502c3bfb7 --- /dev/null +++ b/packages/app-store/jelly/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.jpeg + - 2.jpeg + - 3.jpeg +--- + +{DESCRIPTION} diff --git a/packages/app-store/jelly/api/add.ts b/packages/app-store/jelly/api/add.ts new file mode 100644 index 00000000000000..11cbaf52f2a564 --- /dev/null +++ b/packages/app-store/jelly/api/add.ts @@ -0,0 +1,45 @@ +import type { NextApiRequest } from "next"; +import { stringify } from "querystring"; +import { z } from "zod"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState"; + +const jellyAppKeysSchema = z.object({ + client_id: z.string(), + client_secret: z.string(), +}); + +export const getJellyAppKeys = async () => { + const appKeys = await getAppKeysFromSlug("jelly"); + return jellyAppKeysSchema.parse(appKeys); +}; + +async function handler(req: NextApiRequest) { + // Get user + const user = req?.session?.user; + if (!user) { + return { status: 401, body: { error: "Unauthorized" } }; + } + + const { client_id } = await getJellyAppKeys(); + const state = encodeOAuthState(req); + + const params = { + response_type: "code", + app_id: client_id, + redirect_uri: `${WEBAPP_URL}/api/integrations/jelly/callback`, + state, + scope: "write:jellies,read:user_email_phone", + }; + const query = stringify(params); + const url = `https://jellyjelly.com/login/oauth?${query}`; + return { url }; +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/jelly/api/callback.ts b/packages/app-store/jelly/api/callback.ts new file mode 100644 index 00000000000000..3ba62642868354 --- /dev/null +++ b/packages/app-store/jelly/api/callback.ts @@ -0,0 +1,58 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import prisma from "@calcom/prisma"; + +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const userId = req.session?.user.id; + if (!userId) { + return res.status(404).json({ message: "No user found" }); + } + const { code } = req.query; + const { client_id, client_secret } = await getAppKeysFromSlug("jelly"); + + const result = await fetch(`https://www.jellyjelly.com/login/oauth/access_token`, { + method: "POST", + body: JSON.stringify({ code, client_id, client_secret }), + }); + + if (result.status !== 200) { + let errorMessage = "Something is wrong with the Jelly API"; + try { + const responseBody = await result.json(); + errorMessage = responseBody.error; + } catch (e) {} + + res.status(400).json({ message: errorMessage }); + return; + } + + const responseBody = await result.json(); + if (responseBody.error) { + res.status(400).json({ message: responseBody.error }); + return; + } + + /** + * With this we take care of no duplicate jelly key for a single user + * when creating a room using deleteMany if there is already a jelly key + * */ + await prisma.credential.deleteMany({ + where: { + type: "jelly_conferencing", + userId, + appId: "jelly", + }, + }); + + await createOAuthAppCredential( + { appId: "jelly", type: "jelly_conferencing" }, + { access_token: responseBody.access_token }, + req + ); + + res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "jelly" })); +} diff --git a/packages/app-store/jelly/api/index.ts b/packages/app-store/jelly/api/index.ts new file mode 100644 index 00000000000000..eb12c1b4ed2c4f --- /dev/null +++ b/packages/app-store/jelly/api/index.ts @@ -0,0 +1,2 @@ +export { default as add } from "./add"; +export { default as callback } from "./callback"; diff --git a/packages/app-store/jelly/config.json b/packages/app-store/jelly/config.json new file mode 100644 index 00000000000000..7893b9cf662e30 --- /dev/null +++ b/packages/app-store/jelly/config.json @@ -0,0 +1,23 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Jelly", + "slug": "jelly", + "type": "jelly_conferencing", + "logo": "icon.svg", + "url": "https://jellyjelly.com", + "variant": "conferencing", + "categories": ["conferencing"], + "publisher": "Jelly", + "email": "support@jellyjelly.com", + "appData": { + "location": { + "type": "integrations:{SLUG}_video", + "label": "{TITLE}", + "linkType": "dynamic" + } + }, + "description": "Jelly is a camera-free voice chat platform. No frills, no makeup needed, just good talking. Our AI magic handles the rest by highlighting and publishing key moments with rich visuals.", + "isTemplate": false, + "__createdUsingCli": true, + "__template": "event-type-location-video-static" +} diff --git a/packages/app-store/jelly/index.ts b/packages/app-store/jelly/index.ts new file mode 100644 index 00000000000000..e2e9d7b029c031 --- /dev/null +++ b/packages/app-store/jelly/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export * as lib from "./lib"; diff --git a/packages/app-store/jelly/lib/VideoApiAdapter.ts b/packages/app-store/jelly/lib/VideoApiAdapter.ts new file mode 100644 index 00000000000000..d7056e6d51cf4e --- /dev/null +++ b/packages/app-store/jelly/lib/VideoApiAdapter.ts @@ -0,0 +1,52 @@ +import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar"; +import type { CredentialPayload } from "@calcom/types/Credential"; +import type { PartialReference } from "@calcom/types/EventManager"; +import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; + +type JellyToken = { + access_token: string; +}; +const JellyVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => { + return { + createMeeting: async (event: CalendarEvent): Promise => { + // get keys from slug + const keys = credential.key as JellyToken; + const { access_token } = keys; + // create jelly link + const jellyLink = await fetch("https://www.jellyjelly.com/api/ti/start_jelly", { + method: "POST", + headers: { + Authorization: `Bearer ${access_token}`, + "Content-Type": "application/json", + }, + }); + const jellyLinkData = await jellyLink.json(); + + return { + type: "jelly_conferencing", + id: jellyLinkData.talkId, + password: "", + url: jellyLinkData.url, + }; + }, + updateMeeting: async (bookingRef: PartialReference, event: CalendarEvent): Promise => { + // don't update jelly link + return { + type: "jelly_conferencing", + id: bookingRef.externalCalendarId ? bookingRef.externalCalendarId : "", + password: "", + url: bookingRef.meetingUrl ? bookingRef.meetingUrl : "", + }; + }, + deleteMeeting: async (uid: string): Promise => { + // delete jelly link + return {}; + }, + getAvailability: async (dateFrom?: string, dateTo?: string): Promise => { + // get jelly availability + return []; + }, + }; +}; + +export default JellyVideoApiAdapter; diff --git a/packages/app-store/jelly/lib/index.ts b/packages/app-store/jelly/lib/index.ts new file mode 100644 index 00000000000000..dc61768d6007df --- /dev/null +++ b/packages/app-store/jelly/lib/index.ts @@ -0,0 +1 @@ +export { default as VideoApiAdapter } from "./VideoApiAdapter"; diff --git a/packages/app-store/jelly/package.json b/packages/app-store/jelly/package.json new file mode 100644 index 00000000000000..e9d9cb6331f742 --- /dev/null +++ b/packages/app-store/jelly/package.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/jelly", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + }, + "description": "Jelly is a camera-free voice chat platform. No frills, no makeup needed, just good talking. Our AI magic handles the rest by highlighting and publishing key moments with rich visuals." +} diff --git a/packages/app-store/jelly/static/1.jpeg b/packages/app-store/jelly/static/1.jpeg new file mode 100644 index 00000000000000..3dbb6a7d6c80b7 Binary files /dev/null and b/packages/app-store/jelly/static/1.jpeg differ diff --git a/packages/app-store/jelly/static/2.jpeg b/packages/app-store/jelly/static/2.jpeg new file mode 100644 index 00000000000000..b55e9a29e0cdb3 Binary files /dev/null and b/packages/app-store/jelly/static/2.jpeg differ diff --git a/packages/app-store/jelly/static/3.jpeg b/packages/app-store/jelly/static/3.jpeg new file mode 100644 index 00000000000000..cbfd97255ae7b4 Binary files /dev/null and b/packages/app-store/jelly/static/3.jpeg differ diff --git a/packages/app-store/jelly/static/icon.svg b/packages/app-store/jelly/static/icon.svg new file mode 100644 index 00000000000000..43b702b3736f39 --- /dev/null +++ b/packages/app-store/jelly/static/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/app-store/jelly/zod.ts b/packages/app-store/jelly/zod.ts new file mode 100644 index 00000000000000..0a84054ebef3f9 --- /dev/null +++ b/packages/app-store/jelly/zod.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const appDataSchema = z.object({}); + +export const appKeysSchema = z.object({ + client_id: z.string().min(1), + client_secret: z.string().min(1), +});