feat: support lark calendar (#3019)

* feat: support lark calendar

* New Crowdin translations by Github Action (#3016)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: lark calendar get app ticket bugs

* feat: add send msg service for lark bot

* fix: comment on PR of lark-calendar

* chore: update lark bot message

* Refactors add endpoint

* Adds missing GET endpoint handler

* Update yarn.lock

* fix: comments

* Update yarn.lock

* fix: comment about inferred type

* Added fetcher helper

* Update yarn.lock

Co-authored-by: chengcheng.frontend <chengcheng.frontend@bytedance.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Cheng CHENG 2022-08-06 01:56:20 +08:00 committed by GitHub
parent 775fe92960
commit be16b72400
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1330 additions and 26 deletions

View File

@ -88,3 +88,11 @@ VITAL_REGION="us"
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
ZAPIER_INVITE_LINK=""
# *********************************************************************************************************
# - LARK
# Needed to enable Lark Calendar integration and Login with Lark
# @see <https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/g>
LARK_OPEN_APP_ID=""
LARK_OPEN_APP_SECRET=""
LARK_OPEN_VERIFICATION_TOKEN=""
# *********************************************************************************************************

View File

@ -3,15 +3,7 @@
"description": "Open Source Scheduling",
"repository": "https://github.com/calcom/cal.com",
"logo": "https://cal.com/android-chrome-512x512.png",
"keywords": [
"react",
"typescript",
"node",
"nextjs",
"prisma",
"postgres",
"trpc"
],
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
"addons": [
{
"plan": "heroku-postgresql:hobby-dev"

View File

@ -17,6 +17,7 @@ import { metadata as googlevideo_meta } from "./googlevideo/_metadata";
import { metadata as hubspotothercalendar_meta } from "./hubspotothercalendar/_metadata";
import { metadata as huddle01video_meta } from "./huddle01video/_metadata";
import { metadata as jitsivideo_meta } from "./jitsivideo/_metadata";
import { metadata as larkcalendar_meta } from "./larkcalendar/_metadata";
import { metadata as metamask_meta } from "./metamask/_metadata";
import { metadata as office365calendar_meta } from "./office365calendar/_metadata";
import { metadata as office365video_meta } from "./office365video/_metadata";
@ -44,6 +45,7 @@ export const appStoreMetadata = {
hubspotothercalendar: hubspotothercalendar_meta,
huddle01video: huddle01video_meta,
jitsivideo: jitsivideo_meta,
larkcalendar: larkcalendar_meta,
metamask: metamask_meta,
office365calendar: office365calendar_meta,
office365video: office365video_meta,
@ -68,6 +70,7 @@ export const InstallAppButtonMap = {
hubspotothercalendar: dynamic(() => import("./hubspotothercalendar/components/InstallAppButton")),
huddle01video: dynamic(() => import("./huddle01video/components/InstallAppButton")),
jitsivideo: dynamic(() => import("./jitsivideo/components/InstallAppButton")),
larkcalendar: dynamic(() => import("./larkcalendar/components/InstallAppButton")),
metamask: dynamic(() => import("./metamask/components/InstallAppButton")),
office365calendar: dynamic(() => import("./office365calendar/components/InstallAppButton")),
office365video: dynamic(() => import("./office365video/components/InstallAppButton")),

View File

@ -14,6 +14,7 @@ export const apiHandlers = {
hubspotothercalendar: import("./hubspotothercalendar/api"),
huddle01video: import("./huddle01video/api"),
jitsivideo: import("./jitsivideo/api"),
larkcalendar: import("./larkcalendar/api"),
metamask: import("./metamask/api"),
office365calendar: import("./office365calendar/api"),
office365video: import("./office365video/api"),

View File

@ -10,6 +10,7 @@ import * as googlevideo from "./googlevideo";
import * as hubspotothercalendar from "./hubspotothercalendar";
import * as huddle01video from "./huddle01video";
import * as jitsivideo from "./jitsivideo";
import * as larkcalendar from "./larkcalendar";
import * as metamask from "./metamask";
import * as office365calendar from "./office365calendar";
import * as office365video from "./office365video";
@ -31,6 +32,7 @@ const appStore = {
hubspotothercalendar,
huddle01video,
jitsivideo,
larkcalendar,
office365calendar,
office365video,
slackmessaging,

View File

@ -0,0 +1,8 @@
---
items:
- /api/app-store/larkcalendar/icon.svg
---
<Slider items={items} />
Lark Calendar is a time management and scheduling service developed by Lark. Allows users to create and edit events, with options available for type and time. Available to anyone that has a Lark account on both mobile and web versions.

View File

@ -0,0 +1,25 @@
import type { App } from "@calcom/types/App";
import _package from "./package.json";
export const metadata = {
name: "Lark Calendar",
description: _package.description,
installed: true,
type: "lark_calendar",
title: "Lark Calendar",
imageSrc: "/api/app-store/larkcalendar/icon.svg",
variant: "calendar",
category: "calendar",
logo: "/api/app-store/larkcalendar/icon.svg",
publisher: "Cal.com",
rating: 5,
reviews: 69,
slug: "lark-calendar",
trending: false,
url: "https://cal.com/",
verified: true,
email: "help@cal.com",
} as App;
export default metadata;

View File

@ -0,0 +1,50 @@
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 { encodeOAuthState } from "../../_utils/encodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { LARK_HOST } from "../common";
const larkKeysSchema = z.object({
app_id: z.string(),
app_secret: z.string(),
});
async function getHandler(req: NextApiRequest) {
const appKeys = await getAppKeysFromSlug("lark-calendar");
const { app_secret, app_id } = larkKeysSchema.parse(appKeys);
const state = encodeOAuthState(req);
const params = {
app_id,
redirect_uri: WEBAPP_URL + "/api/integrations/larkcalendar/callback",
state,
};
const query = stringify(params);
const url = `https://${LARK_HOST}/open-apis/authen/v1/index?${query}`;
// trigger app_ticket_immediately
fetch(`https://${LARK_HOST}/open-apis/auth/v3/app_ticket/resend`, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
app_id,
app_secret,
}),
});
return { url };
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});

View File

@ -0,0 +1,100 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import { LARK_HOST } from "../common";
import { getAppAccessToken } from "../lib/AppAccessToken";
import type { LarkAuthCredentials } from "../types/LarkCalendar";
const log = logger.getChildLogger({ prefix: [`[[lark/api/callback]`] });
const callbackQuerySchema = z.object({
code: z.string().min(1),
});
async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { code } = callbackQuerySchema.parse(req.query);
const state = decodeOAuthState(req);
try {
const appAccessToken = await getAppAccessToken();
const response = await fetch(`https://${LARK_HOST}/open-apis/authen/v1/access_token`, {
method: "POST",
headers: {
Authorization: "Bearer " + appAccessToken,
"Content-Type": "application/json",
},
body: JSON.stringify({
grant_type: "authorization_code",
code,
}),
});
const responseBody = await response.json();
if (!response.ok || responseBody.code !== 0) {
log.error("get user_access_token failed with none 0 code", responseBody);
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody));
}
const key: LarkAuthCredentials = {
expiry_date: Math.round(+new Date() / 1000 + responseBody.data.expires_in),
access_token: responseBody.data.access_token,
refresh_token: responseBody.data.refresh_token,
refresh_expires_date: Math.round(+new Date() / 1000 + responseBody.data.refresh_expires_in),
};
/**
* A user can have only one pair of refresh_token and access_token effective
* at same time. Newly created refresh_token and access_token will invalidate
* older ones. So we need to keep only one lark credential per user only.
* However, a user may connect many times, since both userId and type are
* not unique in schema, so we have to use credential id as index for looking
* for the unique access_token token. In this case, id does not exist before created, so we cannot use credential id (which may not exist) as where statement
*/
const currentCredential = await prisma.credential.findFirst({
where: {
userId: req.session?.user.id,
type: "lark_calendar",
},
});
if (!currentCredential) {
await prisma.credential.create({
data: {
type: "lark_calendar",
key,
userId: req.session?.user.id,
appId: "lark-calendar",
},
});
} else {
await prisma.credential.update({
data: {
type: "lark_calendar",
key,
userId: req.session?.user.id,
appId: "lark-calendar",
},
where: {
id: currentCredential.id,
},
});
}
res.redirect(getSafeRedirectUrl(state?.returnTo) ?? "/apps/installed");
} catch (error) {
log.error("handle callback error", error);
res.redirect(state?.returnTo ?? "/apps/installed");
}
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});

