diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 40f6d986d6..c896d27cdc 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -4,6 +4,7 @@ **/ import { appKeysSchema as alby_zod_ts } from "./alby/zod"; import { appKeysSchema as basecamp3_zod_ts } from "./basecamp3/zod"; +import { appKeysSchema as campsite_zod_ts } from "./campsite/zod"; import { appKeysSchema as closecom_zod_ts } from "./closecom/zod"; import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; import { appKeysSchema as fathom_zod_ts } from "./fathom/zod"; @@ -50,6 +51,7 @@ import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; export const appKeysSchemas = { alby: alby_zod_ts, basecamp3: basecamp3_zod_ts, + campsite: campsite_zod_ts, closecom: closecom_zod_ts, dailyvideo: dailyvideo_zod_ts, fathom: fathom_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index bc43051456..b50a5ae7d2 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -12,6 +12,7 @@ import basecamp3_config_json from "./basecamp3/config.json"; import cal_ai_config_json from "./cal-ai/config.json"; import { metadata as caldavcalendar__metadata_ts } from "./caldavcalendar/_metadata"; import campfire_config_json from "./campfire/config.json"; +import campsite_config_json from "./campsite/config.json"; import clic_config_json from "./clic/config.json"; import closecom_config_json from "./closecom/config.json"; import cron_config_json from "./cron/config.json"; @@ -103,6 +104,7 @@ export const appStoreMetadata = { "cal-ai": cal_ai_config_json, caldavcalendar: caldavcalendar__metadata_ts, campfire: campfire_config_json, + campsite: campsite_config_json, clic: clic_config_json, closecom: closecom_config_json, cron: cron_config_json, diff --git a/packages/app-store/apps.schemas.generated.ts b/packages/app-store/apps.schemas.generated.ts index 318c654374..8fc3cd923d 100644 --- a/packages/app-store/apps.schemas.generated.ts +++ b/packages/app-store/apps.schemas.generated.ts @@ -4,6 +4,7 @@ **/ import { appDataSchema as alby_zod_ts } from "./alby/zod"; import { appDataSchema as basecamp3_zod_ts } from "./basecamp3/zod"; +import { appDataSchema as campsite_zod_ts } from "./campsite/zod"; import { appDataSchema as closecom_zod_ts } from "./closecom/zod"; import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod"; import { appDataSchema as fathom_zod_ts } from "./fathom/zod"; @@ -50,6 +51,7 @@ import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod"; export const appDataSchemas = { alby: alby_zod_ts, basecamp3: basecamp3_zod_ts, + campsite: campsite_zod_ts, closecom: closecom_zod_ts, dailyvideo: dailyvideo_zod_ts, fathom: fathom_zod_ts, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 0fc49f5327..9ba075ee3c 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -13,6 +13,7 @@ export const apiHandlers = { "cal-ai": import("./cal-ai/api"), caldavcalendar: import("./caldavcalendar/api"), campfire: import("./campfire/api"), + campsite: import("./campsite/api"), clic: import("./clic/api"), closecom: import("./closecom/api"), cron: import("./cron/api"), diff --git a/packages/app-store/bookerApps.metadata.generated.ts b/packages/app-store/bookerApps.metadata.generated.ts index d56837a050..04fd743a8c 100644 --- a/packages/app-store/bookerApps.metadata.generated.ts +++ b/packages/app-store/bookerApps.metadata.generated.ts @@ -4,6 +4,7 @@ **/ import around_config_json from "./around/config.json"; import campfire_config_json from "./campfire/config.json"; +import campsite_config_json from "./campsite/config.json"; import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata"; import demodesk_config_json from "./demodesk/config.json"; import discord_config_json from "./discord/config.json"; @@ -45,6 +46,7 @@ import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata"; export const appStoreMetadata = { around: around_config_json, campfire: campfire_config_json, + campsite: campsite_config_json, dailyvideo: dailyvideo__metadata_ts, demodesk: demodesk_config_json, discord: discord_config_json, diff --git a/packages/app-store/campsite/DESCRIPTION.md b/packages/app-store/campsite/DESCRIPTION.md new file mode 100644 index 0000000000..61e7037d43 --- /dev/null +++ b/packages/app-store/campsite/DESCRIPTION.md @@ -0,0 +1,8 @@ +--- +items: + - 1.png + - 2.png + - 3.png +--- + +{DESCRIPTION} diff --git a/packages/app-store/campsite/api/add.ts b/packages/app-store/campsite/api/add.ts new file mode 100644 index 0000000000..56a0b14609 --- /dev/null +++ b/packages/app-store/campsite/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 campsiteAppKeysSchema = z.object({ + client_id: z.string(), + client_secret: z.string(), +}); + +export const getCampsiteAppKeys = async () => { + const appKeys = await getAppKeysFromSlug("campsite"); + return campsiteAppKeysSchema.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 getCampsiteAppKeys(); + const state = encodeOAuthState(req); + + const params = { + response_type: "code", + client_id, + redirect_uri: `${WEBAPP_URL}/api/integrations/campsite/callback`, + state, + scope: "read_user write_call_room", + }; + const query = stringify(params); + const url = `https://auth.campsite.co/oauth/authorize?${query}`; + return { url }; +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/packages/app-store/campsite/api/callback.ts b/packages/app-store/campsite/api/callback.ts new file mode 100644 index 0000000000..06a9f09d22 --- /dev/null +++ b/packages/app-store/campsite/api/callback.ts @@ -0,0 +1,73 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; +import prisma from "@calcom/prisma"; + +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential"; +import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const state = decodeOAuthState(req); + 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("campsite"); + + const result = await fetch(`https://auth.campsite.co/oauth/token`, { + method: "POST", + body: JSON.stringify({ + grant_type: "authorization_code", + code, + client_id, + client_secret, + redirect_uri: `${WEBAPP_URL}/api/integrations/campsite/callback`, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + if (result.status !== 200) { + let errorMessage = "Something is wrong with the Campsite 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 Campsite keys for a single user + * when creating a room using deleteMany if there is already a Campsite key + * */ + await prisma.credential.deleteMany({ + where: { + type: "campsite_conferencing", + userId, + appId: "campsite", + }, + }); + + await createOAuthAppCredential( + { appId: "campsite", type: "campsite_conferencing" }, + { access_token: responseBody.access_token }, + req + ); + + res.redirect( + getSafeRedirectUrl(state?.returnTo) ?? getInstalledAppPath({ variant: "conferencing", slug: "campsite" }) + ); +} diff --git a/packages/app-store/campsite/api/index.ts b/packages/app-store/campsite/api/index.ts new file mode 100644 index 0000000000..eb12c1b4ed --- /dev/null +++ b/packages/app-store/campsite/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/campsite/config.json b/packages/app-store/campsite/config.json new file mode 100644 index 0000000000..de891b0547 --- /dev/null +++ b/packages/app-store/campsite/config.json @@ -0,0 +1,23 @@ +{ + "/*": "Don't modify slug - If required, do it using cli edit command", + "name": "Campsite", + "slug": "campsite", + "type": "campsite_conferencing", + "logo": "icon.svg", + "url": "https://campsite.co", + "variant": "conferencing", + "categories": ["conferencing"], + "publisher": "Campsite", + "email": "support@campsite.co", + "appData": { + "location": { + "type": "integrations:{SLUG}_conferencing", + "label": "{TITLE}", + "linkType": "dynamic" + } + }, + "description": "Campsite is an all-in-one communication app that helps async teams move faster. Posts, docs, calls, and chat in one place — finally. Everything is searchable and easy to find.", + "isTemplate": false, + "__createdUsingCli": true, + "__template": "event-type-location-video-static" +} diff --git a/packages/app-store/campsite/index.ts b/packages/app-store/campsite/index.ts new file mode 100644 index 0000000000..e2e9d7b029 --- /dev/null +++ b/packages/app-store/campsite/index.ts @@ -0,0 +1,2 @@ +export * as api from "./api"; +export * as lib from "./lib"; diff --git a/packages/app-store/campsite/lib/VideoApiAdapter.ts b/packages/app-store/campsite/lib/VideoApiAdapter.ts new file mode 100644 index 0000000000..dd38079c7e --- /dev/null +++ b/packages/app-store/campsite/lib/VideoApiAdapter.ts @@ -0,0 +1,47 @@ +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 CampsiteToken = { + access_token: string; +}; +const CampsiteVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => { + return { + createMeeting: async (_event: CalendarEvent): Promise => { + const keys = credential.key as CampsiteToken; + const { access_token } = keys; + const response = await fetch("https://api.campsite.co/v1/integrations/cal_dot_com/call_rooms", { + method: "POST", + headers: { + Authorization: `Bearer ${access_token}`, + "Content-Type": "application/json", + }, + }); + const callRoom = await response.json(); + + return { + type: "campsite_conferencing", + id: callRoom.id, + password: "", + url: callRoom.url, + }; + }, + updateMeeting: async (bookingRef: PartialReference, _event: CalendarEvent): Promise => { + return { + type: "campsite_conferencing", + id: bookingRef.externalCalendarId ? bookingRef.externalCalendarId : "", + password: "", + url: bookingRef.meetingUrl ? bookingRef.meetingUrl : "", + }; + }, + deleteMeeting: async (_uid: string): Promise => { + return {}; + }, + getAvailability: async (_dateFrom?: string, _dateTo?: string): Promise => { + return []; + }, + }; +}; + +export default CampsiteVideoApiAdapter; diff --git a/packages/app-store/campsite/lib/index.ts b/packages/app-store/campsite/lib/index.ts new file mode 100644 index 0000000000..dc61768d60 --- /dev/null +++ b/packages/app-store/campsite/lib/index.ts @@ -0,0 +1 @@ +export { default as VideoApiAdapter } from "./VideoApiAdapter"; diff --git a/packages/app-store/campsite/package.json b/packages/app-store/campsite/package.json new file mode 100644 index 0000000000..afce1273f4 --- /dev/null +++ b/packages/app-store/campsite/package.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "name": "@calcom/campsite", + "version": "0.0.0", + "main": "./index.ts", + "dependencies": { + "@calcom/lib": "*" + }, + "devDependencies": { + "@calcom/types": "*" + }, + "description": "Campsite is an all-in-one communication app that helps async teams move faster. Posts, docs, calls, and chat in one place — finally. Everything is searchable and easy to find." +} diff --git a/packages/app-store/campsite/static/1.png b/packages/app-store/campsite/static/1.png new file mode 100644 index 0000000000..c90fd86c22 Binary files /dev/null and b/packages/app-store/campsite/static/1.png differ diff --git a/packages/app-store/campsite/static/2.png b/packages/app-store/campsite/static/2.png new file mode 100644 index 0000000000..a4b9149e56 Binary files /dev/null and b/packages/app-store/campsite/static/2.png differ diff --git a/packages/app-store/campsite/static/3.png b/packages/app-store/campsite/static/3.png new file mode 100644 index 0000000000..3c73c072ff Binary files /dev/null and b/packages/app-store/campsite/static/3.png differ diff --git a/packages/app-store/campsite/static/icon.svg b/packages/app-store/campsite/static/icon.svg new file mode 100644 index 0000000000..35e6cd92d1 --- /dev/null +++ b/packages/app-store/campsite/static/icon.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app-store/campsite/zod.ts b/packages/app-store/campsite/zod.ts new file mode 100644 index 0000000000..0a84054ebe --- /dev/null +++ b/packages/app-store/campsite/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), +}); diff --git a/packages/app-store/index.ts b/packages/app-store/index.ts index 7512438254..8d8c7343f8 100644 --- a/packages/app-store/index.ts +++ b/packages/app-store/index.ts @@ -4,6 +4,7 @@ const appStore = { applecalendar: () => import("./applecalendar"), aroundvideo: () => import("./around"), caldavcalendar: () => import("./caldavcalendar"), + campsiteconferencing: () => import("./campsite"), closecom: () => import("./closecom"), dailyvideo: () => import("./dailyvideo"), googlecalendar: () => import("./googlecalendar"),