Delete slack app (#5462)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>pull/5436/head^2
parent
7bda15aaa3
commit
29c4efe4a8
50
README.md
50
README.md
|
@ -354,56 +354,6 @@ following
|
|||
5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
|
||||
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
|
||||
|
||||
### Obtaining Slack Client ID and Secret and Signing Secret
|
||||
|
||||
To test this you will need to create a Slack app for yourself on [their apps website](https://api.slack.com/apps).
|
||||
|
||||
Copy and paste the app manifest below into the setting on your slack app. Be sure to replace `YOUR_DOMAIN` with your own domain or your proxy host if you're testing locally.
|
||||
|
||||
<details>
|
||||
<summary>App Manifest</summary>
|
||||
|
||||
```yaml
|
||||
display_information:
|
||||
name: Cal.com Slack
|
||||
features:
|
||||
bot_user:
|
||||
display_name: Cal.com Slack
|
||||
always_online: false
|
||||
slash_commands:
|
||||
- command: /create-event
|
||||
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
|
||||
description: Create an event within Cal!
|
||||
should_escape: false
|
||||
- command: /today
|
||||
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
|
||||
description: View all your bookings for today
|
||||
should_escape: false
|
||||
oauth_config:
|
||||
redirect_urls:
|
||||
- https://YOUR_DOMAIN/api/integrations/slackmessaging/callback
|
||||
scopes:
|
||||
bot:
|
||||
- chat:write
|
||||
- commands
|
||||
- chat:write.public
|
||||
settings:
|
||||
interactivity:
|
||||
is_enabled: true
|
||||
request_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
|
||||
message_menu_options_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
|
||||
org_deploy_enabled: false
|
||||
socket_mode_enabled: false
|
||||
token_rotation_enabled: false
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Add the integration as normal - slack app - add. Follow the oauth flow to add it to a server.
|
||||
|
||||
Next make sure you have your app running `yarn dx`. Then in the slack chat type one of these commands: `/create-event` or `/today`
|
||||
|
||||
> NOTE: Next you will need to setup a proxy server like [ngrok](https://ngrok.com/) to allow your local host machine to be hosted on a public https server.
|
||||
|
||||
### Obtaining Zoom Client ID and Secret
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ export const InstallAppButtonMap = {
|
|||
office365calendar: dynamic(() => import("./office365calendar/components/InstallAppButton")),
|
||||
office365video: dynamic(() => import("./office365video/components/InstallAppButton")),
|
||||
riverside: dynamic(() => import("./riverside/components/InstallAppButton")),
|
||||
slackmessaging: dynamic(() => import("./slackmessaging/components/InstallAppButton")),
|
||||
tandemvideo: dynamic(() => import("./tandemvideo/components/InstallAppButton")),
|
||||
vital: dynamic(() => import("./vital/components/InstallAppButton")),
|
||||
whereby: dynamic(() => import("./whereby/components/InstallAppButton")),
|
||||
|
|
|
@ -32,7 +32,6 @@ import { metadata as raycast_meta } from "./raycast/_metadata";
|
|||
import { metadata as riverside_meta } from "./riverside/_metadata";
|
||||
import { metadata as sendgridothercalendar_meta } from "./sendgridothercalendar/_metadata";
|
||||
import { metadata as sirius_video_meta } from "./sirius_video/_metadata";
|
||||
import { metadata as slackmessaging_meta } from "./slackmessaging/_metadata";
|
||||
import { metadata as stripepayment_meta } from "./stripepayment/_metadata";
|
||||
import { metadata as tandemvideo_meta } from "./tandemvideo/_metadata";
|
||||
import { metadata as typeform_meta } from "./typeform/_metadata";
|
||||
|
@ -74,7 +73,6 @@ export const appStoreMetadata = {
|
|||
riverside: riverside_meta,
|
||||
sendgridothercalendar: sendgridothercalendar_meta,
|
||||
sirius_video: sirius_video_meta,
|
||||
slackmessaging: slackmessaging_meta,
|
||||
stripepayment: stripepayment_meta,
|
||||
tandemvideo: tandemvideo_meta,
|
||||
typeform: typeform_meta,
|
||||
|
|
|
@ -31,7 +31,6 @@ export const apiHandlers = {
|
|||
riverside: import("./riverside/api"),
|
||||
sendgridothercalendar: import("./sendgridothercalendar/api"),
|
||||
sirius_video: import("./sirius_video/api"),
|
||||
slackmessaging: import("./slackmessaging/api"),
|
||||
stripepayment: import("./stripepayment/api"),
|
||||
tandemvideo: import("./tandemvideo/api"),
|
||||
typeform: import("./typeform/api"),
|
||||
|
|
|
@ -16,7 +16,6 @@ import * as larkcalendar from "./larkcalendar";
|
|||
import * as office365calendar from "./office365calendar";
|
||||
import * as office365video from "./office365video";
|
||||
import * as sendgridothercalendar from "./sendgridothercalendar";
|
||||
import * as slackmessaging from "./slackmessaging";
|
||||
import * as stripepayment from "./stripepayment";
|
||||
import * as tandemvideo from "./tandemvideo";
|
||||
import * as vital from "./vital";
|
||||
|
@ -39,7 +38,6 @@ const appStore = {
|
|||
office365calendar,
|
||||
office365video,
|
||||
sendgridothercalendar,
|
||||
slackmessaging,
|
||||
stripepayment,
|
||||
tandemvideo,
|
||||
vital,
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
items:
|
||||
- /api/app-store/slackmessaging/1.jpg
|
||||
- /api/app-store/slackmessaging/2.jpg
|
||||
- /api/app-store/slackmessaging/3.jpg
|
||||
---
|
||||
|
||||
Slack is a proprietary business communication platform that includes many IRC (internet relay chat) features - these include channels, private groups, direct messaging and more. Users are able to send pictures, and videos as well as even hop on calls with others using the paid version. Slack is available via desktop app, web browser or mobile app. The Cal.com Slack App can be used to display your links, upcoming bookings or to create events. Use it with your team members in a Slack, in a Community Slack or even with external contributors in a Slack Connect Channel.
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import type { AppMeta } from "@calcom/types/App";
|
||||
|
||||
import _package from "./package.json";
|
||||
|
||||
export const metadata = {
|
||||
name: "Slack App",
|
||||
description: _package.description,
|
||||
category: "messaging",
|
||||
imageSrc: "/apps/slack.svg",
|
||||
logo: "/apps/slack.svg",
|
||||
publisher: "Cal.com",
|
||||
rating: 5,
|
||||
reviews: 69,
|
||||
slug: "slack",
|
||||
title: "Slack App",
|
||||
trending: true,
|
||||
// DB has type slack_app. It is an inconsistency
|
||||
type: "slack_messaging",
|
||||
url: "https://slack.com/",
|
||||
variant: "conferencing",
|
||||
verified: true,
|
||||
email: "help@cal.com",
|
||||
} as AppMeta;
|
||||
|
||||
export default metadata;
|
|
@ -1,40 +0,0 @@
|
|||
import type { NextApiRequest } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSlackAppKeys } from "../lib/utils";
|
||||
|
||||
const scopes = ["commands", "users:read", "users:read.email", "chat:write", "chat:write.public"];
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
if (!req.session?.user?.id) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
const { client_id } = await getSlackAppKeys();
|
||||
// Get user
|
||||
await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: req.session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const params = {
|
||||
client_id,
|
||||
scope: scopes.join(","),
|
||||
};
|
||||
const query = stringify(params);
|
||||
const url = `https://slack.com/oauth/v2/authorize?${query}&user_`;
|
||||
// const url =
|
||||
// "https://slack.com/oauth/v2/authorize?client_id=3194129032064.3178385871204&scope=chat:write,commands&user_scope=";
|
||||
return { url };
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
|
@ -1,61 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stringify } from "querystring";
|
||||
import { z } from "zod";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import { getSlackAppKeys } from "../lib/utils";
|
||||
|
||||
const callbackQuerySchema = z.object({
|
||||
code: z.string().min(1),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!req.session?.user?.id) {
|
||||
throw new HttpError({ statusCode: 401, message: "You must be logged in to do this" });
|
||||
}
|
||||
|
||||
// Get user
|
||||
const parsedCallbackQuery = callbackQuerySchema.safeParse(req.query);
|
||||
|
||||
if (!parsedCallbackQuery.success) {
|
||||
return res.redirect("/apps/installed"); // Redirect to where the user was if they cancel the signup or if the oauth fails
|
||||
}
|
||||
|
||||
const { code } = parsedCallbackQuery.data;
|
||||
const { client_id, client_secret } = await getSlackAppKeys();
|
||||
|
||||
const query = {
|
||||
client_secret,
|
||||
client_id,
|
||||
code,
|
||||
};
|
||||
const params = stringify(query);
|
||||
const url = `https://slack.com/api/oauth.v2.access?${params}`;
|
||||
const result = await fetch(url);
|
||||
const responseBody = await result.json();
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: req.session.user.id,
|
||||
},
|
||||
data: {
|
||||
credentials: {
|
||||
create: {
|
||||
type: "slack_app",
|
||||
key: responseBody,
|
||||
appId: "slack",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.redirect(getInstalledAppPath({ variant: "conferencing", slug: "slack" }));
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import { showCreateEventMessage, showTodayMessage } from "../lib";
|
||||
import showLinksMessage from "../lib/showLinksMessage";
|
||||
|
||||
export enum SlackAppCommands {
|
||||
CREATE_EVENT = "create-event",
|
||||
TODAY = "today",
|
||||
LINKS = "links",
|
||||
}
|
||||
|
||||
const commandHandlerBodySchema = z.object({
|
||||
command: z.string().min(1),
|
||||
user_id: z.string(),
|
||||
trigger_id: z.string(),
|
||||
channel_id: z.string().optional(),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const body = commandHandlerBodySchema.parse(req.body);
|
||||
const command = body.command.split("/").pop();
|
||||
switch (command) {
|
||||
case SlackAppCommands.CREATE_EVENT:
|
||||
return await showCreateEventMessage(req, res);
|
||||
case SlackAppCommands.TODAY:
|
||||
return await showTodayMessage(req, res);
|
||||
case SlackAppCommands.LINKS:
|
||||
return await showLinksMessage(req, res);
|
||||
default:
|
||||
return res.status(404).json({ message: `Command not found` });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
POST: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
||||
export { default as commandHandler } from "./commandHandler";
|
||||
export { default as interactiveHandler } from "./interactiveHandler";
|
|
@ -1,21 +0,0 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import createEvent from "../lib/actions/createEvent";
|
||||
|
||||
enum InteractionEvents {
|
||||
CREATE_EVENT = "cal.event.create",
|
||||
}
|
||||
|
||||
export default async function interactiveHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method === "POST") {
|
||||
const payload = JSON.parse(req.body.payload);
|
||||
const actions = payload.view.callback_id;
|
||||
switch (actions) {
|
||||
case InteractionEvents.CREATE_EVENT:
|
||||
await createEvent(req, res);
|
||||
default:
|
||||
return res.status(200).end(); // Techincally an invalid request but we don't want to return an throw an error to slack - 200 just does nothing
|
||||
}
|
||||
}
|
||||
return res.status(200).end(); // Send 200 if we dont have a case for the action_id
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("slack_messaging");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default as InstallAppButton } from "./InstallAppButton";
|
|
@ -1,2 +0,0 @@
|
|||
export * as api from "./api";
|
||||
export { default } from "./_metadata";
|
|
@ -1,9 +0,0 @@
|
|||
export const WhereCredsEqualsId = (userId: string) => ({
|
||||
where: {
|
||||
type: "slack_app",
|
||||
key: {
|
||||
path: ["authed_user", "id"],
|
||||
equals: userId,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,119 +0,0 @@
|
|||
import { WebClient } from "@slack/web-api";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import db from "@calcom/prisma";
|
||||
import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { WhereCredsEqualsId } from "../WhereCredsEqualsID";
|
||||
import { getUserEmail } from "../utils";
|
||||
|
||||
export default async function createEvent(req: NextApiRequest, res: NextApiResponse) {
|
||||
const {
|
||||
user,
|
||||
view: {
|
||||
state: { values },
|
||||
id: view_id,
|
||||
},
|
||||
response_url,
|
||||
} = JSON.parse(req.body.payload);
|
||||
// This is a mess I have no idea why slack makes getting infomation this hard.
|
||||
const {
|
||||
eventName: {
|
||||
event_name: { value: selected_name },
|
||||
},
|
||||
eventType: {
|
||||
"create.event.type": {
|
||||
selected_option: { value: selected_event_id },
|
||||
},
|
||||
},
|
||||
selectedUsers: {
|
||||
invite_users: { selected_users },
|
||||
},
|
||||
eventDate: {
|
||||
event_date: { selected_date },
|
||||
},
|
||||
eventTime: {
|
||||
event_start_time: { selected_time },
|
||||
},
|
||||
} = values;
|
||||
|
||||
// Im sure this query can be made more efficient... The JSON filtering wouldnt work when doing it directly on user.
|
||||
const foundUser = await db.credential
|
||||
.findFirstOrThrow({
|
||||
...WhereCredsEqualsId(user.id),
|
||||
})
|
||||
.user({
|
||||
select: {
|
||||
username: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
eventTypes: {
|
||||
where: {
|
||||
id: parseInt(selected_event_id),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
length: true,
|
||||
locations: true,
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
...WhereCredsEqualsId(user.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const SlackCredentialsSchema = z.object({
|
||||
access_token: z.string(),
|
||||
});
|
||||
|
||||
const slackCredentials = SlackCredentialsSchema.parse(foundUser?.credentials[0].key); // Only one slack credential for user
|
||||
|
||||
const access_token = slackCredentials?.access_token;
|
||||
// https://api.slack.com/authentication/best-practices#verifying since we verify the request is coming from slack we can store the access_token in the DB.
|
||||
const client = new WebClient(access_token);
|
||||
// This could get a bit weird as there is a 3 second limit until the post times ou
|
||||
|
||||
// Compute all users that have been selected and get their email.
|
||||
const invitedGuestsEmails = selected_users.map((userId: string) => getUserEmail(client, userId));
|
||||
|
||||
const startDate = dayjs(`${selected_date} ${selected_time}`, "YYYY-MM-DD HH:mm");
|
||||
|
||||
const PostData: BookingCreateBody = {
|
||||
start: dayjs(startDate).format(),
|
||||
end: dayjs(startDate)
|
||||
.add(foundUser?.eventTypes[0]?.length ?? 0, "minute")
|
||||
.format(),
|
||||
eventTypeId: foundUser?.eventTypes[0]?.id ?? 0,
|
||||
user: foundUser?.username ?? "",
|
||||
email: foundUser?.email ?? "",
|
||||
name: foundUser?.username ?? "",
|
||||
guests: await Promise.all(invitedGuestsEmails),
|
||||
location: DailyLocationType, // Defaulting to daily video to make this a bit more usefull than in-person
|
||||
timeZone: foundUser?.timeZone ?? "",
|
||||
language: foundUser?.locale ?? "en",
|
||||
customInputs: [{ label: "", value: "" }],
|
||||
metadata: {},
|
||||
notes: "This event was created with slack.",
|
||||
};
|
||||
|
||||
const response = await fetch(`${WEBAPP_URL}/api/book/event`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(PostData),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const body = await response.json();
|
||||
client.chat.postMessage({
|
||||
token: access_token,
|
||||
channel: user.id,
|
||||
text: body.errorCode ? `Error: ${body.errorCode}` : "Booking has been created.",
|
||||
});
|
||||
return res.status(200).send("");
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { default as showCreateEventMessage } from "./showCreateEventMessage";
|
||||
export { default as showTodayMessage } from "./showTodayMessage";
|
||||
export * as utils from "./utils";
|
|
@ -1,41 +0,0 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import { WebClient } from "@slack/web-api";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { WhereCredsEqualsId } from "./WhereCredsEqualsID";
|
||||
import slackVerify from "./slackVerify";
|
||||
import { CreateEventModal, NoUserMessage } from "./views";
|
||||
|
||||
export default async function showCreateEventMessage(req: NextApiRequest, res: NextApiResponse) {
|
||||
const body = req.body;
|
||||
await slackVerify(req, res);
|
||||
|
||||
const data = await prisma.credential.findFirst({
|
||||
...WhereCredsEqualsId(body.user_id),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
eventTypes: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) return res.status(200).json(NoUserMessage);
|
||||
const slackCredentials = data?.key; // Only one slack credential for user
|
||||
const access_token = (slackCredentials as Prisma.JsonObject)?.access_token as string;
|
||||
const slackClient = new WebClient(access_token);
|
||||
await slackClient.views.open({
|
||||
trigger_id: body.trigger_id,
|
||||
view: CreateEventModal(data),
|
||||
});
|
||||
return res.status(200).end();
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import { WebClient } from "@slack/web-api";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { WhereCredsEqualsId } from "./WhereCredsEqualsID";
|
||||
import slackVerify from "./slackVerify";
|
||||
import { NoUserMessage } from "./views";
|
||||
import ShowLinks from "./views/ShowLinks";
|
||||
|
||||
export default async function showLinksMessage(req: NextApiRequest, res: NextApiResponse) {
|
||||
const body = req.body;
|
||||
await slackVerify(req, res);
|
||||
|
||||
const data = await prisma.credential.findFirst({
|
||||
...WhereCredsEqualsId(body.user_id),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
eventTypes: {
|
||||
where: {
|
||||
hidden: false,
|
||||
},
|
||||
select: {
|
||||
slug: true,
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) return res.status(200).json(NoUserMessage);
|
||||
const slackCredentials = data?.key; // Only one slack credential for user
|
||||
const access_token = (slackCredentials as Prisma.JsonObject)?.access_token as string;
|
||||
const slackClient = new WebClient(access_token);
|
||||
const blocks = JSON.parse(ShowLinks(data.user?.eventTypes, data.user?.username ?? "")).blocks;
|
||||
|
||||
slackClient.chat.postMessage({
|
||||
channel: body.channel_id,
|
||||
text: `${data.user?.username}'s Cal.com Links`,
|
||||
blocks,
|
||||
});
|
||||
|
||||
return res.status(200).end();
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import { BookingStatus } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { WhereCredsEqualsId } from "./WhereCredsEqualsID";
|
||||
import slackVerify from "./slackVerify";
|
||||
import { NoUserMessage, TodayMessage } from "./views";
|
||||
|
||||
export default async function showCreateEventMessage(req: NextApiRequest, res: NextApiResponse) {
|
||||
const body = req.body;
|
||||
await slackVerify(req, res);
|
||||
const foundUser = await prisma.credential.findFirst({
|
||||
...WhereCredsEqualsId(body.user_id),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!foundUser) res.status(200).json(NoUserMessage);
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
userId: foundUser?.userId,
|
||||
},
|
||||
{
|
||||
attendees: {
|
||||
some: {
|
||||
email: foundUser?.user?.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
AND: [
|
||||
{
|
||||
endTime: { gte: dayjs().startOf("day").toDate(), lte: dayjs().endOf("day").toDate() },
|
||||
AND: [
|
||||
{ NOT: { status: { equals: BookingStatus.CANCELLED } } },
|
||||
{ NOT: { status: { equals: BookingStatus.REJECTED } } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return res.status(200).json(TodayMessage(bookings));
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { createHmac } from "crypto";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import { getSlackAppKeys } from "./utils";
|
||||
|
||||
export default async function slackVerify(req: NextApiRequest, res: NextApiResponse) {
|
||||
const timeStamp = req.headers["x-slack-request-timestamp"] as string; // Always returns a string and not a string[]
|
||||
const slackSignature = req.headers["x-slack-signature"] as string;
|
||||
const currentTime = dayjs().unix();
|
||||
const { signing_secret: signingSecret } = await getSlackAppKeys();
|
||||
const [version, hash] = slackSignature.split("=");
|
||||
|
||||
if (!timeStamp) {
|
||||
return res.status(400).json({ message: "Missing X-Slack-Request-Timestamp header" });
|
||||
}
|
||||
|
||||
if (!signingSecret) {
|
||||
return res.status(400).json({ message: "Missing Slack's signing_secret" });
|
||||
}
|
||||
|
||||
if (Math.abs(currentTime - parseInt(timeStamp)) > 60 * 5) {
|
||||
return res.status(400).json({ message: "Request is too old" });
|
||||
}
|
||||
|
||||
const hmac = createHmac("sha256", signingSecret);
|
||||
|
||||
hmac.update(`${version}:${timeStamp}:${stringify(req.body)}`);
|
||||
|
||||
const signed_sig = hmac.digest("hex");
|
||||
console.log({ signed_sig, hash, match: signed_sig === hash });
|
||||
if (signed_sig !== hash) {
|
||||
throw new Error("Hashes do not match ");
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { WebClient } from "@slack/web-api";
|
||||
import { z } from "zod";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
|
||||
export const getUserEmail = async (client: WebClient, userId: string) =>
|
||||
(await client.users.info({ user: userId })).user?.profile?.email;
|
||||
|
||||
const slackAppKeysSchema = z.object({
|
||||
client_id: z.string(),
|
||||
client_secret: z.string(),
|
||||
signing_secret: z.string(),
|
||||
});
|
||||
|
||||
export const getSlackAppKeys = async () => {
|
||||
const appKeys = await getAppKeysFromSlug("slack");
|
||||
return slackAppKeysSchema.parse(appKeys);
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
import { Blocks, Message } from "slack-block-builder";
|
||||
|
||||
const BookingSuccess = () => {
|
||||
return Message()
|
||||
.blocks(Blocks.Section({ text: `Your booking has been created!` }))
|
||||
.buildToObject();
|
||||
};
|
||||
|
||||
export default BookingSuccess;
|
|
@ -1,53 +0,0 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
import { Bits, Blocks, Elements, Modal, setIfTruthy } from "slack-block-builder";
|
||||
|
||||
const CreateEventModal = (
|
||||
data:
|
||||
| (Credential & {
|
||||
user: {
|
||||
username: string | null;
|
||||
eventTypes: {
|
||||
id: number;
|
||||
title: string;
|
||||
}[];
|
||||
} | null;
|
||||
})
|
||||
| null,
|
||||
invalidInput = false
|
||||
) => {
|
||||
return Modal({ title: "Create Booking", submit: "Create", callbackId: "cal.event.create" })
|
||||
.blocks(
|
||||
Blocks.Section({ text: `Hey there, *${data?.user?.username}!*` }),
|
||||
Blocks.Divider(),
|
||||
Blocks.Input({ label: "Your Name", blockId: "eventName" }).element(
|
||||
Elements.TextInput({ placeholder: "Event Name" }).actionId("event_name")
|
||||
),
|
||||
Blocks.Input({ label: "Which event would you like to create?", blockId: "eventType" }).element(
|
||||
Elements.StaticSelect({ placeholder: "Which event would you like to create?" })
|
||||
.actionId("create.event.type")
|
||||
.options(
|
||||
data?.user?.eventTypes.map((item: any) =>
|
||||
Bits.Option({ text: item.title ?? "No Name", value: item.id.toString() })
|
||||
)
|
||||
)
|
||||
), // This doesnt need to reach out to the server when the user changes the selection
|
||||
Blocks.Input({
|
||||
label: "Who would you like to invite to your event?",
|
||||
blockId: "selectedUsers",
|
||||
}).element(
|
||||
Elements.UserMultiSelect({ placeholder: "Who would you like to invite to your event?" }).actionId(
|
||||
"invite_users"
|
||||
)
|
||||
),
|
||||
Blocks.Input({ label: "When would this event be?", blockId: "eventDate" }).element(
|
||||
Elements.DatePicker({ placeholder: "Select Date" }).actionId("event_date")
|
||||
),
|
||||
Blocks.Input({ label: "What time would you like to start?", blockId: "eventTime" }).element(
|
||||
Elements.TimePicker({ placeholder: "Select Time" }).actionId("event_start_time")
|
||||
), // TODO: We could in future validate if the time is in the future or if busy at point - Didnt see much point as this gets validated when you submit. Could be better UX
|
||||
setIfTruthy(invalidInput, [Blocks.Section({ text: "Please fill in all the fields" })])
|
||||
)
|
||||
.buildToObject();
|
||||
};
|
||||
|
||||
export default CreateEventModal;
|
|
@ -1,20 +0,0 @@
|
|||
import { Message, Blocks, Elements } from "slack-block-builder";
|
||||
|
||||
import { BASE_URL } from "@calcom/lib/constants";
|
||||
|
||||
const NoUserMessage = () => {
|
||||
return Message()
|
||||
.blocks(
|
||||
Blocks.Section({ text: "This slack account is not linked with a cal.com account" }),
|
||||
Blocks.Actions().elements(
|
||||
Elements.Button({ text: "Cancel", actionId: "cancel" }).danger(),
|
||||
Elements.Button({
|
||||
text: "Connect",
|
||||
actionId: "open.connect.link",
|
||||
url: `${BASE_URL}/apps/installed`,
|
||||
}).primary()
|
||||
)
|
||||
)
|
||||
.buildToJSON();
|
||||
};
|
||||
export default NoUserMessage;
|
|
@ -1,31 +0,0 @@
|
|||
import { Blocks, Elements, Message } from "slack-block-builder";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
interface IEventTypes {
|
||||
slug: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ShowLinks = (eventLinks: IEventTypes[] | undefined, username: string) => {
|
||||
if (eventLinks?.length === 0 || !eventLinks) {
|
||||
return Message()
|
||||
.blocks(Blocks.Section({ text: "You do not have any links." }))
|
||||
.asUser()
|
||||
.buildToJSON();
|
||||
}
|
||||
return Message()
|
||||
.blocks(
|
||||
Blocks.Section({ text: `${username}'s Cal.com Links` }),
|
||||
Blocks.Divider(),
|
||||
eventLinks.map((links) =>
|
||||
Blocks.Section({
|
||||
text: `${links.title} | ${WEBAPP_URL}/${username}/${links.slug}`,
|
||||
}).accessory(Elements.Button({ text: "Open", url: `${WEBAPP_URL}/${username}/${links.slug}` }))
|
||||
)
|
||||
)
|
||||
.buildToJSON();
|
||||
};
|
||||
|
||||
export default ShowLinks;
|
|
@ -1,27 +0,0 @@
|
|||
import { Booking } from "@prisma/client";
|
||||
import { Blocks, Elements, Message } from "slack-block-builder";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
const TodayMessage = (bookings: Booking[]) => {
|
||||
if (bookings.length === 0) {
|
||||
return Message()
|
||||
.blocks(Blocks.Section({ text: "You do not have any bookings for today." }))
|
||||
.asUser()
|
||||
.buildToObject();
|
||||
}
|
||||
return Message()
|
||||
.blocks(
|
||||
Blocks.Section({ text: `Todays Bookings.` }),
|
||||
Blocks.Divider(),
|
||||
bookings.map((booking) =>
|
||||
Blocks.Section({
|
||||
text: `${booking.title} | ${dayjs(booking.startTime).format("HH:mm")}`,
|
||||
}).accessory(Elements.Button({ text: "Cancel", url: `${WEBAPP_URL}/cancel/${booking.uid}` }))
|
||||
)
|
||||
)
|
||||
.buildToObject();
|
||||
};
|
||||
|
||||
export default TodayMessage;
|
|
@ -1,3 +0,0 @@
|
|||
export { default as CreateEventModal } from "./CreateEventModal";
|
||||
export { default as TodayMessage } from "./TodayMessage";
|
||||
export { default as NoUserMessage } from "./NoUser";
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/slackmessaging",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"description": "A business communication platform that includes persistent chat rooms (channels), private groups and direct messaging.",
|
||||
"dependencies": {
|
||||
"@calcom/prisma": "*",
|
||||
"@slack/web-api": "^6.7.2",
|
||||
"slack-block-builder": "^2.6.0",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 203 KiB |
Binary file not shown.
Before Width: | Height: | Size: 278 KiB |
Binary file not shown.
Before Width: | Height: | Size: 132 KiB |
|
@ -1,31 +0,0 @@
|
|||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 270 270" style="enable-background:new 0 0 270 270;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#E01E5A;}
|
||||
.st1{fill:#36C5F0;}
|
||||
.st2{fill:#2EB67D;}
|
||||
.st3{fill:#ECB22E;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M99.4,151.2c0,7.1-5.8,12.9-12.9,12.9c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9h12.9V151.2z"/>
|
||||
<path class="st0" d="M105.9,151.2c0-7.1,5.8-12.9,12.9-12.9s12.9,5.8,12.9,12.9v32.3c0,7.1-5.8,12.9-12.9,12.9
|
||||
s-12.9-5.8-12.9-12.9V151.2z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M118.8,99.4c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9s12.9,5.8,12.9,12.9v12.9H118.8z"/>
|
||||
<path class="st1" d="M118.8,105.9c7.1,0,12.9,5.8,12.9,12.9s-5.8,12.9-12.9,12.9H86.5c-7.1,0-12.9-5.8-12.9-12.9
|
||||
s5.8-12.9,12.9-12.9H118.8z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st2" d="M170.6,118.8c0-7.1,5.8-12.9,12.9-12.9c7.1,0,12.9,5.8,12.9,12.9s-5.8,12.9-12.9,12.9h-12.9V118.8z"/>
|
||||
<path class="st2" d="M164.1,118.8c0,7.1-5.8,12.9-12.9,12.9c-7.1,0-12.9-5.8-12.9-12.9V86.5c0-7.1,5.8-12.9,12.9-12.9
|
||||
c7.1,0,12.9,5.8,12.9,12.9V118.8z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st3" d="M151.2,170.6c7.1,0,12.9,5.8,12.9,12.9c0,7.1-5.8,12.9-12.9,12.9c-7.1,0-12.9-5.8-12.9-12.9v-12.9H151.2z"/>
|
||||
<path class="st3" d="M151.2,164.1c-7.1,0-12.9-5.8-12.9-12.9c0-7.1,5.8-12.9,12.9-12.9h32.3c7.1,0,12.9,5.8,12.9,12.9
|
||||
c0,7.1-5.8,12.9-12.9,12.9H151.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -266,14 +266,7 @@ export default async function main() {
|
|||
|
||||
// Web3 apps
|
||||
await createApp("huddle01", "huddle01video", ["web3", "video"], "huddle01_video");
|
||||
// Messaging apps
|
||||
if (process.env.SLACK_CLIENT_ID && process.env.SLACK_CLIENT_SECRET && process.env.SLACK_SIGNING_SECRET) {
|
||||
await createApp("slack", "slackmessaging", ["messaging"], "slack_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 &&
|
||||
|
|
Loading…
Reference in New Issue