View File

@ -0,0 +1,122 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { getAppKeys } from "../common";
import { sendPostMsg } from "../lib/BotService";
const log = logger.getChildLogger({ prefix: [`[lark/api/events]`] });
const larkKeysSchema = z.object({
open_verification_token: z.string(),
});
const appTicketEventsReqSchema = z.object({
body: z.object({
event: z.object({
app_ticket: z.string().min(1),
}),
}),
});
const imMessageReceivedEventsReqSchema = z.object({
body: z.object({
header: z.object({
tenant_key: z.string().min(1),
}),
event: z.object({
sender: z.object({
sender_id: z.object({
open_id: z.string().min(1),
}),
}),
}),
}),
});
const p2pChatCreateEventsReqSchema = z.object({
body: z.object({
tenant_key: z.string().min(1),
event: z.object({
user: z.object({
open_id: z.string().min(1),
}),
}),
}),
});
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
log.debug("receive events", req.body);
const appKeys = await getAppKeys();
const { open_verification_token } = larkKeysSchema.parse(appKeys);
// used for events handler binding in lark open platform, see
// https://open.larksuite.com/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM?lang=en-US
if (req.body.type === "url_verification") {
return res.status(200).json({ challenge: req.body.challenge });
}
// used for receiving app_ticket, see
// https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/application-v6/event/app_ticket-events
if (req.body.event?.type === "app_ticket" && open_verification_token === req.body.token) {
const {
body: {
event: { app_ticket: appTicket },
},
} = appTicketEventsReqSchema.parse(req);
await prisma.app.update({
where: { slug: "lark-calendar" },
data: {
keys: {
...appKeys,
app_ticket: appTicket,
},
},
});
return res.status(200).json({ code: 0, msg: "success" });
}
// used for handle user at bot in lark chat with cal.com connector bot, see
// https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive
if (req.body.header?.event_type === "im.message.receive_v1") {
const {
body: {
header: { tenant_key: tenantKey },
event: {
sender: {
sender_id: { open_id: senderOpenId },
},
},
},
} = imMessageReceivedEventsReqSchema.parse(req);
sendPostMsg(tenantKey, senderOpenId);
return res.status(200).json({ code: 0, msg: "success" });
}
// used for handle user first talk with cal.com connector bot, see
// https://open.larksuite.com/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/bot-events
if (req.body.event?.type === "p2p_chat_create") {
const {
body: {
tenant_key: tenantKey,
event: {
user: { open_id: senderOpenId },
},
},
} = p2pChatCreateEventsReqSchema.parse(req);
sendPostMsg(tenantKey, senderOpenId);
return res.status(200).json({ code: 0, msg: "success" });
}
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});

View File

@ -0,0 +1,3 @@
export { default as add } from "./add";
export { default as callback } from "./callback";
export { default as events } from "./events";

View File

