feat: slack channel suggestions

This commit is contained in:
Sheen Capadngan
2024-09-04 18:03:07 +08:00
parent 15c5fe4095
commit 71b8c59050
10 changed files with 284 additions and 78 deletions
+11 -9
View File
@@ -576,16 +576,18 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
projectId: req.params.workspaceId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_SLACK_CONFIG,
metadata: {
id: slackConfig.id
if (slackConfig) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.GET_PROJECT_SLACK_CONFIG,
metadata: {
id: slackConfig.id
}
}
}
});
});
}
return slackConfig;
}
@@ -232,6 +232,44 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/:slackIntegrationId/channels",
config: {
rateLimit: readLimit
},
schema: {
security: [
{
bearerAuth: []
}
],
params: z.object({
slackIntegrationId: z.string()
}),
response: {
200: z
.object({
name: z.string(),
id: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const slackChannels = await server.services.slack.getSlackIntegrationChannels({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.slackIntegrationId
});
return slackChannels;
}
});
server.route({
method: "PATCH",
url: "/:slackIntegrationId",
+46 -32
View File
@@ -1,11 +1,44 @@
import { Block, WebClient } from "@slack/web-api";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
import { SlackTriggerFeature } from "./slack-types";
export const fetchSlackChannels = async (botKey: string) => {
const slackChannels: {
name: string;
id: string;
}[] = [];
const slackWebClient = new WebClient(botKey);
let cursor;
do {
// eslint-disable-next-line no-await-in-loop
const response = await slackWebClient.conversations.list({
cursor,
limit: 1000,
types: "public_channel,private_channel"
});
response.channels?.forEach((channel) =>
slackChannels.push({
name: channel.name_normalized as string,
id: channel.id as string
})
);
// Set the cursor for the next page
cursor = response.response_metadata?.next_cursor;
} while (cursor); // Continue while there is a cursor
return slackChannels;
};
export const triggerSlackNotification = async ({
projectId,
payloadBlocks,
@@ -30,15 +63,15 @@ export const triggerSlackNotification = async ({
return;
}
let targetChannels: string[] = [];
let targetChannelIds: string[] = [];
if (feature === SlackTriggerFeature.ACCESS_REQUEST) {
targetChannels = slackIntegration.accessRequestChannels?.split(", ") || [];
if (!targetChannels.length || !slackIntegration.isAccessRequestNotificationEnabled) {
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
return;
}
} else if (feature === SlackTriggerFeature.SECRET_APPROVAL) {
targetChannels = slackIntegration.secretRequestChannels?.split(", ") || [];
if (!targetChannels.length || !slackIntegration.isSecretRequestNotificationEnabled) {
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
return;
}
}
@@ -52,35 +85,16 @@ export const triggerSlackNotification = async ({
cipherTextBlob: slackIntegration.encryptedBotAccessToken
}).toString("utf8");
const targetChannelSet = new Set<string>(targetChannels);
const slackWebClient = new WebClient(botKey);
const channelIdsToSendNotif: string[] = [];
let cursor;
do {
// eslint-disable-next-line no-await-in-loop
const response = await slackWebClient.conversations.list({
cursor,
limit: 1000,
types: "public_channel,private_channel"
});
response.channels?.forEach((channel) => {
if (channel.name_normalized && targetChannelSet.has(channel.name_normalized)) {
channelIdsToSendNotif.push(channel.id as string);
}
});
// Set the cursor for the next page
cursor = response.response_metadata?.next_cursor;
} while (cursor); // Continue while there is a cursor
for await (const conversationId of channelIdsToSendNotif) {
for await (const conversationId of targetChannelIds) {
// we send both text and blocks for compatibility with barebone clients
await slackWebClient.chat.postMessage({
channel: conversationId,
text: payloadMessage,
blocks: payloadBlocks
});
await slackWebClient.chat
.postMessage({
channel: conversationId,
text: payloadMessage,
blocks: payloadBlocks
})
.catch((err) => void logger.error(err));
}
};
+40 -1
View File
@@ -8,6 +8,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { fetchSlackChannels } from "./slack-fns";
import { TSlackIntegrationDALFactory } from "./slack-integration-dal";
import {
TCompleteSlackIntegrationDTO,
@@ -16,6 +17,7 @@ import {
TGetSlackInstallUrlDTO,
TGetSlackIntegrationByIdDTO,
TGetSlackIntegrationByOrgDTO,
TGetSlackIntegrationChannelsDTO,
TReinstallSlackIntegrationDTO,
TUpdateSlackIntegrationDTO
} from "./slack-types";
@@ -285,6 +287,42 @@ export const slackServiceFactory = ({
return slackIntegration;
};
const getSlackIntegrationChannels = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id
}: TGetSlackIntegrationChannelsDTO) => {
const slackIntegration = await slackIntegrationDAL.findById(id);
if (!slackIntegration) {
throw new NotFoundError({
message: "Slack integration not found."
});
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
slackIntegration.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
orgId: slackIntegration.orgId,
type: KmsDataKey.Organization
});
const botKey = orgDataKeyDecryptor({
cipherTextBlob: slackIntegration.encryptedBotAccessToken
}).toString("utf8");
return fetchSlackChannels(botKey);
};
const updateSlackIntegration = async ({
actorId,
actor,
@@ -352,6 +390,7 @@ export const slackServiceFactory = ({
completeSlackIntegration,
getSlackInstaller,
updateSlackIntegration,
deleteSlackIntegration
deleteSlackIntegration,
getSlackIntegrationChannels
};
};
@@ -13,6 +13,8 @@ export type TGetSlackIntegrationByOrgDTO = Omit<TOrgPermission, "orgId">;
export type TGetSlackIntegrationByIdDTO = { id: string } & Omit<TOrgPermission, "orgId">;
export type TGetSlackIntegrationChannelsDTO = { id: string } & Omit<TOrgPermission, "orgId">;
export type TUpdateSlackIntegrationDTO = { id: string; slug?: string; description?: string } & Omit<
TOrgPermission,
"orgId"