Fixes/app store keys in db (#2651)
* Adds available apps * Adds App Model * WIP * Updates seeder script * Seeder fixes * lowercase categories * Upgrades prisma * WIP * WIP * Hopefully fixes circular deps * Type fixes * Fixes seeder * Adds migration to connect Credentials to Apps * Updates app store callbacks * Updates google credentials * Uses dirName from DB * Type fixes * Update reschedule.ts * Seeder fixes * Fixes categories listing * Update index.ts * Update schema.prisma * Updates dependencies * Renames giphy app * Uses dynamic imports for app metadata * Fixes credentials error * Uses dynamic import for api handlers * Dynamic import fixes * Allows for simple folder names in app store * Squashes app migrations * seeder fixes * Fixes dyamic imports * Update apiHandlers.tsx
This commit is contained in:
parent
21867c9cd4
commit
6a27fb2959
68 changed files with 995 additions and 1628 deletions
|
@ -117,6 +117,7 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
|
|||
# **********************************************************************************************************
|
||||
|
||||
# - APP STORE **********************************************************************************************
|
||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||
# - DAILY.CO VIDEO
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 943cd10de1f6661273d2ec18acdaa93118852714
|
||||
Subproject commit cf71a8b47ec9d37da7e4facb61356a293cb0bd13
|
2
apps/api
2
apps/api
|
@ -1 +1 @@
|
|||
Subproject commit 2449d90bcbbf4c0a379f4d766aee299caad488a2
|
||||
Subproject commit 6124577bc21502c018378a299e50fc96bff14b99
|
|
@ -11,15 +11,15 @@ import {
|
|||
MoonIcon,
|
||||
ViewGridIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { UserPlan } from "@prisma/client";
|
||||
import { SessionContextValue, signOut, useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Fragment, ReactNode, useEffect } from "react";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useIsEmbed } from "@calcom/embed-core";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { UserPlan } from "@calcom/prisma/client";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Attendee } from "@prisma/client";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { Attendee } from "@calcom/prisma/client";
|
||||
import { Person } from "@calcom/types/Calendar";
|
||||
|
||||
export const attendeeToPersonConversionType = (attendees: Attendee[], t: TFunction): Person[] => {
|
||||
|
|
|
@ -6,7 +6,6 @@ import utc from "dayjs/plugin/utc";
|
|||
import { createEvent, DateArray, Person } from "ics";
|
||||
|
||||
import { getCancelLink } from "@calcom/lib/CalEventParser";
|
||||
import { Attendee } from "@calcom/prisma/client";
|
||||
import { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
|
||||
async function getBooking(prisma: PrismaClient, uid: string) {
|
||||
const booking = await prisma.booking.findFirst({
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { UserPlan } from "@calcom/prisma/client";
|
||||
import { UserPlan } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* TODO: It should be exposed at a single place.
|
||||
|
|
|
@ -87,7 +87,7 @@ const nextConfig = {
|
|||
];
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
const redirects = [
|
||||
{
|
||||
source: "/settings",
|
||||
destination: "/settings/profile",
|
||||
|
@ -104,6 +104,28 @@ const nextConfig = {
|
|||
permanent: false,
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
||||
redirects.push(
|
||||
{
|
||||
source: "/apps/dailyvideo",
|
||||
destination: "/apps/daily-video",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/apps/huddle01_video",
|
||||
destination: "/apps/huddle01",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/apps/jitsi_video",
|
||||
destination: "/apps/jitsi",
|
||||
permanent: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return redirects;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
|
@ -19,13 +17,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
||||
|
||||
try {
|
||||
// TODO: Find a way to dynamically import these modules
|
||||
// const app = (await import(`@calcom/${appName}`)).default;
|
||||
const app = appStore[appName as keyof typeof appStore];
|
||||
if (!(app && "api" in app && apiEndpoint in app.api))
|
||||
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
||||
|
||||
const handler = app.api[apiEndpoint as keyof typeof app.api] as NextApiHandler;
|
||||
/* Absolute path didn't work */
|
||||
const handlerMap = (await import("@calcom/app-store/apiHandlers")).default;
|
||||
const handlers = await handlerMap[appName as keyof typeof handlerMap];
|
||||
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
|
||||
|
||||
if (typeof handler !== "function")
|
||||
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import { GetStaticPaths, GetStaticPathsResult, GetStaticPropsContext } from "next";
|
||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import { MDXRemote } from "next-mdx-remote";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import path from "path";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import useMediaQuery from "@lib/hooks/useMediaQuery";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
@ -68,11 +69,8 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
||||
const appStore = getAppRegistry();
|
||||
const paths = appStore.reduce((paths, app) => {
|
||||
paths.push({ params: { slug: app.slug } });
|
||||
return paths;
|
||||
}, [] as GetStaticPathsResult<{ slug: string }>["paths"]);
|
||||
const appStore = await prisma.app.findMany({ select: { slug: true } });
|
||||
const paths = appStore.map(({ slug }) => ({ params: { slug } }));
|
||||
|
||||
return {
|
||||
paths,
|
||||
|
@ -81,23 +79,19 @@ export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
|
|||
};
|
||||
|
||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
const appStore = getAppRegistry();
|
||||
if (typeof ctx.params?.slug !== "string") return { notFound: true };
|
||||
|
||||
if (typeof ctx.params?.slug !== "string") {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const app = await prisma.app.findUnique({
|
||||
where: { slug: ctx.params.slug },
|
||||
});
|
||||
|
||||
const singleApp = appStore.find((app) => app.slug === ctx.params?.slug);
|
||||
if (!app) return { notFound: true };
|
||||
|
||||
if (!singleApp) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const singleApp = await getAppWithMetadata(app);
|
||||
|
||||
const appDirname = singleApp.type.replace("_", "");
|
||||
if (!singleApp) return { notFound: true };
|
||||
|
||||
const appDirname = app.dirName;
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`);
|
||||
const postFilePath = path.join(README_PATH);
|
||||
let source = "";
|
||||
|
|
|
@ -50,7 +50,7 @@ export default function Apps({ appStore }: InferGetStaticPropsType<typeof getSta
|
|||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const appStore = getAppRegistry();
|
||||
const appStore = await getAppRegistry();
|
||||
const paths = appStore.reduce((categories, app) => {
|
||||
if (!categories.includes(app.category)) {
|
||||
categories.push(app.category);
|
||||
|
@ -67,7 +67,7 @@ export const getStaticPaths = async () => {
|
|||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
appStore: getAppRegistry(),
|
||||
appStore: await getAppRegistry(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const appStore = getAppRegistry();
|
||||
const appStore = await getAppRegistry();
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
||||
return c;
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
|||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const appStore = getAppRegistry();
|
||||
const appStore = await getAppRegistry();
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
||||
return c;
|
||||
|
|
|
@ -136,26 +136,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
};
|
||||
})[0];
|
||||
|
||||
async function getBooking() {
|
||||
return prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: asStringOrThrow(context.query.rescheduleUid),
|
||||
},
|
||||
select: {
|
||||
description: true,
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type Booking = Prisma.PromiseReturnType<typeof getBooking>;
|
||||
let booking: Booking | null = null;
|
||||
|
||||
const profile = {
|
||||
name: user.name || user.username,
|
||||
image: user.avatar,
|
||||
|
@ -173,7 +153,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
locationLabels: getLocationLabels(t),
|
||||
profile,
|
||||
eventType: eventTypeObject,
|
||||
booking,
|
||||
booking: null,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isDynamicGroupBooking: false,
|
||||
hasHashedBookingLink: true,
|
||||
|
|
|
@ -29,7 +29,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
|||
import { JSONObject } from "superjson/dist/types";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SelectGifInput } from "@calcom/app-store/giphyother/components";
|
||||
import { SelectGifInput } from "@calcom/app-store/giphy/components";
|
||||
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
|
@ -2104,6 +2104,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -708,6 +708,7 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { UserPlan } from "@prisma/client";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { UserPlan } from "@calcom/prisma/client";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import getBooking, { GetBookingType } from "@lib/getBooking";
|
||||
|
|
|
@ -55,6 +55,7 @@ async function getUserFromSession({
|
|||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
},
|
||||
orderBy: {
|
||||
id: "asc",
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { expect, it } from "@jest/globals";
|
||||
import { Availability } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import MockDate from "mockdate";
|
||||
|
||||
import { Availability } from "@calcom/prisma/client";
|
||||
|
||||
import { getAvailabilityFromSchedule } from "@lib/availability";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
|
|
@ -1,15 +1,33 @@
|
|||
import prisma from "@calcom/prisma";
|
||||
import { App } from "@calcom/types/App";
|
||||
|
||||
import appStoreMetadata from "./metadata";
|
||||
export async function getAppWithMetadata(app: { dirName: string }) {
|
||||
let appMetadata: App | null = null;
|
||||
try {
|
||||
appMetadata = (await import(`./${app.dirName}/_metadata`)).default as App;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`No metadata found for: "${app.dirName}". Message:`, error.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!appMetadata) return null;
|
||||
// Let's not leak api keys to the front end
|
||||
const { key, ...metadata } = appMetadata;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
|
||||
export function getAppRegistry() {
|
||||
return Object.values(appStoreMetadata).reduce((apps, app) => {
|
||||
export async function getAppRegistry() {
|
||||
const dbApps = await prisma.app.findMany({ select: { dirName: true, slug: true, categories: true } });
|
||||
const apps = [] as Omit<App, "key">[];
|
||||
for await (const dbapp of dbApps) {
|
||||
const app = await getAppWithMetadata(dbapp);
|
||||
if (!app) continue;
|
||||
// Skip if app isn't installed
|
||||
if (!app.installed) return apps;
|
||||
// Let's not leak api keys to the front end
|
||||
const { key, ...metadata } = app;
|
||||
apps.push(metadata);
|
||||
return apps;
|
||||
}, [] as Omit<App, "key">[]);
|
||||
/* This is now handled from the DB */
|
||||
// if (!app.installed) return apps;
|
||||
apps.push(app);
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
|
10
packages/app-store/_utils/getAppKeysFromSlug.ts
Normal file
10
packages/app-store/_utils/getAppKeysFromSlug.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
async function getAppKeysFromSlug(slug: string) {
|
||||
const app = await prisma.app.findUnique({ where: { slug } });
|
||||
return app?.keys as Prisma.JsonObject;
|
||||
}
|
||||
|
||||
export default getAppKeysFromSlug;
|
|
@ -5,7 +5,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { App } from "@calcom/types/App";
|
||||
|
||||
function useAddAppMutation(type: App["type"], options?: Parameters<typeof useMutation>[2]) {
|
||||
const appName = type.replaceAll("_", "");
|
||||
const appName = type.replace(/_/g, "");
|
||||
const mutation = useMutation(async () => {
|
||||
const state: IntegrationOAuthCallbackState = {
|
||||
returnTo: WEBAPP_URL + "/apps/installed" + location.search,
|
||||
|
|
19
packages/app-store/apiHandlers.tsx
Normal file
19
packages/app-store/apiHandlers.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
export const apiHandlers = {
|
||||
// examplevideo: import("./_example/api"),
|
||||
applecalendar: import("./applecalendar/api"),
|
||||
caldavcalendar: import("./caldavcalendar/api"),
|
||||
googlecalendar: import("./googlecalendar/api"),
|
||||
hubspotothercalendar: import("./hubspotothercalendar/api"),
|
||||
office365calendar: import("./office365calendar/api"),
|
||||
slackmessaging: import("./slackmessaging/api"),
|
||||
stripepayment: import("./stripepayment/api"),
|
||||
tandemvideo: import("./tandemvideo/api"),
|
||||
zoomvideo: import("@calcom/zoomvideo/api"),
|
||||
office365video: import("@calcom/office365video/api"),
|
||||
wipemycalother: import("./wipemycalother/api"),
|
||||
jitsivideo: import("./jitsivideo/api"),
|
||||
huddle01video: import("./huddle01video/api"),
|
||||
giphy: import("./giphy/api"),
|
||||
};
|
||||
|
||||
export default apiHandlers;
|
|
@ -24,6 +24,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
type: "apple_calendar",
|
||||
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY!),
|
||||
userId: user.id,
|
||||
appId: "apple-calendar",
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
process.env.CALENDSO_ENCRYPTION_KEY!
|
||||
),
|
||||
userId: user.id,
|
||||
appId: "caldav-calendar",
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
|
@ -23,7 +23,7 @@ export const InstallAppButtonMap = {
|
|||
wipemycalother: dynamic(() => import("./wipemycalother/components/InstallAppButton")),
|
||||
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
|
||||
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
|
||||
giphyother: dynamic(() => import("./giphyother/components/InstallAppButton")),
|
||||
giphy: dynamic(() => import("./giphy/components/InstallAppButton")),
|
||||
};
|
||||
|
||||
export const InstallAppButton = (
|
||||
|
@ -33,8 +33,14 @@ export const InstallAppButton = (
|
|||
) => {
|
||||
const { status } = useSession();
|
||||
const { t } = useLocale();
|
||||
const appName = props.type.replaceAll("_", "") as keyof typeof InstallAppButtonMap;
|
||||
const InstallAppButtonComponent = InstallAppButtonMap[appName];
|
||||
let appName = props.type.replace(/_/g, "");
|
||||
let InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
|
||||
/** So we can either call it by simple name (ex. `slack`, `giphy`) instead of
|
||||
* `slackmessaging`, `giphyother` while maintaining retro-compatibility. */
|
||||
if (!InstallAppButtonComponent) {
|
||||
[appName] = props.type.split("_");
|
||||
InstallAppButtonComponent = InstallAppButtonMap[appName as keyof typeof InstallAppButtonMap];
|
||||
}
|
||||
if (!InstallAppButtonComponent) return null;
|
||||
if (status === "unauthenticated")
|
||||
return (
|
||||
|
|
|
@ -57,6 +57,7 @@ export const FAKE_DAILY_CREDENTIAL: Credential = {
|
|||
type: "daily_video",
|
||||
key: { apikey: process.env.DAILY_API_KEY },
|
||||
userId: +new Date().getTime(),
|
||||
appId: "daily-video",
|
||||
};
|
||||
|
||||
const DailyVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
|
||||
|
|
|
@ -8,8 +8,8 @@ export const metadata = {
|
|||
installed: !!process.env.GIPHY_API_KEY,
|
||||
category: "other",
|
||||
// If using static next public folder, can then be referenced from the base URL (/).
|
||||
imageSrc: "/api/app-store/giphyother/icon.svg",
|
||||
logo: "/api/app-store/giphyother/icon.svg",
|
||||
imageSrc: "/api/app-store/giphy/icon.svg",
|
||||
logo: "/api/app-store/giphy/icon.svg",
|
||||
publisher: "Cal.com",
|
||||
rating: 0,
|
||||
reviews: 0,
|
|
@ -2,7 +2,8 @@ import useAddAppMutation from "../../_utils/useAddAppMutation";
|
|||
import { InstallAppButtonProps } from "../../types";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("giphy_other");
|
||||
// @ts-ignore TODO: deprecate App types in favor of DB slugs
|
||||
const mutation = useAddAppMutation("giphy");
|
||||
|
||||
return (
|
||||
<>
|
|
@ -30,7 +30,7 @@ export const SearchDialog = (props: ISearchDialog) => {
|
|||
}
|
||||
setIsLoading(true);
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/giphyother/search", {
|
||||
const res = await fetch("/api/integrations/giphy/search", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
|
@ -1,6 +1,15 @@
|
|||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
let api_key = "";
|
||||
|
||||
export const searchGiphy = async (locale: string, keyword: string, offset: number = 0) => {
|
||||
const appKeys = await getAppKeysFromSlug("giphy");
|
||||
if (typeof appKeys.api_key === "string") api_key = appKeys.api_key;
|
||||
if (!api_key) throw new HttpError({ statusCode: 400, message: "Missing Giphy api_key" });
|
||||
const queryParams = new URLSearchParams({
|
||||
api_key: String(process.env.GIPHY_API_KEY),
|
||||
api_key,
|
||||
q: keyword,
|
||||
limit: "1",
|
||||
offset: String(offset),
|
Before Width: | Height: | Size: 734 B After Width: | Height: | Size: 734 B |
|
@ -4,17 +4,24 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { encodeOAuthState } from "../../_utils/encodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
const credentials = process.env.GOOGLE_API_CREDENTIALS!;
|
||||
const scopes = [
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
"https://www.googleapis.com/auth/calendar.events",
|
||||
];
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "GET") {
|
||||
// Get token from Google Calendar API
|
||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||
const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback";
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@ import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
|||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
const credentials = process.env.GOOGLE_API_CREDENTIALS;
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { code } = req.query;
|
||||
|
@ -15,11 +17,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
res.status(400).json({ message: "`code` must be a string" });
|
||||
return;
|
||||
}
|
||||
if (!credentials) {
|
||||
res.status(400).json({ message: "There are no Google Credentials installed." });
|
||||
return;
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
const { client_secret, client_id } = JSON.parse(credentials).web;
|
||||
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||
|
||||
const redirect_uri = WEBAPP_URL + "/api/integrations/googlecalendar/callback";
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
@ -36,7 +43,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
data: {
|
||||
type: "google_calendar",
|
||||
key,
|
||||
userId: req.session?.user.id,
|
||||
userId: req.session.user.id,
|
||||
appId: "google-calendar",
|
||||
},
|
||||
});
|
||||
const state = decodeOAuthState(req);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Auth, calendar_v3, google } from "googleapis";
|
|||
|
||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import CalendarService from "@calcom/lib/CalendarService";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type {
|
||||
|
@ -14,26 +15,38 @@ import type {
|
|||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
|
||||
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
export default class GoogleCalendarService implements Calendar {
|
||||
private url = "";
|
||||
private integrationName = "";
|
||||
private auth: { getToken: () => Promise<MyGoogleAuth> };
|
||||
private auth: Promise<{ getToken: () => Promise<MyGoogleAuth> }>;
|
||||
private log: typeof logger;
|
||||
private client_id = "";
|
||||
private client_secret = "";
|
||||
private redirect_uri = "";
|
||||
|
||||
constructor(credential: Credential) {
|
||||
this.integrationName = "google_calendar";
|
||||
|
||||
this.auth = this.googleAuth(credential);
|
||||
this.auth = this.googleAuth(credential).then((m) => m);
|
||||
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
}
|
||||
|
||||
private googleAuth = (credential: Credential) => {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(GOOGLE_API_CREDENTIALS).web;
|
||||
private googleAuth = async (credential: Credential) => {
|
||||
const appKeys = await getAppKeysFromSlug("google-calendar");
|
||||
if (typeof appKeys.client_id === "string") this.client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") this.client_secret = appKeys.client_secret;
|
||||
if (typeof appKeys.redirect_uris === "object" && Array.isArray(appKeys.redirect_uris)) {
|
||||
this.redirect_uri = appKeys.redirect_uris[0] as string;
|
||||
}
|
||||
if (!this.client_id) throw new HttpError({ statusCode: 400, message: "Google client_id missing." });
|
||||
if (!this.client_secret)
|
||||
throw new HttpError({ statusCode: 400, message: "Google client_secret missing." });
|
||||
if (!this.redirect_uri) throw new HttpError({ statusCode: 400, message: "Google redirect_uri missing." });
|
||||
|
||||
const myGoogleAuth = new MyGoogleAuth(client_id, client_secret, redirect_uris[0]);
|
||||
const myGoogleAuth = new MyGoogleAuth(this.client_id, this.client_secret, this.redirect_uri);
|
||||
|
||||
const googleCredentials = credential.key as Auth.Credentials;
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
|
@ -43,23 +56,20 @@ export default class GoogleCalendarService implements Calendar {
|
|||
const refreshAccessToken = () =>
|
||||
myGoogleAuth
|
||||
.refreshToken(googleCredentials.refresh_token)
|
||||
.then((res: GetTokenResponse) => {
|
||||
.then(async (res: GetTokenResponse) => {
|
||||
const token = res.res?.data;
|
||||
googleCredentials.access_token = token.access_token;
|
||||
googleCredentials.expiry_date = token.expiry_date;
|
||||
return prisma.credential
|
||||
.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: googleCredentials as Prisma.InputJsonValue,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
return myGoogleAuth;
|
||||
});
|
||||
await prisma.credential.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: googleCredentials as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
myGoogleAuth.setCredentials(googleCredentials);
|
||||
return myGoogleAuth;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("Error refreshing google token", err);
|
||||
|
@ -73,164 +83,164 @@ export default class GoogleCalendarService implements Calendar {
|
|||
};
|
||||
|
||||
async createEvent(calEventRaw: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: calEventRaw.title,
|
||||
description: getRichDescription(calEventRaw),
|
||||
start: {
|
||||
dateTime: calEventRaw.startTime,
|
||||
timeZone: calEventRaw.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: calEventRaw.endTime,
|
||||
timeZone: calEventRaw.organizer.timeZone,
|
||||
},
|
||||
attendees: calEventRaw.attendees.map((attendee) => ({
|
||||
...attendee,
|
||||
responseStatus: "accepted",
|
||||
})),
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const auth = await this.auth;
|
||||
const myGoogleAuth = await auth.getToken();
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: calEventRaw.title,
|
||||
description: getRichDescription(calEventRaw),
|
||||
start: {
|
||||
dateTime: calEventRaw.startTime,
|
||||
timeZone: calEventRaw.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: calEventRaw.endTime,
|
||||
timeZone: calEventRaw.organizer.timeZone,
|
||||
},
|
||||
attendees: calEventRaw.attendees.map((attendee) => ({
|
||||
...attendee,
|
||||
responseStatus: "accepted",
|
||||
})),
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (calEventRaw.location) {
|
||||
payload["location"] = getLocation(calEventRaw);
|
||||
}
|
||||
if (calEventRaw.location) {
|
||||
payload["location"] = getLocation(calEventRaw);
|
||||
}
|
||||
|
||||
if (calEventRaw.conferenceData && calEventRaw.location === "integrations:google:meet") {
|
||||
payload["conferenceData"] = calEventRaw.conferenceData;
|
||||
}
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
});
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
if (calEventRaw.conferenceData && calEventRaw.location === "integrations:google:meet") {
|
||||
payload["conferenceData"] = calEventRaw.conferenceData;
|
||||
}
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
});
|
||||
calendar.events.insert(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: calEventRaw.destinationCalendar?.externalId
|
||||
? calEventRaw.destinationCalendar.externalId
|
||||
: "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err || !event?.data) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
calendar.events.patch({
|
||||
// Update the same event but this time we know the hangout link
|
||||
calendarId: calEventRaw.destinationCalendar?.externalId
|
||||
? calEventRaw.destinationCalendar.externalId
|
||||
: "primary",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err || !event?.data) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
auth: myGoogleAuth,
|
||||
eventId: event.data.id || "",
|
||||
requestBody: {
|
||||
description: getRichDescription({
|
||||
...calEventRaw,
|
||||
additionInformation: { hangoutLink: event.data.hangoutLink || "" },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
calendar.events.patch({
|
||||
// Update the same event but this time we know the hangout link
|
||||
calendarId: calEventRaw.destinationCalendar?.externalId
|
||||
? calEventRaw.destinationCalendar.externalId
|
||||
: "primary",
|
||||
auth: myGoogleAuth,
|
||||
eventId: event.data.id || "",
|
||||
requestBody: {
|
||||
description: getRichDescription({
|
||||
...calEventRaw,
|
||||
additionInformation: { hangoutLink: event.data.hangoutLink || "" },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return resolve({
|
||||
uid: "",
|
||||
...event.data,
|
||||
id: event.data.id || "",
|
||||
additionalInfo: {
|
||||
hangoutLink: event.data.hangoutLink || "",
|
||||
},
|
||||
type: "google_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
return resolve({
|
||||
uid: "",
|
||||
...event.data,
|
||||
id: event.data.id || "",
|
||||
additionalInfo: {
|
||||
hangoutLink: event.data.hangoutLink || "",
|
||||
},
|
||||
type: "google_calendar",
|
||||
password: "",
|
||||
url: "",
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const auth = await this.auth;
|
||||
const myGoogleAuth = await auth.getToken();
|
||||
const payload: calendar_v3.Schema$Event = {
|
||||
summary: event.title,
|
||||
description: getRichDescription(event),
|
||||
start: {
|
||||
dateTime: event.startTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
end: {
|
||||
dateTime: event.endTime,
|
||||
timeZone: event.organizer.timeZone,
|
||||
},
|
||||
attendees: event.attendees,
|
||||
reminders: {
|
||||
useDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
if (event.location) {
|
||||
payload["location"] = getLocation(event);
|
||||
}
|
||||
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.update(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.update(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
requestBody: payload,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
requestBody: payload,
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
return reject(err);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string, event: CalendarEvent): Promise<void> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const auth = await this.auth;
|
||||
const myGoogleAuth = await auth.getToken();
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.delete(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
calendar.events.delete(
|
||||
{
|
||||
auth: myGoogleAuth,
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(event?.data);
|
||||
calendarId: event.destinationCalendar?.externalId
|
||||
? event.destinationCalendar.externalId
|
||||
: "primary",
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
},
|
||||
function (err, event) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
return reject(err);
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
return resolve(event?.data);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
|
@ -238,96 +248,96 @@ export default class GoogleCalendarService implements Calendar {
|
|||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const auth = await this.auth;
|
||||
const myGoogleAuth = await auth.getToken();
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
const selectedCalendarIds = selectedCalendars
|
||||
.filter((e) => e.integration === this.integrationName)
|
||||
.map((e) => e.externalId);
|
||||
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
|
||||
// Only calendars of other integrations selected
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
(selectedCalendarIds.length === 0
|
||||
? calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
)
|
||||
.then((calsIds) => {
|
||||
calendar.freebusy.query(
|
||||
{
|
||||
requestBody: {
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id: id })),
|
||||
},
|
||||
(selectedCalendarIds.length === 0
|
||||
? calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => cals.data.items?.map((cal) => cal.id).filter(Boolean) || [])
|
||||
: Promise.resolve(selectedCalendarIds)
|
||||
)
|
||||
.then((calsIds) => {
|
||||
calendar.freebusy.query(
|
||||
{
|
||||
requestBody: {
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id: id })),
|
||||
},
|
||||
(err, apires) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
let result: Prisma.PromiseReturnType<CalendarService["getAvailability"]> = [];
|
||||
|
||||
if (apires?.data.calendars) {
|
||||
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
||||
i.busy?.forEach((busyTime) => {
|
||||
c.push({
|
||||
start: busyTime.start || "",
|
||||
end: busyTime.end || "",
|
||||
});
|
||||
});
|
||||
return c;
|
||||
}, [] as typeof result);
|
||||
}
|
||||
resolve(result);
|
||||
},
|
||||
(err, apires) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
let result: Prisma.PromiseReturnType<CalendarService["getAvailability"]> = [];
|
||||
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
);
|
||||
if (apires?.data.calendars) {
|
||||
result = Object.values(apires.data.calendars).reduce((c, i) => {
|
||||
i.busy?.forEach((busyTime) => {
|
||||
c.push({
|
||||
start: busyTime.start || "",
|
||||
end: busyTime.end || "",
|
||||
});
|
||||
});
|
||||
return c;
|
||||
}, [] as typeof result);
|
||||
}
|
||||
resolve(result);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async listCalendars(): Promise<IntegrationCalendar[]> {
|
||||
return new Promise((resolve, reject) =>
|
||||
this.auth.getToken().then((myGoogleAuth) => {
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const auth = await this.auth;
|
||||
const myGoogleAuth = await auth.getToken();
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
auth: myGoogleAuth,
|
||||
});
|
||||
|
||||
calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => {
|
||||
resolve(
|
||||
cals.data.items?.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No id",
|
||||
integration: this.integrationName,
|
||||
name: cal.summary ?? "No name",
|
||||
primary: cal.primary ?? false,
|
||||
};
|
||||
return calendar;
|
||||
}) || []
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
|
||||
calendar.calendarList
|
||||
.list()
|
||||
.then((cals) => {
|
||||
resolve(
|
||||
cals.data.items?.map((cal) => {
|
||||
const calendar: IntegrationCalendar = {
|
||||
externalId: cal.id ?? "No id",
|
||||
integration: this.integrationName,
|
||||
name: cal.summary ?? "No name",
|
||||
primary: cal.primary ?? false,
|
||||
};
|
||||
return calendar;
|
||||
}) || []
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.log.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
reject(err);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,10 @@ import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
|||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
const client_id = process.env.HUBSPOT_CLIENT_ID;
|
||||
const client_secret = process.env.HUBSPOT_CLIENT_SECRET;
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
const hubspotClient = new hubspot.Client();
|
||||
|
||||
export type HubspotToken = TokenResponseIF & {
|
||||
|
@ -24,15 +25,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return;
|
||||
}
|
||||
|
||||
if (!client_id) {
|
||||
res.status(400).json({ message: "HubSpot client id missing." });
|
||||
return;
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
if (!client_secret) {
|
||||
res.status(400).json({ message: "HubSpot client secret missing." });
|
||||
return;
|
||||
}
|
||||
const appKeys = await getAppKeysFromSlug("hubspot");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "HubSpot client secret missing." });
|
||||
|
||||
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
|
||||
"authorization_code",
|
||||
|
@ -48,7 +49,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
data: {
|
||||
type: "hubspot_other_calendar",
|
||||
key: hubspotToken as any,
|
||||
userId: req.session?.user.id,
|
||||
userId: req.session.user.id,
|
||||
appId: "hubspot",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
type: appType,
|
||||
key: {},
|
||||
userId: req.session.user.id,
|
||||
appId: "huddle01",
|
||||
},
|
||||
});
|
||||
if (!installation) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import * as applecalendar from "./applecalendar";
|
||||
import * as caldavcalendar from "./caldavcalendar";
|
||||
import * as dailyvideo from "./dailyvideo";
|
||||
import * as giphyother from "./giphyother";
|
||||
import * as giphy from "./giphy";
|
||||
import * as googlecalendar from "./googlecalendar";
|
||||
import * as googlevideo from "./googlevideo";
|
||||
import * as hubspotothercalendar from "./hubspotothercalendar";
|
||||
|
@ -33,7 +33,7 @@ const appStore = {
|
|||
tandemvideo,
|
||||
zoomvideo,
|
||||
wipemycalother,
|
||||
giphyother,
|
||||
giphy,
|
||||
};
|
||||
|
||||
export default appStore;
|
||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
type: appType,
|
||||
key: {},
|
||||
userId: req.session.user.id,
|
||||
appId: "jitsi",
|
||||
},
|
||||
});
|
||||
if (!installation) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { metadata as applecalendar } from "./applecalendar/_metadata";
|
||||
import { metadata as caldavcalendar } from "./caldavcalendar/_metadata";
|
||||
import { metadata as dailyvideo } from "./dailyvideo/_metadata";
|
||||
import { metadata as giphy } from "./giphyother/_metadata";
|
||||
import { metadata as giphy } from "./giphy/_metadata";
|
||||
import { metadata as googlecalendar } from "./googlecalendar/_metadata";
|
||||
import { metadata as googlevideo } from "./googlevideo/_metadata";
|
||||
import { metadata as hubspotothercalendar } from "./hubspotothercalendar/_metadata";
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { BASE_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
const scopes = ["offline_access", "Calendars.Read", "Calendars.ReadWrite"];
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { code } = req.query;
|
||||
|
||||
|
@ -16,18 +20,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return;
|
||||
}
|
||||
|
||||
const appKeys = await getAppKeysFromSlug("office365-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Office 365 client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Office 365 client_secret missing." });
|
||||
|
||||
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||
Object.keys(payload)
|
||||
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
||||
.join("&");
|
||||
|
||||
const body = toUrlEncoded({
|
||||
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
||||
client_id,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
scope: scopes.join(" "),
|
||||
redirect_uri: BASE_URL + "/api/integrations/office365calendar/callback",
|
||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
|
||||
redirect_uri: WEBAPP_URL + "/api/integrations/office365calendar/callback",
|
||||
client_secret,
|
||||
});
|
||||
|
||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
|
@ -59,6 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
type: "office365_calendar",
|
||||
key: responseBody,
|
||||
userId: req.session?.user.id,
|
||||
appId: "office365-calendar",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { BASE_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
const scopes = ["OnlineMeetings.ReadWrite"];
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { code } = req.query;
|
||||
console.log("🚀 ~ file: callback.ts ~ line 14 ~ handler ~ code", req.query);
|
||||
|
@ -17,18 +21,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return;
|
||||
}
|
||||
|
||||
const appKeys = await getAppKeysFromSlug("office365-calendar");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||
if (!client_id) return res.status(400).json({ message: "Office 365 client_id missing." });
|
||||
if (!client_secret) return res.status(400).json({ message: "Office 365 client_secret missing." });
|
||||
|
||||
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||
Object.keys(payload)
|
||||
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
||||
.join("&");
|
||||
|
||||
const body = toUrlEncoded({
|
||||
client_id: process.env.MS_GRAPH_CLIENT_ID!,
|
||||
client_id,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
scope: scopes.join(" "),
|
||||
redirect_uri: BASE_URL + "/api/integrations/office365video/callback",
|
||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET!,
|
||||
redirect_uri: WEBAPP_URL + "/api/integrations/office365video/callback",
|
||||
client_secret,
|
||||
});
|
||||
|
||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
|
@ -60,6 +70,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
type: "office365_video",
|
||||
key: responseBody,
|
||||
userId: req.session?.user.id,
|
||||
appId: "msteams",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ import { stringify } from "querystring";
|
|||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const client_id = process.env.SLACK_CLIENT_ID;
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
let client_id = "";
|
||||
const scopes = ["commands", "users:read", "users:read.email", "chat:write", "chat:write.public"];
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -12,6 +14,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const appKeys = await getAppKeysFromSlug("slack");
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (!client_id) return res.status(400).json({ message: "Slack client_id missing" });
|
||||
// Get user
|
||||
await prisma.user.findFirst({
|
||||
rejectOnNotFound: true,
|
||||
|
|
|
@ -14,6 +14,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return;
|
||||
}
|
||||
|
||||
if (!req.session?.user?.id) {
|
||||
return res.status(401).json({ message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const response = await stripe.oauth.token({
|
||||
grant_type: "authorization_code",
|
||||
code: code.toString(),
|
||||
|
@ -29,7 +33,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
data: {
|
||||
type: "stripe_payment",
|
||||
key: data as unknown as Prisma.InputJsonObject,
|
||||
userId: req.session?.user.id,
|
||||
userId: req.session.user.id,
|
||||
appId: "stripe",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
|
|||
}, {} as Record<string, App>);
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true, key: true, userId: true },
|
||||
select: { id: true, type: true, key: true, userId: true, appId: true },
|
||||
});
|
||||
|
||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||
|
@ -66,6 +66,7 @@ function getApps(userCredentials: CredentialData[]) {
|
|||
type: appMeta.type,
|
||||
key: appMeta.key!,
|
||||
userId: +new Date().getTime(),
|
||||
appId: appMeta.slug,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
type: appType,
|
||||
key: {},
|
||||
userId: req.session.user.id,
|
||||
appId: "wipe-my-cal",
|
||||
},
|
||||
});
|
||||
if (!installation) {
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
import { getCalendar } from "../../_utils/getCalendar";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||
|
||||
/** TODO: Remove once all references are updated to app-store */
|
||||
export { getCalendar };
|
||||
|
||||
export const createEvent = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
const calendar = getCalendar(credential);
|
||||
let success = true;
|
||||
|
||||
// Check if the disabledNotes flag is set to true
|
||||
if (calEvent.hideCalendarNotes) {
|
||||
calEvent.additionalNotes = "Notes have been hidden by the organizer"; // TODO: i18n this string?
|
||||
}
|
||||
|
||||
const creationResult = calendar
|
||||
? await calendar.createEvent(calEvent).catch((e) => {
|
||||
log.error("createEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
createdEvent: creationResult,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateEvent = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRefUid: string | null
|
||||
): Promise<EventResult> => {
|
||||
const uid = getUid(calEvent);
|
||||
const calendar = getCalendar(credential);
|
||||
let success = true;
|
||||
|
||||
const updatedResult =
|
||||
calendar && bookingRefUid
|
||||
? await calendar.updateEvent(bookingRefUid, calEvent).catch((e) => {
|
||||
log.error("updateEvent failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
updatedEvent: updatedResult,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
export const deleteEvent = (credential: Credential, uid: string, event: CalendarEvent): Promise<unknown> => {
|
||||
const calendar = getCalendar(credential);
|
||||
if (calendar) {
|
||||
return calendar.deleteEvent(uid, event);
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
};
|
|
@ -1,386 +0,0 @@
|
|||
import { Credential, DestinationCalendar } from "@prisma/client";
|
||||
import async from "async";
|
||||
import merge from "lodash/merge";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AdditionInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type {
|
||||
CreateUpdateResult,
|
||||
EventResult,
|
||||
PartialBooking,
|
||||
PartialReference,
|
||||
} from "@calcom/types/EventManager";
|
||||
import type { VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import { LocationType } from "../../locations";
|
||||
import getApps from "../../utils";
|
||||
import { createEvent, updateEvent } from "./calendarManager";
|
||||
import { createMeeting, updateMeeting } from "./videoClient";
|
||||
|
||||
export type Event = AdditionInformation & VideoCallData;
|
||||
|
||||
export const isZoom = (location: string): boolean => {
|
||||
return location === "integrations:zoom";
|
||||
};
|
||||
|
||||
export const isDaily = (location: string): boolean => {
|
||||
return location === "integrations:daily";
|
||||
};
|
||||
|
||||
export const isHuddle01 = (location: string): boolean => {
|
||||
return location === "integrations:huddle01";
|
||||
};
|
||||
|
||||
export const isTandem = (location: string): boolean => {
|
||||
return location === "integrations:tandem";
|
||||
};
|
||||
|
||||
export const isTeams = (location: string): boolean => {
|
||||
return location === "integrations:office365_video";
|
||||
};
|
||||
|
||||
export const isJitsi = (location: string): boolean => {
|
||||
return location === "integrations:jitsi";
|
||||
};
|
||||
|
||||
export const isDedicatedIntegration = (location: string): boolean => {
|
||||
return (
|
||||
isZoom(location) ||
|
||||
isDaily(location) ||
|
||||
isHuddle01(location) ||
|
||||
isTandem(location) ||
|
||||
isJitsi(location) ||
|
||||
isTeams(location)
|
||||
);
|
||||
};
|
||||
|
||||
export const getLocationRequestFromIntegration = (location: string) => {
|
||||
if (
|
||||
/** TODO: Handle this dynamically */
|
||||
location === LocationType.GoogleMeet.valueOf() ||
|
||||
location === LocationType.Zoom.valueOf() ||
|
||||
location === LocationType.Daily.valueOf() ||
|
||||
location === LocationType.Jitsi.valueOf() ||
|
||||
location === LocationType.Huddle01.valueOf() ||
|
||||
location === LocationType.Tandem.valueOf() ||
|
||||
location === LocationType.Teams.valueOf()
|
||||
) {
|
||||
const requestId = uuidv5(location, uuidv5.URL);
|
||||
|
||||
return {
|
||||
conferenceData: {
|
||||
createRequest: {
|
||||
requestId: requestId,
|
||||
},
|
||||
},
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const processLocation = (event: CalendarEvent): CalendarEvent => {
|
||||
// If location is set to an integration location
|
||||
// Build proper transforms for evt object
|
||||
// Extend evt object with those transformations
|
||||
if (event.location?.includes("integration")) {
|
||||
const maybeLocationRequestObject = getLocationRequestFromIntegration(event.location);
|
||||
|
||||
event = merge(event, maybeLocationRequestObject);
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
type EventManagerUser = {
|
||||
credentials: Credential[];
|
||||
destinationCalendar: DestinationCalendar | null;
|
||||
};
|
||||
|
||||
export default class EventManager {
|
||||
calendarCredentials: Credential[];
|
||||
videoCredentials: Credential[];
|
||||
|
||||
/**
|
||||
* Takes an array of credentials and initializes a new instance of the EventManager.
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
constructor(user: EventManagerUser) {
|
||||
const appCredentials = getApps(user.credentials).flatMap((app) => app.credentials);
|
||||
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
this.videoCredentials = appCredentials.filter((cred) => cred.type.endsWith("_video"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a CalendarEvent and creates all necessary integration entries for it.
|
||||
* When a video integration is chosen as the event's location, a video integration
|
||||
* event will be scheduled for it as well.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
public async create(event: CalendarEvent): Promise<CreateUpdateResult> {
|
||||
const evt = processLocation(event);
|
||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||
|
||||
const results: Array<EventResult> = [];
|
||||
// If and only if event type is a dedicated meeting, create a dedicated video meeting.
|
||||
if (isDedicated) {
|
||||
const result = await this.createVideoEvent(evt);
|
||||
if (result.createdEvent) {
|
||||
evt.videoCallData = result.createdEvent;
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Create the calendar event with the proper video call data
|
||||
results.push(...(await this.createAllCalendarEvents(evt)));
|
||||
|
||||
const referencesToCreate: Array<PartialReference> = results.map((result: EventResult) => {
|
||||
return {
|
||||
type: result.type,
|
||||
uid: result.createdEvent?.id.toString() ?? "",
|
||||
meetingId: result.createdEvent?.id.toString(),
|
||||
meetingPassword: result.createdEvent?.password,
|
||||
meetingUrl: result.createdEvent?.url,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
results,
|
||||
referencesToCreate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a calendarEvent and a rescheduleUid and updates the event that has the
|
||||
* given uid using the data delivered in the given CalendarEvent.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
public async update(
|
||||
event: CalendarEvent,
|
||||
rescheduleUid: string,
|
||||
newBookingId?: number
|
||||
): Promise<CreateUpdateResult> {
|
||||
const evt = processLocation(event);
|
||||
|
||||
if (!rescheduleUid) {
|
||||
throw new Error("You called eventManager.update without an `rescheduleUid`. This should never happen.");
|
||||
}
|
||||
|
||||
// Get details of existing booking.
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: rescheduleUid,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
references: {
|
||||
// NOTE: id field removed from select as we don't require for deletingMany
|
||||
// but was giving error on recreate for reschedule, probably because promise.all() didn't finished
|
||||
select: {
|
||||
type: true,
|
||||
uid: true,
|
||||
meetingId: true,
|
||||
meetingPassword: true,
|
||||
meetingUrl: true,
|
||||
},
|
||||
},
|
||||
destinationCalendar: true,
|
||||
payment: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new Error("booking not found");
|
||||
}
|
||||
|
||||
const isDedicated = evt.location ? isDedicatedIntegration(evt.location) : null;
|
||||
const results: Array<EventResult> = [];
|
||||
// If and only if event type is a dedicated meeting, update the dedicated video meeting.
|
||||
if (isDedicated) {
|
||||
const result = await this.updateVideoEvent(evt, booking);
|
||||
const [updatedEvent] = Array.isArray(result.updatedEvent) ? result.updatedEvent : [result.updatedEvent];
|
||||
if (updatedEvent) {
|
||||
evt.videoCallData = updatedEvent;
|
||||
evt.location = updatedEvent.url;
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
// Update all calendar events.
|
||||
results.push(...(await this.updateAllCalendarEvents(evt, booking)));
|
||||
|
||||
const bookingPayment = booking?.payment;
|
||||
|
||||
// Updating all payment to new
|
||||
if (bookingPayment && newBookingId) {
|
||||
const paymentIds = bookingPayment.map((payment) => payment.id);
|
||||
await prisma.payment.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: paymentIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
bookingId: newBookingId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Now we can delete the old booking and its references.
|
||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
const attendeeDeletes = prisma.attendee.deleteMany({
|
||||
where: {
|
||||
bookingId: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
const bookingDeletes = prisma.booking.delete({
|
||||
where: {
|
||||
id: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Wait for all deletions to be applied.
|
||||
await Promise.all([bookingReferenceDeletes, attendeeDeletes, bookingDeletes]);
|
||||
|
||||
return {
|
||||
results,
|
||||
referencesToCreate: [...booking.references],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates event entries for all calendar integrations given in the credentials.
|
||||
* When noMail is true, no mails will be sent. This is used when the event is
|
||||
* a video meeting because then the mail containing the video credentials will be
|
||||
* more important than the mails created for these bare calendar events.
|
||||
*
|
||||
* When the optional uid is set, it will be used instead of the auto generated uid.
|
||||
*
|
||||
* @param event
|
||||
* @param noMail
|
||||
* @private
|
||||
*/
|
||||
private async createAllCalendarEvents(event: CalendarEvent): Promise<Array<EventResult>> {
|
||||
/** Can I use destinationCalendar here? */
|
||||
/* How can I link a DC to a cred? */
|
||||
if (event.destinationCalendar) {
|
||||
const destinationCalendarCredentials = this.calendarCredentials.filter(
|
||||
(c) => c.type === event.destinationCalendar?.integration
|
||||
);
|
||||
return Promise.all(destinationCalendarCredentials.map(async (c) => await createEvent(c, event)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Not ideal but, if we don't find a destination calendar,
|
||||
* fallback to the first connected calendar
|
||||
*/
|
||||
const [credential] = this.calendarCredentials;
|
||||
if (!credential) {
|
||||
return [];
|
||||
}
|
||||
return [await createEvent(credential, event)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks which video integration is needed for the event's location and returns
|
||||
* credentials for that - if existing.
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
|
||||
private getVideoCredential(event: CalendarEvent): Credential | undefined {
|
||||
if (!event.location) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const integrationName = event.location.replace("integrations:", "");
|
||||
|
||||
return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a video event entry for the selected integration location.
|
||||
*
|
||||
* When optional uid is set, it will be used instead of the auto generated uid.
|
||||
*
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
private createVideoEvent(event: CalendarEvent): Promise<EventResult> {
|
||||
const credential = this.getVideoCredential(event);
|
||||
|
||||
if (credential) {
|
||||
return createMeeting(credential, event);
|
||||
} else {
|
||||
return Promise.reject(
|
||||
`No suitable credentials given for the requested integration name:${event.location}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the event entries for all calendar integrations given in the credentials.
|
||||
* When noMail is true, no mails will be sent. This is used when the event is
|
||||
* a video meeting because then the mail containing the video credentials will be
|
||||
* more important than the mails created for these bare calendar events.
|
||||
*
|
||||
* @param event
|
||||
* @param booking
|
||||
* @private
|
||||
*/
|
||||
private updateAllCalendarEvents(
|
||||
event: CalendarEvent,
|
||||
booking: PartialBooking
|
||||
): Promise<Array<EventResult>> {
|
||||
return async.mapLimit(this.calendarCredentials, 5, async (credential: Credential) => {
|
||||
const bookingRefUid = booking
|
||||
? booking.references.filter((ref) => ref.type === credential.type)[0]?.uid
|
||||
: null;
|
||||
|
||||
return updateEvent(credential, event, bookingRefUid);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single video event.
|
||||
*
|
||||
* @param event
|
||||
* @param booking
|
||||
* @private
|
||||
*/
|
||||
private updateVideoEvent(event: CalendarEvent, booking: PartialBooking) {
|
||||
const credential = this.getVideoCredential(event);
|
||||
|
||||
if (credential) {
|
||||
const bookingRef = booking ? booking.references.filter((ref) => ref.type === credential.type)[0] : null;
|
||||
return updateMeeting(credential, event, bookingRef);
|
||||
} else {
|
||||
return Promise.reject(
|
||||
`No suitable credentials given for the requested integration name:${event.location}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update event to set a cancelled event placeholder on users calendar
|
||||
* remove if virtual calendar is already done and user availability its read from there
|
||||
* and not only in their calendars
|
||||
* @param event
|
||||
* @param booking
|
||||
* @public
|
||||
*/
|
||||
public async updateAndSetCancelledPlaceholder(event: CalendarEvent, booking: PartialBooking) {
|
||||
await this.updateAllCalendarEvents(event, booking);
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import { BookingStatus, User, Booking, BookingReference } from "@prisma/client";
|
||||
import { Booking, BookingReference, BookingStatus, User } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import type { TFunction } from "next-i18next";
|
||||
|
||||
import EventManager from "@calcom/core/EventManager";
|
||||
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
||||
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
@ -11,8 +13,6 @@ import { Person } from "@calcom/types/Calendar";
|
|||
|
||||
import { getCalendar } from "../../_utils/getCalendar";
|
||||
import { sendRequestRescheduleEmail } from "./emailManager";
|
||||
import EventManager from "./eventManager";
|
||||
import { deleteMeeting } from "./videoClient";
|
||||
|
||||
type PersonAttendeeCommonFields = Pick<User, "id" | "email" | "name" | "locale" | "timeZone" | "username">;
|
||||
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
// @NOTE: This was copy from core since core uses app/store and creates circular dependency
|
||||
// @TODO: Improve import export on appstore/core
|
||||
import { Credential } from "@prisma/client";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoApiAdapterFactory } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import appStore from "../../index";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
|
||||
|
||||
const translator = short();
|
||||
|
||||
// factory
|
||||
const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
|
||||
withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
|
||||
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
||||
const app = appStore[appName as keyof typeof appStore];
|
||||
if (app && "lib" in app && "VideoApiAdapter" in app.lib) {
|
||||
const makeVideoApiAdapter = app.lib.VideoApiAdapter as VideoApiAdapterFactory;
|
||||
const videoAdapter = makeVideoApiAdapter(cred);
|
||||
acc.push(videoAdapter);
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const getBusyVideoTimes = (withCredentials: Credential[]) =>
|
||||
Promise.all(getVideoAdapters(withCredentials).map((c) => c.getAvailability())).then((results) =>
|
||||
results.reduce((acc, availability) => acc.concat(availability), [])
|
||||
);
|
||||
|
||||
const createMeeting = async (credential: Credential, calEvent: CalendarEvent): Promise<EventResult> => {
|
||||
const uid: string = getUid(calEvent);
|
||||
|
||||
if (!credential) {
|
||||
throw new Error(
|
||||
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
|
||||
);
|
||||
}
|
||||
|
||||
const videoAdapters = getVideoAdapters([credential]);
|
||||
const [firstVideoAdapter] = videoAdapters;
|
||||
const createdMeeting = await firstVideoAdapter.createMeeting(calEvent).catch((e) => {
|
||||
log.error("createMeeting failed", e, calEvent);
|
||||
});
|
||||
|
||||
if (!createdMeeting) {
|
||||
return {
|
||||
type: credential.type,
|
||||
success: false,
|
||||
uid,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success: true,
|
||||
uid,
|
||||
createdEvent: createdMeeting,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
const updateMeeting = async (
|
||||
credential: Credential,
|
||||
calEvent: CalendarEvent,
|
||||
bookingRef: PartialReference | null
|
||||
): Promise<EventResult> => {
|
||||
const uid = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL));
|
||||
|
||||
let success = true;
|
||||
|
||||
const [firstVideoAdapter] = getVideoAdapters([credential]);
|
||||
const updatedMeeting =
|
||||
credential && bookingRef
|
||||
? await firstVideoAdapter.updateMeeting(bookingRef, calEvent).catch((e) => {
|
||||
log.error("updateMeeting failed", e, calEvent);
|
||||
success = false;
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (!updatedMeeting) {
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: credential.type,
|
||||
success,
|
||||
uid,
|
||||
updatedEvent: updatedMeeting,
|
||||
originalEvent: calEvent,
|
||||
};
|
||||
};
|
||||
|
||||
const deleteMeeting = (credential: Credential, uid: string): Promise<unknown> => {
|
||||
if (credential) {
|
||||
return getVideoAdapters([credential])[0].deleteMeeting(uid);
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
export { getBusyVideoTimes, createMeeting, updateMeeting, deleteMeeting };
|
|
@ -304,6 +304,7 @@ export default class EventManager {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/** @fixme potential bug since Google Meet are saved as `integrations:google:meet` and there are no `google:meet` type in our DB */
|
||||
const integrationName = event.location.replace("integrations:", "");
|
||||
|
||||
return this.videoCredentials.find((credential: Credential) => credential.type.includes(integrationName));
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { Availability } from "@prisma/client";
|
||||
import dayjs, { ConfigType } from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
import { nameOfDay } from "@calcom/lib/weekday";
|
||||
import type { Availability } from "@calcom/prisma/client";
|
||||
import type { Schedule, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "AppCategories" AS ENUM ('calendar', 'messaging', 'other', 'payment', 'video', 'web3');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Credential" ADD COLUMN "appId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "App" (
|
||||
"slug" TEXT NOT NULL,
|
||||
"dirName" TEXT NOT NULL,
|
||||
"keys" JSONB,
|
||||
"categories" "AppCategories"[],
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "App_pkey" PRIMARY KEY ("slug")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "App_slug_key" ON "App"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "App_dirName_key" ON "App"("dirName");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Credential" ADD CONSTRAINT "Credential_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App"("slug") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- Connects each saved Credential to their respective App
|
||||
UPDATE "Credential" SET "appId" = 'apple-calendar' WHERE "type" = 'apple_calendar';
|
||||
UPDATE "Credential" SET "appId" = 'caldav-calendar' WHERE "type" = 'caldav_calendar';
|
||||
UPDATE "Credential" SET "appId" = 'google-calendar' WHERE "type" = 'google_calendar';
|
||||
UPDATE "Credential" SET "appId" = 'google-meet' WHERE "type" = 'google_video';
|
||||
UPDATE "Credential" SET "appId" = 'office365-calendar' WHERE "type" = 'office365_calendar';
|
||||
UPDATE "Credential" SET "appId" = 'msteams' WHERE "type" = 'office365_video';
|
||||
UPDATE "Credential" SET "appId" = 'dailyvideo' WHERE "type" = 'daily_video';
|
||||
UPDATE "Credential" SET "appId" = 'tandem' WHERE "type" = 'tandem_video';
|
||||
UPDATE "Credential" SET "appId" = 'zoom' WHERE "type" = 'zoom_video';
|
||||
UPDATE "Credential" SET "appId" = 'jitsi' WHERE "type" = 'jitsi_video';
|
||||
UPDATE "Credential" SET "appId" = 'hubspot' WHERE "type" = 'hubspot_other_calendar';
|
||||
UPDATE "Credential" SET "appId" = 'wipe-my-cal' WHERE "type" = 'wipemycal_other';
|
||||
UPDATE "Credential" SET "appId" = 'huddle01' WHERE "type" = 'huddle01_video';
|
||||
UPDATE "Credential" SET "appId" = 'slack' WHERE "type" = 'slack_messaging';
|
||||
UPDATE "Credential" SET "appId" = 'stripe' WHERE "type" = 'stripe_payment';
|
|
@ -13,21 +13,22 @@
|
|||
"db-setup": "run-s db-up db-deploy db-seed",
|
||||
"db-studio": "yarn prisma studio",
|
||||
"db-up": "docker-compose up -d",
|
||||
"deploy": "yarn prisma migrate deploy",
|
||||
"deploy": "yarn prisma migrate deploy && yarn seed-app-store",
|
||||
"dx": "yarn db-setup",
|
||||
"generate-schemas": "prisma generate && prisma format",
|
||||
"postinstall": "yarn generate-schemas"
|
||||
"postinstall": "yarn generate-schemas",
|
||||
"seed-app-store": "ts-node --transpile-only ./seed-app-store.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "3.10.0",
|
||||
"prisma": "^3.13.0",
|
||||
"ts-node": "^10.6.0",
|
||||
"zod": "^3.14.4",
|
||||
"zod-prisma": "^0.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@prisma/client": "^3.12.0"
|
||||
"@prisma/client": "^3.13.0"
|
||||
},
|
||||
"main": "index.ts",
|
||||
"types": "index.d.ts",
|
||||
|
@ -37,6 +38,6 @@
|
|||
"zod-utils.ts"
|
||||
],
|
||||
"prisma": {
|
||||
"seed": "ts-node ./seed.ts"
|
||||
"seed": "ts-node --transpile-only ./seed.ts"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,11 +77,13 @@ model EventType {
|
|||
}
|
||||
|
||||
model Credential {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
type String
|
||||
key Json
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int?
|
||||
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||
appId String?
|
||||
}
|
||||
|
||||
enum UserPlan {
|
||||
|
@ -440,3 +442,26 @@ model Session {
|
|||
expires DateTime
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum AppCategories {
|
||||
calendar
|
||||
messaging
|
||||
other
|
||||
payment
|
||||
video
|
||||
web3
|
||||
}
|
||||
|
||||
model App {
|
||||
// The slug for the app store public page inside `/apps/[slug]`
|
||||
slug String @id @unique
|
||||
// The directory name for `/packages/app-store/[dirName]`
|
||||
dirName String @unique
|
||||
// Needed API Keys
|
||||
keys Json?
|
||||
// One or multiple categories to which this app belongs
|
||||
categories AppCategories[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
credentials Credential[]
|
||||
}
|
||||
|
|
113
packages/prisma/seed-app-store.ts
Normal file
113
packages/prisma/seed-app-store.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import prisma from ".";
|
||||
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
|
||||
async function createApp(
|
||||
slug: Prisma.AppCreateInput["slug"],
|
||||
/** The directory name for `/packages/app-store/[dirName]` */
|
||||
dirName: Prisma.AppCreateInput["dirName"],
|
||||
categories: Prisma.AppCreateInput["categories"],
|
||||
keys?: Prisma.AppCreateInput["keys"]
|
||||
) {
|
||||
await prisma.app.upsert({
|
||||
where: { slug },
|
||||
create: { slug, dirName, categories, keys },
|
||||
update: { dirName, categories, keys },
|
||||
});
|
||||
console.log(`📲 Upserted app: '${slug}'`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Calendar apps
|
||||
await createApp("apple-calendar", "applecalendar", ["calendar"]);
|
||||
await createApp("caldav-calendar", "caldavcalendar", ["calendar"]);
|
||||
try {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web;
|
||||
await createApp("google-calendar", "googlecalendar", ["calendar"], {
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uris,
|
||||
});
|
||||
await createApp("google-meet", "googlevideo", ["video"], { client_id, client_secret, redirect_uris });
|
||||
} catch (e) {
|
||||
if (e instanceof Error) console.error("Error adding google credentials to DB:", e.message);
|
||||
}
|
||||
if (process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET) {
|
||||
await createApp("office365-calendar", "office365calendar", ["calendar"], {
|
||||
client_id: process.env.MS_GRAPH_CLIENT_ID,
|
||||
client_secret: process.env.MS_GRAPH_CLIENT_SECRET,
|
||||
});
|
||||
await createApp("msteams", "office365video", ["video"]);
|
||||
}
|
||||
// Video apps
|
||||
if (process.env.DAILY_API_KEY) {
|
||||
await createApp("dailyvideo", "dailyvideo", ["video"], {
|
||||
api_key: process.env.DAILY_API_KEY,
|
||||
scale_plan: process.env.DAILY_SCALE_PLAN,
|
||||
});
|
||||
}
|
||||
if (process.env.TANDEM_CLIENT_ID && process.env.TANDEM_CLIENT_SECRET) {
|
||||
await createApp("tandem", "tandemvideo", ["video"], {
|
||||
client_id: process.env.TANDEM_CLIENT_ID as string,
|
||||
client_secret: process.env.TANDEM_CLIENT_SECRET as string,
|
||||
base_url: (process.env.TANDEM_BASE_URL as string) || "https://tandem.chat",
|
||||
});
|
||||
}
|
||||
if (process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET) {
|
||||
await createApp("zoom", "zoomvideo", ["video"], {
|
||||
client_id: process.env.ZOOM_CLIENT_ID,
|
||||
client_secret: process.env.ZOOM_CLIENT_SECRET,
|
||||
});
|
||||
}
|
||||
await createApp("jitsi", "jitsivideo", ["video"]);
|
||||
// Other apps
|
||||
if (process.env.HUBSPOT_CLIENT_ID && process.env.HUBSPOT_CLIENT_SECRET) {
|
||||
await createApp("hubspot", "hubspotothercalendar", ["other"], {
|
||||
client_id: process.env.HUBSPOT_CLIENT_ID,
|
||||
client_secret: process.env.HUBSPOT_CLIENT_SECRET,
|
||||
});
|
||||
}
|
||||
await createApp("wipe-my-cal", "wipemycalother", ["other"]);
|
||||
if (process.env.GIPHY_API_KEY) {
|
||||
await createApp("giphy", "giphy", ["other"], {
|
||||
api_key: process.env.GIPHY_API_KEY,
|
||||
});
|
||||
}
|
||||
// Web3 apps
|
||||
await createApp("huddle01", "huddle01video", ["web3", "video"]);
|
||||
// Messaging apps
|
||||
if (process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET && process.env.SLACK_SIGNING_SECRET) {
|
||||
await createApp("slack", "slackmessaging", ["messaging"], {
|
||||
client_id: process.env.SLACK_CLIENT_ID,
|
||||
client_secret: process.env.SLACK_CLIENT_SECRET,
|
||||
signing_secret: process.env.SLACK_SIGNING_SECRET,
|
||||
});
|
||||
}
|
||||
// Payment apps
|
||||
if (
|
||||
process.env.STRIPE_CLIENT_ID &&
|
||||
process.env.STRIPE_PRIVATE_KEY &&
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||
process.env.STRIPE_WEBHOOK_SECRET
|
||||
) {
|
||||
await createApp("stripe", "stripepayment", ["payment"], {
|
||||
client_id: process.env.STRIPE_CLIENT_ID,
|
||||
client_secret: process.env.STRIPE_PRIVATE_KEY,
|
||||
payment_fee_fixed: 10,
|
||||
payment_fee_percentage: 0.005,
|
||||
public_key: process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY,
|
||||
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
|
@ -1,13 +1,14 @@
|
|||
import { MembershipRole, Prisma, PrismaClient, UserPlan } from "@prisma/client";
|
||||
import { MembershipRole, Prisma, UserPlan } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
import { uuid } from "short-uuid";
|
||||
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
import prisma from ".";
|
||||
import "./seed-app-store";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
|
||||
async function createUserAndEventType(opts: {
|
||||
user: {
|
||||
|
|
0
test
0
test
Loading…
Reference in a new issue