@ -0,0 +1,26 @@
import logger from "@calcom/lib/logger";
import getAppKeysFromSlug from "../_utils/getAppKeysFromSlug";
import { LarkAppKeys } from "./types/LarkCalendar";
export const LARK_HOST = "open.larksuite.com";
export const getAppKeys = () => getAppKeysFromSlug("lark-calendar") as Promise<LarkAppKeys>;
export const isExpired = (expiryDate: number) =>
!expiryDate || expiryDate < Math.round(Number(new Date()) / 1000);
export async function handleLarkError<T extends { code: number; msg: string }>(
response: Response,
log: typeof logger
): Promise<T> {
const data: T = await response.json();
if (!response.ok || data.code !== 0) {
log.error("lark error with error: ", data, ", logid is:", response.headers.get("X-Tt-Logid"));
log.debug("lark request with data", data);
throw data;
}
log.info("lark request with logid:", response.headers.get("X-Tt-Logid"));
log.debug("lark request with data", data);
return data;
}

View File

@ -0,0 +1,18 @@
import type { InstallAppButtonProps } from "@calcom/app-store/types";
import useAddAppMutation from "../../_utils/useAddAppMutation";
export default function InstallAppButton(props: InstallAppButtonProps) {
const mutation = useAddAppMutation("lark_calendar");
return (
<>
{props.render({
onClick() {
mutation.mutate("");
},
loading: mutation.isLoading,
})}
</>
);
}

View File

@ -0,0 +1 @@
export { default as InstallAppButton } from "./InstallAppButton";

View File

@ -0,0 +1,3 @@
export * as api from "./api";
export * as lib from "./lib";
export { metadata } from "./_metadata";

View File

@ -0,0 +1,155 @@
import { z } from "zod";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { LARK_HOST, getAppKeys, isExpired } from "../common";
const log = logger.getChildLogger({ prefix: [`[[LarkAppCredential]`] });
function makePoolingPromise<T>(
promiseCreator: () => Promise<T | null>,
times = 24,
delay = 5 * 1000
): Promise<T | null> {
return new Promise((resolve, reject) => {
promiseCreator()
.then(resolve)
.catch((err) => {
if (times <= 0) {
reject(err);
return;
}
setTimeout(() => {
makePoolingPromise(promiseCreator, times - 1, delay)
.then(resolve)
.catch(reject);
}, delay);
});
});
}
const appKeysSchema = z.object({
app_id: z.string().min(1),
app_secret: z.string().min(1),
});
const getValidAppKeys = async (): Promise<ReturnType<typeof getAppKeys>> => {
const appKeys = await getAppKeys();
const validAppKeys = appKeysSchema.parse(appKeys);
return validAppKeys;
};
const getAppTicketFromKeys = async (): Promise<string> => {
const appKeys = await getValidAppKeys();
const appTicketNew = appKeys?.app_ticket;
if (appTicketNew) {
return appTicketNew;
}
throw Error("lark appTicketNew not found in getAppTicketFromKeys");
};
const getAppTicket = async (): Promise<string> => {
log.debug("get app ticket invoked");
const appKeys = await getValidAppKeys();
if (typeof appKeys.app_ticket === "string" && appKeys.app_ticket !== "") {
log.debug("has app ticket", appKeys.app_ticket);
return appKeys.app_ticket;
}
/**
* Trigger app-ticket resend. app ticket can only be obtained from
* app_ticket event.
* see https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/application-v6/event/app_ticket-events
*/
log.info("Invoke app-ticket resend", appKeys.app_ticket);
fetch(`https://${LARK_HOST}/open-apis/auth/v3/app_ticket/resend`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: appKeys.app_id,
app_secret: appKeys.app_secret,
}),
});
/**
* 1. App_ticket is only valid for 1 hr.
* 2. The we cannot retrieve app_ticket by calling a API.
* 3. App_ticket can only be retrieved in app_ticket event, which is push from lark every hour.
* 4. We can trigger lark to push a new app_ticket
* 5. Therefore, after trigger resend app_ticket ticket, we have to
* pooling DB, as app_ticket will update ticket in DB
* see
* https://open.larksuite.com/document/ugTN1YjL4UTN24CO1UjN/uQjN1YjL0YTN24CN2UjN
* https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/app_ticket_resend
*/
const appTicketNew = await makePoolingPromise(getAppTicketFromKeys);
if (appTicketNew) {
log.debug("has new app ticket", appTicketNew);
return appTicketNew;
}
log.error("app ticket not found");
throw new Error("No app ticket found");
};
export const getAppAccessToken: () => Promise<string> = async () => {
log.debug("get app access token invoked");
const appKeys = await getValidAppKeys();
const appAccessToken = appKeys.app_access_token;
const expireDate = appKeys.expire_date;
if (appAccessToken && expireDate && !isExpired(expireDate)) {
log.debug("get app access token not expired", appAccessToken);
return appAccessToken;
}
const appTicket = await getAppTicket();
const fetchAppAccessToken = (app_ticket: string) =>
fetch(`https://${LARK_HOST}/open-apis/auth/v3/app_access_token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
app_id: appKeys.app_id,
app_secret: appKeys.app_secret,
app_ticket,
}),
});
const resp = await fetchAppAccessToken(appTicket);
const data = await resp.json();
if (!resp.ok || data.code !== 0) {
logger.error("lark error with error: ", data, ", logid is:", resp.headers.get("X-Tt-Logid"));
// appticket invalid, mostly outdated, delete and renew one
if (data.code === 10012) {
await prisma.app.update({
where: { slug: "lark-calendar" },
data: { keys: { ...appKeys, app_ticket: "" } },
});
return getAppAccessToken();
}
}
const newAppAccessToken = data.app_access_token;
const newExpireDate = Math.round(Number(new Date()) / 1000 + data.expire);
await prisma.app.update({
where: { slug: "lark-calendar" },
data: {
keys: {
...appKeys,
app_access_token: newAppAccessToken,
expire_date: newExpireDate,
},
},
});
return newAppAccessToken;
};

View File

@ -0,0 +1,130 @@
import logger from "@calcom/lib/logger";
import { LARK_HOST } from "../common";
import { getAppAccessToken } from "./AppAccessToken";
const log = logger.getChildLogger({ prefix: [`[[LarkTenantCredential]`] });
const msg = {
en_us: {
title: "Welcome to Cal.com!",
content: [
[
{
tag: "text",
text: "Cal.com is an open source scheduling infrastructure.",
},
],
[
{
tag: "text",
text: 'It allows users to send a unique "cal.com" URL that allows anyone to create bookings on their calendars',
},
],
[
{
tag: "text",
text: "",
},
],
[
{
tag: "text",
text: "Get started",
},
],
[
{
tag: "text",
text: "1. Visit https://cal.com and sign up for an account.",
},
],
[
{
tag: "text",
text: '2. Then go to "Apps" in Cal -> install ',
},
{
tag: "a",
text: '"Larksuite Calendar"',
href: "https://www.larksuite.com/hc/articles/057527702350",
},
{
tag: "text",
text: " -> sign-in via Lark",
},
],
[
{
tag: "text",
text: "3. Done. Create your Event Types and share your booking links with external participants!",
},
],
[
{
tag: "text",
text: "",
},
],
[
{
tag: "text",
text: "Do not hesitate to reach out to our agents if you need any assistance.",
},
],
[
{
tag: "a",
text: "Get Help",
href: "https://applink.larksuite.com/client/helpdesk/open?id=6650327445582905610",
},
],
],
},
};
async function getTenantAccessTokenByTenantKey(tenantKey: string): Promise<string> {
try {
const appAccessToken = await getAppAccessToken();
const resp = await fetch(`https://${LARK_HOST}/open-apis/auth/v3/tenant_access_token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
app_access_token: appAccessToken,
tenant_key: tenantKey,
}),
});
const data = await resp.json();
return data.tenant_access_token;
} catch (error) {
log.error(error);
throw error;
}
}
export async function sendPostMsg(
tenantKey: string,
senderOpenId: string,
message: string = JSON.stringify(msg)
): Promise<{ code: number; msg: string }> {
const tenantAccessToken = await getTenantAccessTokenByTenantKey(tenantKey);
const response = await fetch(`https://${LARK_HOST}/open-apis/im/v1/messages?receive_id_type=open_id`, {
method: "POST",
headers: {
Authorization: "Bearer " + tenantAccessToken,
"Content-Type": "application/json",
},
body: JSON.stringify({
receive_id: senderOpenId,
content: message,
msg_type: "post",
}),
});
const responseBody = await response.json();
log.debug("send message success", responseBody);
return responseBody;
}

