Skip to content

Commit e2cfd0d

Browse files
aar2dee2PeerRichhariombalhara
authored
Webex integration (#7651)
* start webex app creation * webex integration wip * fix lint errors * fix lint errors * add webex env vars in appStore.example * webex app wip * fix lint errors * edit webex oauth scopes * add location in webex app config * add site url placeholder and regex in webex config location * debug translateEvent * fix utc formatting for event start time, add test boilerplate for webex, add envs * fix location and datetime formatting * get correct videoCredentials for deleteMeeting * Move webex specific readme content to webex README * Fix app not visible in app-store * Delete setup route * add webex icon * delete prev icon * webex api fix * add app screenshots * Revert tests changes as they dont run * Use config instead of hardcoding vales * Update README * Remove all env variables related to WEBEX app. They can be added through settings->admin->apps interface * update from origin * fix icon path * update webex readme * Update yarn.lock * update webex readme * Remove unnecessary URL from webex * revert changes in cancel booking handler * simply webex zod schemas, logs for debugging --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
1 parent 067b77f commit e2cfd0d

31 files changed

+912
-86
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,10 @@ following
460460
9. Click the "Save" button at the bottom footer.
461461
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
462462

463+
### Obtaining Webex Client ID and Secret
464+
465+
[See Webex Readme](./packages/app-store/webex/)
466+
463467
### Obtaining ZohoCRM Client ID and Secret
464468

465469
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.

packages/app-store/apps.keys-schemas.generated.ts

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { appKeysSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
2424
import { appKeysSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
2525
import { appKeysSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod";
2626
import { appKeysSchema as vital_zod_ts } from "./vital/zod";
27+
import { appKeysSchema as webex_zod_ts } from "./webex/zod";
2728
import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod";
2829
import { appKeysSchema as zapier_zod_ts } from "./zapier/zod";
2930
import { appKeysSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod";
@@ -53,6 +54,7 @@ export const appKeysSchemas = {
5354
"booking-pages-tag": booking_pages_tag_zod_ts,
5455
"event-type-app-card": event_type_app_card_zod_ts,
5556
vital: vital_zod_ts,
57+
webex: webex_zod_ts,
5658
wordpress: wordpress_zod_ts,
5759
zapier: zapier_zod_ts,
5860
"zoho-bigin": zoho_bigin_zod_ts,

packages/app-store/apps.metadata.generated.ts

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import typeform_config_json from "./typeform/config.json";
5555
import vimcal_config_json from "./vimcal/config.json";
5656
import { metadata as vital__metadata_ts } from "./vital/_metadata";
5757
import weather_in_your_calendar_config_json from "./weather_in_your_calendar/config.json";
58+
import webex_config_json from "./webex/config.json";
5859
import whatsapp_config_json from "./whatsapp/config.json";
5960
import whereby_config_json from "./whereby/config.json";
6061
import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata";
@@ -118,6 +119,7 @@ export const appStoreMetadata = {
118119
vimcal: vimcal_config_json,
119120
vital: vital__metadata_ts,
120121
weather_in_your_calendar: weather_in_your_calendar_config_json,
122+
webex: webex_config_json,
121123
whatsapp: whatsapp_config_json,
122124
whereby: whereby_config_json,
123125
wipemycalother: wipemycalother__metadata_ts,

packages/app-store/apps.schemas.generated.ts

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { appDataSchema as tandemvideo_zod_ts } from "./tandemvideo/zod";
2424
import { appDataSchema as booking_pages_tag_zod_ts } from "./templates/booking-pages-tag/zod";
2525
import { appDataSchema as event_type_app_card_zod_ts } from "./templates/event-type-app-card/zod";
2626
import { appDataSchema as vital_zod_ts } from "./vital/zod";
27+
import { appDataSchema as webex_zod_ts } from "./webex/zod";
2728
import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod";
2829
import { appDataSchema as zapier_zod_ts } from "./zapier/zod";
2930
import { appDataSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod";
@@ -53,6 +54,7 @@ export const appDataSchemas = {
5354
"booking-pages-tag": booking_pages_tag_zod_ts,
5455
"event-type-app-card": event_type_app_card_zod_ts,
5556
vital: vital_zod_ts,
57+
webex: webex_zod_ts,
5658
wordpress: wordpress_zod_ts,
5759
zapier: zapier_zod_ts,
5860
"zoho-bigin": zoho_bigin_zod_ts,

packages/app-store/apps.server.generated.ts

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const apiHandlers = {
5555
vimcal: import("./vimcal/api"),
5656
vital: import("./vital/api"),
5757
weather_in_your_calendar: import("./weather_in_your_calendar/api"),
58+
webex: import("./webex/api"),
5859
whatsapp: import("./whatsapp/api"),
5960
whereby: import("./whereby/api"),
6061
wipemycalother: import("./wipemycalother/api"),

packages/app-store/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const appStore = {
2121
vital: () => import("./vital"),
2222
zoomvideo: () => import("./zoomvideo"),
2323
wipemycalother: () => import("./wipemycalother"),
24+
webexvideo: () => import("./webex"),
2425
giphy: () => import("./giphy"),
2526
zapier: () => import("./zapier"),
2627
exchange2013calendar: () => import("./exchange2013calendar"),
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
items:
3+
- 1.jpeg
4+
- 2.jpeg
5+
- 3.jpeg
6+
- 4.jpeg
7+
---
8+
9+
{DESCRIPTION}

packages/app-store/webex/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
### Obtaining Webex Client ID and Secret
2+
3+
1. Create a [Webex](https://www.webex.com/) acount, if you don't already have one.
4+
2. Go to [Webex for Developers](https://developer.webex.com/) and sign into to your Webex account. (Note: If you're creating a new account, create it on [Webex](https://www.webex.com/), not on [Webex for Developers](https://developer.webex.com/))
5+
3. On the upper right, click the profile icon and go to ["My Webex Apps"](https://developer.webex.com/my-apps)
6+
4. Click on "Create a New App" and select ["Integration"](https://developer.webex.com/my-apps/new/integration)
7+
5. Choose "No" for "Will this use a mobile SDK?"
8+
6. Give your app a name.
9+
7. Upload an icon or choose one of the default icons.
10+
8. Give your app a short description.
11+
9. Set the Redirect URI as `<Cal.com URL>/api/integrations/webex/callback` replacing Cal.com URL with the URI at which your application runs.
12+
10. Select the following scopes: "meeting:schedules_read", "meeting:schedules_write".
13+
11. Click "Add Integration".
14+
12. Copy the Client ID and Client Secret and add these while enabling the app through Settings -> Admin -> Apps interface

packages/app-store/webex/api/add.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { NextApiRequest } from "next";
2+
import { stringify } from "querystring";
3+
4+
import { WEBAPP_URL } from "@calcom/lib/constants";
5+
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
6+
import prisma from "@calcom/prisma";
7+
8+
import config from "../config.json";
9+
import { getWebexAppKeys } from "../lib/getWebexAppKeys";
10+
11+
async function handler(req: NextApiRequest) {
12+
// Get user
13+
await prisma.user.findFirstOrThrow({
14+
where: {
15+
id: req.session?.user?.id,
16+
},
17+
select: {
18+
id: true,
19+
},
20+
});
21+
22+
const { client_id } = await getWebexAppKeys();
23+
24+
/** @link https://developer.webex.com/docs/integrations#requesting-permission */
25+
const params = {
26+
response_type: "code",
27+
client_id,
28+
redirect_uri: `${WEBAPP_URL}/api/integrations/${config.slug}/callback`,
29+
scope: "spark:kms meeting:schedules_read meeting:schedules_write", //should be "A space-separated list of scopes being requested by your integration"
30+
state: "",
31+
};
32+
const query = stringify(params).replaceAll("+", "%20");
33+
const url = `https://webexapis.com/v1/authorize?${query}`;
34+
return { url };
35+
}
36+
37+
export default defaultHandler({
38+
GET: Promise.resolve({ default: defaultResponder(handler) }),
39+
});
+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
3+
import { WEBAPP_URL } from "@calcom/lib/constants";
4+
import prisma from "@calcom/prisma";
5+
6+
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
7+
import config from "../config.json";
8+
import { getWebexAppKeys } from "../lib/getWebexAppKeys";
9+
10+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
11+
const { code } = req.query;
12+
const { client_id, client_secret } = await getWebexAppKeys();
13+
14+
/** @link https://developer.webex.com/docs/integrations#getting-an-access-token **/
15+
16+
const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/${config.slug}/callback`);
17+
const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64");
18+
const result = await fetch(
19+
"https://webexapis.com/v1/access_token?grant_type=authorization_code&client_id" +
20+
client_id +
21+
"&client_secret=" +
22+
client_secret +
23+
"&code=" +
24+
code +
25+
"&redirect_uri=" +
26+
redirectUri,
27+
{
28+
method: "POST",
29+
headers: {
30+
Authorization: authHeader,
31+
"Content-Type": "application/x-www-form-urlencoded",
32+
},
33+
}
34+
);
35+
36+
if (result.status !== 200) {
37+
let errorMessage = "Something is wrong with Webex API";
38+
try {
39+
const responseBody = await result.json();
40+
errorMessage = responseBody.error;
41+
} catch (e) {}
42+
43+
res.status(400).json({ message: errorMessage });
44+
return;
45+
}
46+
47+
const responseBody = await result.json();
48+
49+
if (responseBody.error) {
50+
res.status(400).json({ message: responseBody.error });
51+
return;
52+
}
53+
54+
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
55+
delete responseBody.expires_in;
56+
57+
const userId = req.session?.user.id;
58+
if (!userId) {
59+
return res.status(404).json({ message: "No user found" });
60+
}
61+
/**
62+
* With this we take care of no duplicate webex key for a single user
63+
* when creating a video room we only do findFirst so the if they have more than 1
64+
* others get ignored
65+
* */
66+
const existingCredentialWebexVideo = await prisma.credential.findMany({
67+
select: {
68+
id: true,
69+
},
70+
where: {
71+
type: config.type,
72+
userId: req.session?.user.id,
73+
appId: config.slug,
74+
},
75+
});
76+
77+
// Making sure we only delete webex_video
78+
const credentialIdsToDelete = existingCredentialWebexVideo.map((item) => item.id);
79+
if (credentialIdsToDelete.length > 0) {
80+
await prisma.credential.deleteMany({ where: { id: { in: credentialIdsToDelete }, userId } });
81+
}
82+
83+
await prisma.user.update({
84+
where: {
85+
id: req.session?.user.id,
86+
},
87+
data: {
88+
credentials: {
89+
create: {
90+
type: config.type,
91+
key: responseBody,
92+
appId: config.slug,
93+
},
94+
},
95+
},
96+
});
97+
98+
res.redirect(getInstalledAppPath({ variant: config.variant, slug: config.slug }));
99+
}

packages/app-store/webex/api/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as add } from "./add";
2+
export { default as callback } from "./callback";

packages/app-store/webex/config.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"/*": "Don't modify slug - If required, do it using cli edit command",
3+
"name": "Webex",
4+
"title": "Webex",
5+
"slug": "webex",
6+
"type": "webex_video",
7+
"imageSrc": "/icon.ico",
8+
"logo": "/icon.ico",
9+
"url": "https://cal.com/apps/webex",
10+
"variant": "conferencing",
11+
"categories": ["video"],
12+
"publisher": "Cal.com, Inc.",
13+
"email": "support@cal.com",
14+
"description": "Create meetings with Cisco Webex",
15+
"appData": {
16+
"location": {
17+
"linkType": "dynamic",
18+
"type": "integrations:webex_video",
19+
"label": "Webex"
20+
}
21+
},
22+
"isTemplate": false,
23+
"__createdUsingCli": true,
24+
"__template": "basic"
25+
}

packages/app-store/webex/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * as api from "./api";
2+
export * as lib from "./lib";

0 commit comments

Comments
 (0)