feat: jelly integration (#13929)

* v1

* don't need to search DB for user

* return early if no user

* use one deleteMany to ensure only 1 credential

* rm expiration

* fix API request

* proper type

* Update packages/app-store/jelly/lib/VideoApiAdapter.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Update packages/app-store/jelly/lib/VideoApiAdapter.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Update packages/app-store/jelly/lib/VideoApiAdapter.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Update packages/app-store/jelly/api/callback.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Fix callback URL

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
This commit is contained in:
Matt Hamilton
2024-03-01 16:45:41 +00:00
committed by GitHub
co-authored by Joe Au-Yeung Joe Au-Yeung
parent 3eadae0aad
commit 3a4f944aac
20 changed files with 233 additions and 0 deletions
@@ -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,
@@ -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,
@@ -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,
@@ -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"),
@@ -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,
+1
View File
@@ -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"),
+8
View File
@@ -0,0 +1,8 @@
---
items:
- 1.jpeg
- 2.jpeg
- 3.jpeg
---
{DESCRIPTION}
+45
View File
@@ -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) }),
});
+58
View File
@@ -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" }));
}
+2
View File
@@ -0,0 +1,2 @@
export { default as add } from "./add";
export { default as callback } from "./callback";
+23
View File
@@ -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"
}
+2
View File
@@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";
@@ -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<VideoCallData> => {
// 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<VideoCallData> => {
// 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<unknown> => {
// delete jelly link
return {};
},
getAvailability: async (dateFrom?: string, dateTo?: string): Promise<EventBusyDate[]> => {
// get jelly availability
return [];
},
};
};
export default JellyVideoApiAdapter;
+1
View File
@@ -0,0 +1 @@
export { default as VideoApiAdapter } from "./VideoApiAdapter";
+14
View File
@@ -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."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 756 KiB

+8
View File
@@ -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),
});