View File

@ -0,0 +1,390 @@
import { Credential } from "@prisma/client";
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type {
Calendar,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
} from "@calcom/types/Calendar";
import { handleLarkError, isExpired, LARK_HOST } from "../common";
import type {
CreateAttendeesResp,
CreateEventResp,
FreeBusyResp,
GetPrimaryCalendarsResp,
LarkAuthCredentials,
LarkEvent,
LarkEventAttendee,
ListCalendarsResp,
RefreshTokenResp,
} from "../types/LarkCalendar";
import { getAppAccessToken } from "./AppAccessToken";
function parseEventTime2Timestamp(eventTime: string): string {
return String(+new Date(eventTime) / 1000);
}
export default class LarkCalendarService implements Calendar {
private url = `https://${LARK_HOST}/open-apis`;
private integrationName = "";
private log: typeof logger;
auth: { getToken: () => Promise<string> };
constructor(credential: Credential) {
this.integrationName = "lark_calendar";
this.auth = this.larkAuth(credential);
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
}
private larkAuth = (credential: Credential) => {
const larkAuthCredentials = credential.key as LarkAuthCredentials;
return {
getToken: () =>
!isExpired(larkAuthCredentials.expiry_date)
? Promise.resolve(larkAuthCredentials.access_token)
: this.refreshAccessToken(credential),
};
};
private refreshAccessToken = async (credential: Credential) => {
const larkAuthCredentials = credential.key as LarkAuthCredentials;
const refreshExpireDate = larkAuthCredentials.refresh_expires_date;
const refreshToken = larkAuthCredentials.refresh_token;
if (isExpired(refreshExpireDate) || !refreshToken) {
const res = await fetch("/api/integrations", {
method: "DELETE",
body: JSON.stringify({ id: credential.id }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("disconnection wrong");
}
throw new Error("refresh token expires");
}
try {
const appAccessToken = await getAppAccessToken();
const resp = await this.fetcher(`/authen/v1/refresh_access_token`, {
method: "POST",
headers: {
Authorization: `Bearer ${appAccessToken}`,
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
});
const data = await handleLarkError<RefreshTokenResp>(resp, this.log);
const newLarkAuthCredentials: LarkAuthCredentials = {
...larkAuthCredentials,
access_token: data.data.access_token,
expiry_date: Math.round(+new Date() / 1000 + data.data.expires_in),
};
await prisma.credential.update({
where: {
id: credential.id,
},
data: {
key: newLarkAuthCredentials,
},
});
return newLarkAuthCredentials.access_token;
} catch (error) {
this.log.error("LarkCalendarService refreshAccessToken error", error);
throw error;
}
};
private fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
const accessToken = await this.auth.getToken();
return fetch(`${this.url}${endpoint}`, {
method: "GET",
headers: {
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
...init?.headers,
},
...init,
});
};
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
let eventId = "";
let eventRespData;
const calendarId = event.destinationCalendar?.externalId;
if (!calendarId) {
throw new Error("no calendar id");
}
try {
const eventResponse = await this.fetcher(`/calendar/v4/calendars/${calendarId}/events/create_event`, {
method: "POST",
body: JSON.stringify(this.translateEvent(event)),
});
eventRespData = await handleLarkError<CreateEventResp>(eventResponse, this.log);
eventId = eventRespData.data.event.event_id as string;
} catch (error) {
this.log.error(error);
throw error;
}
try {
await this.createAttendees(event, eventId);
return {
...eventRespData,
uid: eventRespData.data.event.event_id as string,
id: eventRespData.data.event.event_id as string,
type: "lark_calendar",
password: "",
url: "",
additionalInfo: {},
};
} catch (error) {
this.log.error(error);
await this.deleteEvent(eventId, event, calendarId);
throw error;
}
}
private createAttendees = async (event: CalendarEvent, eventId: string) => {
const calendarId = event.destinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in createAttendees");
throw new Error("no calendar id provided in createAttendees");
}
const attendeeResponse = await this.fetcher(
`/calendar/v4/calendars/${calendarId}/events/${eventId}/attendees/create_attendees`,
{
method: "POST",
body: JSON.stringify({
attendees: this.translateAttendees(event),
need_notification: false,
}),
}
);
return handleLarkError<CreateAttendeesResp>(attendeeResponse, this.log);
};
/**
* @param uid
* @param event
* @returns
*/
async updateEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
const eventId = uid;
let eventRespData;
const calendarId = externalCalendarId || event.destinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in updateEvent");
throw new Error("no calendar id provided in updateEvent");
}
try {
const eventResponse = await this.fetcher(
`/calendar/v4/calendars/${calendarId}/events/${eventId}/patch_event`,
{
method: "PATCH",
body: JSON.stringify(this.translateEvent(event)),
}
);
eventRespData = await handleLarkError<CreateEventResp>(eventResponse, this.log);
} catch (error) {
this.log.error(error);
throw error;
}
try {
// Since attendees cannot be changed any more, updateAttendees is not needed
// await this.updateAttendees(event, eventId);
return {
...eventRespData,
uid: eventRespData.data.event.event_id as string,
id: eventRespData.data.event.event_id as string,
type: "lark_calendar",
password: "",
url: "",
additionalInfo: {},
};
} catch (error) {
this.log.error(error);
await this.deleteEvent(eventId, event);
throw error;
}
}
/**
* @param uid
* @param event
* @returns
*/
async deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string) {
const calendarId = externalCalendarId || event.destinationCalendar?.externalId;
if (!calendarId) {
this.log.error("no calendar id provided in deleteEvent");
throw new Error("no calendar id provided in deleteEvent");
}
try {
const response = await this.fetcher(`/calendar/v4/calendars/${calendarId}/events/${uid}`, {
method: "DELETE",
});
await handleLarkError(response, this.log);
} catch (error) {
this.log.error(error);
throw error;
}
}
async getAvailability(
dateFrom: string,
dateTo: string,
selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
const selectedCalendarIds = selectedCalendars
.filter((e) => e.integration === this.integrationName)
.map((e) => e.externalId)
.filter(Boolean);
if (selectedCalendarIds.length === 0 && selectedCalendars.length > 0) {
// Only calendars of other integrations selected
return Promise.resolve([]);
}
try {
let queryIds = selectedCalendarIds;
if (queryIds.length === 0) {
queryIds = (await this.listCalendars()).map((e) => e.externalId).filter(Boolean) || [];
if (queryIds.length === 0) {
return Promise.resolve([]);
}
}
const response = await this.fetcher(`/calendar/v4/freebusy/batch_get`, {
method: "POST",
headers: {
"x-tt-env": "boe_wangzichao",
},
body: JSON.stringify({
time_min: dateFrom,
time_max: dateTo,
calendar_ids: queryIds,
}),
});
const data = await handleLarkError<FreeBusyResp>(response, this.log);
const busyData =
data.data.freebusy_list?.reduce<BufferedBusyTime[]>((acc, cur) => {
acc.push({
start: cur.start_time,
end: cur.end_time,
});
return acc;
}, []) || [];
return busyData;
} catch (error) {
this.log.error(error);
return [];
}
}
listCalendars = async (): Promise<IntegrationCalendar[]> => {
try {
const resp = await this.fetcher(`/calendar/v4/calendars`);
const data = await handleLarkError<ListCalendarsResp>(resp, this.log);
const result = data.data.calendar_list
.filter((cal) => {
if (cal.type !== "primary" && cal.type !== "shared") {
return false;
}
if (cal.permissions === "private") {
return false;
}
if (cal.role === "owner" || cal.role === "writer") {
return true;
}
return false;
})
.map((cal) => {
const calendar: IntegrationCalendar = {
externalId: cal.calendar_id ?? "No Id",
integration: this.integrationName,
name: cal.summary_alias || cal.summary || "No calendar name",
primary: cal.type === "primary",
};
return calendar;
});
if (result.some((cal) => !!cal.primary)) {
return result;
}
// No primary calendar found, get primary calendar directly
const respPrimary = await this.fetcher(`/calendar/v4/calendars/primary`, {
method: "POST",
});
const dataPrimary = await handleLarkError<GetPrimaryCalendarsResp>(respPrimary, this.log);
return dataPrimary.data.calendars.map((item) => {
const cal = item.calendar;
const calendar: IntegrationCalendar = {
externalId: cal.calendar_id ?? "No Id",
integration: this.integrationName,
name: cal.summary_alias || cal.summary || "No calendar name",
primary: cal.type === "primary",
};
return calendar;
});
} catch (err) {
this.log.error("There was an error contacting lark calendar service: ", err);
throw err;
}
};
private translateEvent = (event: CalendarEvent): LarkEvent => {
const larkEvent: LarkEvent = {
summary: event.title,
description: getRichDescription(event),
start_time: {
timestamp: parseEventTime2Timestamp(event.startTime),
timezone: event.organizer.timeZone,
},
end_time: {
timestamp: parseEventTime2Timestamp(event.endTime),
timezone: event.organizer.timeZone,
},
attendee_ability: "none",
free_busy_status: "busy",
reminders: [
{
minutes: 5,
},
],
};
if (event.location) {
larkEvent.location = { name: getLocation(event) };
}
return larkEvent;
};
private translateAttendees = (event: CalendarEvent): LarkEventAttendee[] => {
const attendees: LarkEventAttendee[] = event.attendees
.filter((att) => att.email)
.map((att) => {
const attendee: LarkEventAttendee = {
type: "third_party",
is_optional: false,
third_party_email: att.email,
};
return attendee;
});
return attendees;
};
}

