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:
co-authored by
Joe Au-Yeung
Joe Au-Yeung
parent
3eadae0aad
commit
3a4f944aac
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
items:
|
||||
- 1.jpeg
|
||||
- 2.jpeg
|
||||
- 3.jpeg
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
@@ -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) }),
|
||||
});
|
||||
@@ -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" }));
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as VideoApiAdapter } from "./VideoApiAdapter";
|
||||
@@ -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 |
@@ -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),
|
||||
});
|
||||
Reference in New Issue
Block a user