first commit
This commit is contained in:
50
calcom/packages/app-store/feishucalendar/api/add.ts
Normal file
50
calcom/packages/app-store/feishucalendar/api/add.ts
Normal 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 getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
import { FEISHU_HOST } from "../common";
|
||||
|
||||
const feishuKeysSchema = z.object({
|
||||
app_id: z.string(),
|
||||
app_secret: z.string(),
|
||||
});
|
||||
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const appKeys = await getAppKeysFromSlug("feishu-calendar");
|
||||
const { app_secret, app_id } = feishuKeysSchema.parse(appKeys);
|
||||
|
||||
const state = encodeOAuthState(req);
|
||||
|
||||
const params = {
|
||||
app_id,
|
||||
redirect_uri: `${WEBAPP_URL}/api/integrations/feishucalendar/callback`,
|
||||
state,
|
||||
};
|
||||
|
||||
const query = stringify(params);
|
||||
|
||||
const url = `https://${FEISHU_HOST}/open-apis/authen/v1/index?${query}`;
|
||||
|
||||
// trigger app_ticket_immediately
|
||||
fetch(`https://${FEISHU_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) }),
|
||||
});
|
||||
130
calcom/packages/app-store/feishucalendar/api/callback.ts
Normal file
130
calcom/packages/app-store/feishucalendar/api/callback.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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 getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import { FEISHU_HOST } from "../common";
|
||||
import { getAppAccessToken } from "../lib/AppAccessToken";
|
||||
import type { FeishuAuthCredentials } from "../types/FeishuCalendar";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: [`[[feishu/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://${FEISHU_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: FeishuAuthCredentials = {
|
||||
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 feishu 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: "feishu_calendar",
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentCredential) {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "feishu_calendar",
|
||||
key,
|
||||
userId: req.session?.user.id,
|
||||
appId: "feishu-calendar",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.credential.update({
|
||||
data: {
|
||||
type: "feishu_calendar",
|
||||
key,
|
||||
userId: req.session?.user.id,
|
||||
appId: "feishu-calendar",
|
||||
},
|
||||
where: {
|
||||
id: currentCredential.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const primaryCalendarResponse = await fetch(
|
||||
`https://${FEISHU_HOST}/open-apis/calendar/v4/calendars/primary`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${key.access_token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (primaryCalendarResponse.status === 200) {
|
||||
const primaryCalendar = await primaryCalendarResponse.json();
|
||||
|
||||
if (primaryCalendar.data.calendars.calendar.calendar_id && req.session?.user?.id) {
|
||||
await prisma.selectedCalendar.create({
|
||||
data: {
|
||||
userId: req.session?.user.id,
|
||||
integration: "feishu_calendar",
|
||||
externalId: primaryCalendar.data.calendars.calendar.calendar_id as string,
|
||||
credentialId: currentCredential?.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state?.returnTo) ??
|
||||
getInstalledAppPath({ variant: "calendar", slug: "feishu-calendar" })
|
||||
);
|
||||
} catch (error) {
|
||||
log.error("handle callback error", error);
|
||||
res.redirect(state?.returnTo ?? "/apps/installed");
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||
});
|
||||
123
calcom/packages/app-store/feishucalendar/api/events.ts
Normal file
123
calcom/packages/app-store/feishucalendar/api/events.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
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.getSubLogger({ prefix: [`[feishu/api/events]`] });
|
||||
|
||||
const feishuKeysSchema = 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 } = feishuKeysSchema.parse(appKeys);
|
||||
|
||||
// used for events handler binding in feishu open platform, see
|
||||
// https://open.larksuite.com/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM?lang=en-US
|
||||
if (req.body.type === "url_verification" && req.body.token === open_verification_token) {
|
||||
log.debug("update token", req.body);
|
||||
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: "feishu-calendar" },
|
||||
data: {
|
||||
keys: {
|
||||
...appKeys,
|
||||
app_ticket: appTicket,
|
||||
},
|
||||
},
|
||||
});
|
||||
return res.status(200).json({ code: 0, msg: "success" });
|
||||
}
|
||||
|
||||
// used for handle user at bot in feishu 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) }),
|
||||
});
|
||||
3
calcom/packages/app-store/feishucalendar/api/index.ts
Normal file
3
calcom/packages/app-store/feishucalendar/api/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
||||
export { default as events } from "./events";
|
||||
Reference in New Issue
Block a user