View File

@ -0,0 +1 @@
export { default as CalendarService } from "./CalendarService";

View File

@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/larkcalendar",
"version": "0.0.0",
"main": "./index.ts",
"description": "Lark Calendar is a time management and scheduling service developed by Lark. Allows users to create and edit events, with options available for type and time. Available to anyone that has a Lark account on both mobile and web versions.",
"dependencies": {
"@calcom/lib": "*",
"@calcom/prisma": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}

View File

@ -0,0 +1,5 @@
<svg width="131" height="105" viewBox="0 0 131 105" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M68.046 55.551L68.366 55.232C68.574 55.024 68.798 54.8 69.022 54.592L69.47 54.16L70.797 52.849L72.62 51.074L74.171 49.539L75.626 48.099L77.146 46.596L78.537 45.221L80.488 43.302C80.856 42.934 81.239 42.582 81.623 42.23C82.327 41.591 83.062 40.967 83.798 40.359C84.486 39.816 85.189 39.288 85.909 38.776C86.916 38.057 87.956 37.401 89.011 36.761C90.051 36.154 91.122 35.578 92.21 35.0342C93.233 34.5385 94.288 34.0748 95.36 33.659C95.952 33.4191 96.559 33.2112 97.167 33.0033C97.471 32.9074 97.775 32.7954 98.094 32.6995C95.392 22.0651 90.45 12.1504 83.59 3.5949C82.263 1.9478 80.248 0.988297 78.137 0.988297H22.0867C21.511 0.988297 21.0312 1.452 21.0312 2.0437C21.0312 2.3795 21.1912 2.6834 21.463 2.8913C40.5889 16.9158 56.452 34.9383 67.918 55.695L68.046 55.551Z" fill="#00D6B9"/>
<path d="M45.77 104.885C74.715 104.885 99.934 88.91 113.079 65.306C113.543 64.474 113.99 63.643 114.422 62.795C113.766 64.059 113.031 65.274 112.215 66.425C111.927 66.825 111.639 67.225 111.336 67.625C110.952 68.12 110.568 68.584 110.168 69.048C109.848 69.416 109.529 69.768 109.193 70.119C108.521 70.823 107.818 71.495 107.082 72.118C106.666 72.47 106.266 72.806 105.835 73.126C105.339 73.51 104.827 73.877 104.315 74.213C103.996 74.437 103.66 74.645 103.324 74.853C102.988 75.061 102.636 75.269 102.268 75.477C101.549 75.876 100.797 76.26 100.046 76.596C99.39 76.884 98.718 77.156 98.047 77.412C97.311 77.683 96.575 77.923 95.808 78.131C94.672 78.451 93.537 78.691 92.37 78.867C91.538 78.995 90.675 79.091 89.827 79.155C88.931 79.219 88.02 79.235 87.108 79.235C86.101 79.219 85.093 79.155 84.07 79.043C83.318 78.963 82.567 78.851 81.815 78.723C81.16 78.611 80.504 78.467 79.848 78.307C79.496 78.227 79.161 78.131 78.809 78.035C77.849 77.779 76.89 77.507 75.93 77.236C75.451 77.092 74.971 76.964 74.507 76.82C73.787 76.612 73.084 76.388 72.38 76.164C71.804 75.988 71.229 75.796 70.653 75.604C70.109 75.429 69.55 75.253 69.006 75.061L67.887 74.677C67.439 74.517 66.975 74.357 66.527 74.197L65.568 73.845C64.928 73.622 64.288 73.382 63.665 73.142C63.297 72.998 62.929 72.87 62.561 72.726C62.066 72.534 61.586 72.342 61.09 72.15C60.578 71.942 60.051 71.735 59.539 71.527L58.531 71.111L57.284 70.599L56.325 70.199L55.333 69.768L54.47 69.384L53.686 69.032L52.886 68.664L52.071 68.28L51.031 67.801L49.944 67.289C49.56 67.097 49.176 66.921 48.793 66.729L47.817 66.249C30.6102 57.662 15.0824 46.084 1.93729 32.0435C1.53749 31.6277 0.881889 31.5957 0.450089 31.9955C0.242188 32.1874 0.114288 32.4752 0.114288 32.7631L0.146187 82.225V86.239C0.146187 88.574 1.29759 90.749 3.23259 92.044C15.818 100.455 30.6262 104.917 45.77 104.885Z" fill="#3370FF"/>
<path d="M130.83 35.1629C121.059 30.3815 109.881 29.358 99.406 32.3004C98.958 32.4284 98.527 32.5563 98.095 32.6842C97.791 32.7802 97.487 32.8761 97.167 32.9881C96.56 33.196 95.952 33.4198 95.36 33.6437C94.289 34.0595 93.249 34.5233 92.21 35.019C91.123 35.5467 90.051 36.122 89.012 36.73C87.94 37.354 86.917 38.025 85.909 38.745C85.19 39.257 84.486 39.784 83.799 40.328C83.047 40.936 82.327 41.544 81.624 42.199C81.24 42.551 80.872 42.903 80.488 43.271L78.537 45.19L77.146 46.565L75.627 48.068L74.172 49.507L72.62 51.043L70.813 52.834L69.486 54.145L69.038 54.577C68.83 54.785 68.607 55.008 68.383 55.216L68.063 55.536L67.567 56C67.375 56.176 67.199 56.336 67.007 56.512C62.194 60.941 56.821 64.731 51.048 67.818L52.087 68.297L52.903 68.681L53.702 69.049L54.486 69.401L55.35 69.785L56.341 70.216L57.301 70.616L58.548 71.128L59.555 71.544C60.067 71.752 60.595 71.959 61.107 72.167C61.586 72.359 62.082 72.551 62.578 72.743C62.946 72.887 63.313 73.015 63.681 73.159C64.321 73.399 64.96 73.623 65.584 73.862L66.544 74.214C66.991 74.374 67.439 74.534 67.903 74.694L69.022 75.078C69.566 75.254 70.11 75.446 70.669 75.621C71.245 75.813 71.821 75.989 72.397 76.181C73.1 76.405 73.82 76.613 74.523 76.837C75.003 76.981 75.483 77.125 75.947 77.253C76.906 77.524 77.866 77.796 78.825 78.052C79.177 78.148 79.513 78.228 79.865 78.324C80.52 78.484 81.176 78.612 81.832 78.74C82.583 78.868 83.335 78.98 84.086 79.06C85.11 79.172 86.117 79.236 87.125 79.252C88.036 79.268 88.948 79.236 89.843 79.172C90.707 79.108 91.554 79.012 92.386 78.884C93.537 78.708 94.689 78.452 95.824 78.148C96.576 77.94 97.327 77.7 98.063 77.429C98.735 77.189 99.406 76.917 100.062 76.613C100.813 76.277 101.565 75.893 102.285 75.494C102.637 75.302 102.988 75.094 103.34 74.87C103.692 74.662 104.012 74.438 104.332 74.23C104.843 73.878 105.355 73.527 105.851 73.143C106.283 72.823 106.698 72.487 107.098 72.135C107.818 71.512 108.521 70.84 109.193 70.136C109.529 69.785 109.849 69.433 110.169 69.065C110.568 68.601 110.968 68.121 111.336 67.642C111.64 67.258 111.928 66.858 112.215 66.442C113.015 65.291 113.751 64.092 114.406 62.844L115.158 61.357L121.842 48.036L121.922 47.876C124.129 43.111 127.136 38.825 130.83 35.1629Z" fill="#133C9A"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,160 @@
export type LarkAppKeys = {
app_id?: string;
app_secret?: string;
app_access_token?: string;
app_ticket?: string;
expire_date?: number;
open_verification_token?: string;
};
export type LarkAuthCredentials = {
expiry_date: number;
access_token: string;
refresh_token: string;
refresh_expires_date: number;
};
export type RefreshTokenResp = {
code: number;
msg: string;
data: {
access_token: string;
token_type: string;
expires_in: number;
name: string;
en_name: string;
avatar_url: string;
avatar_thumb: string;
avatar_middle: string;
avatar_big: string;
open_id: string;
union_id: string;
tenant_key: string;
refresh_expires_in: number;
refresh_token: string;
};
};
export type LarkEvent = {
event_id?: string;
organizer_calendar_id?: string;
summary: string;
description: string;
start_time: {
timestamp: string;
timezone: string;
};
end_time: {
timestamp: string;
timezone: string;
};
attendee_ability: "none";
free_busy_status: "busy";
location?: {
name?: string;
};
reminders: [
{
minutes: number;
}
];
};
export type CreateEventResp = {
code: number;
msg: string;
data: {
event: LarkEvent;
};
};
export type LarkEventAttendee = {
type: "user" | "third_party";
is_optional: boolean;
user_id?: string;
third_party_email: string;
};
export type CreateAttendeesResp = {
code: number;
msg: string;
data: {
attendees: LarkEventAttendee[];
};
};
export type ListAttendeesResp = {
code: number;
msg: string;
data: {
items: (LarkEventAttendee & { attendee_id: string })[];
has_more: boolean;
page_token: string;
};
};
export type FreeBusyResp = {
code: number;
msg: string;
data: {
error_calendar_list: {
calendar_id: string;
error_msg: string;
}[];
freebusy_list: {
calendar_id: string;
end_time: string;
start_time: string;
}[];
};
};
export type BufferedBusyTime = {
start: string;
end: string;
};
export type ListCalendarsResp = {
code: number;
msg: string;
data: {
has_more: boolean;
page_token: string;
sync_token: string;
calendar_list: [
{
calendar_id: string;
summary: string;
description: string;
permissions: "private" | "show_only_free_busy" | "public";
type: "unknown" | "shared" | "primary" | "google" | "resource" | "exchange";
summary_alias: string;
is_deleted: boolean;
is_third_party: boolean;
role: "unknown" | "free_busy_reader" | "reader" | "writer" | "owner";
}
];
};
};
export type GetPrimaryCalendarsResp = {
code: number;
msg: string;
data: {
calendars: [
{
calendar: {
calendar_id: string;
color: number;
description: string;
permissions: "private" | "show_only_free_busy" | "public";
role: "unknown" | "free_busy_reader" | "reader" | "writer" | "owner";
summary: string;
summary_alias: string;
type: "unknown" | "shared" | "primary" | "google" | "resource" | "exchange";
};
user_id: string;
}
];
};
};

View File

@ -166,6 +166,17 @@ export default async function main() {
client_secret: process.env.MS_GRAPH_CLIENT_SECRET,
});
}
if (
process.env.LARK_OPEN_APP_ID &&
process.env.LARK_OPEN_APP_SECRET &&
process.env.LARK_OPEN_VERIFICATION_TOKEN
) {
await createApp("lark-calendar", "larkcalendar", ["calendar"], "lark_calendar", {
app_id: process.env.LARK_OPEN_APP_ID,
app_secret: process.env.LARK_OPEN_APP_SECRET,
open_verification_token: process.env.LARK_OPEN_VERIFICATION_TOKEN,
});
}
// Video apps
if (process.env.DAILY_API_KEY) {
await createApp("daily-video", "dailyvideo", ["video"], "daily_video", {

109
yarn.lock
View File

@ -208,7 +208,7 @@
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.18.6", "@babel/generator@^7.18.7", "@babel/generator@^7.7.2":
"@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.7.2":
version "7.18.7"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.7.tgz#2aa78da3c05aadfc82dbac16c99552fc802284bd"
integrity sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==
@ -244,6 +244,24 @@
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.18.6":
version "7.18.12"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.12.tgz#fa58daa303757bd6f5e4bbca91b342040463d9f4"
integrity sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==
dependencies:
"@babel/types" "^7.18.10"
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
"@babel/generator@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.9.tgz#68337e9ea8044d6ddc690fb29acae39359cca0a5"
integrity sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==
dependencies:
"@babel/types" "^7.18.9"
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862"
@ -352,10 +370,10 @@
dependencies:
"@babel/types" "^7.16.7"
"@babel/helper-environment-visitor@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7"
integrity sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q==
"@babel/helper-environment-visitor@^7.18.6", "@babel/helper-environment-visitor@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
"@babel/helper-explode-assignable-expression@^7.18.6":
version "7.18.6"
@ -381,13 +399,13 @@
"@babel/template" "^7.16.7"
"@babel/types" "^7.17.0"
"@babel/helper-function-name@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz#8334fecb0afba66e6d87a7e8c6bb7fed79926b83"
integrity sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw==
"@babel/helper-function-name@^7.18.6", "@babel/helper-function-name@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0"
integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==
dependencies:
"@babel/template" "^7.18.6"
"@babel/types" "^7.18.6"
"@babel/types" "^7.18.9"
"@babel/helper-get-function-arity@^7.16.7":
version "7.16.7"
@ -537,6 +555,11 @@
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-string-parser@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56"
integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==
"@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad"
@ -567,7 +590,7 @@
"@babel/traverse" "^7.18.6"
"@babel/types" "^7.18.6"
"@babel/helpers@^7.12.5", "@babel/helpers@^7.18.6":
"@babel/helpers@^7.12.5":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.6.tgz#4c966140eaa1fcaa3d5a8c09d7db61077d4debfd"
integrity sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==
@ -594,6 +617,15 @@
"@babel/traverse" "^7.17.9"
"@babel/types" "^7.17.0"
"@babel/helpers@^7.18.6":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9"
integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ==
dependencies:
"@babel/template" "^7.18.6"
"@babel/traverse" "^7.18.9"
"@babel/types" "^7.18.9"
"@babel/highlight@^7.16.7":
version "7.16.10"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88"
@ -637,6 +669,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef"
integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==
"@babel/parser@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@ -1600,6 +1637,22 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/traverse@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
dependencies:
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.18.9"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.18.9"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.18.9"
"@babel/types" "^7.18.9"
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@7.13.0":
version "7.13.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80"
@ -1633,6 +1686,23 @@
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@babel/types@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.10.tgz#4908e81b6b339ca7c6b7a555a5fc29446f26dde6"
integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==
dependencies:
"@babel/helper-string-parser" "^7.18.10"
"@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0"
"@babel/types@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.9.tgz#7148d64ba133d8d73a41b3172ac4b83a1452205f"
integrity sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==
dependencies:
"@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0"
"@base2/pretty-print-object@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4"
@ -4391,9 +4461,9 @@
tslib "^1.9.3"
"@sentry/cli@^1.73.0":
version "1.74.4"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.4.tgz#7df82f68045a155e1885bfcbb5d303e5259eb18e"
integrity sha512-BMfzYiedbModsNBJlKeBOLVYUtwSi99LJ8gxxE4Bp5N8hyjNIN0WVrozAVZ27mqzAuy6151Za3dpmOLO86YlGw==
version "1.74.5"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.5.tgz#4a5c622913087c9ab6f82994da9a7526423779b8"
integrity sha512-Ze1ec306ZWHtrxKypOJ8nhtFqkrx2f/6bRH+DcJzEQ3bBePQ0ZnqJTTe4BBHADYBtxFIaUWzCZ6DquLz2Zv/sw==
dependencies:
https-proxy-agent "^5.0.0"
mkdirp "^0.5.5"
@ -5837,6 +5907,11 @@
dependencies:
"@types/node" "*"
"@types/debounce@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.1.tgz#79b65710bc8b6d44094d286aecf38e44f9627852"
integrity sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==
"@types/debug@4.1.7", "@types/debug@^4.0.0":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
@ -17326,9 +17401,9 @@ next-transpile-modules@^9.0.0:
escalade "^3.1.1"
next-validations@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/next-validations/-/next-validations-0.2.0.tgz#ce3c4bc332b115beda633521fd81e587987864eb"
integrity sha512-QMF2hRNSSbjeBaCYqpt3mEM9CkXXzaMCWCvPyi5/vKTBjbgkiYtaQnUfjj5eH8dX+ZmRrBYGgN1EKqL7ZnI0wQ==
version "0.2.1"
resolved "https://registry.yarnpkg.com/next-validations/-/next-validations-0.2.1.tgz#68010c9b017ba48eec4f404fd42eb9b0c7324737"
integrity sha512-92pR14MPTTx0ynlvYH2TwMf7WiGiznNL/l0dtZyKPw3x48rcMhwEZrP1ZmsMJwzp5D+U+sY2deexeLWC8rlNtQ==
next@12.2.0:
version "12.2.0"