Files
calendar/packages/features/tasker/tasks/scanWorkflowBody.ts
T
a82d616b79 feat: Whitelist user's from workflow spam scanning (#20717)
* Add whitelistWorkflows to user

* Do not autolock whitelisted users

* Add endpoint to whitelist a user

* Get users data - include whitelistWorkflows

* Whitelist a user on the user admin page

* Fix typo

* Add test

* Use translations

* Add `updateWhitelistWorkflows` to `UserRepository`

* Use i18 for strings

* Rename to whitelistWorkflows

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-04-16 18:09:29 -04:00

163 lines
4.4 KiB
TypeScript

import { AkismetClient } from "akismet-api";
import type { Comment } from "akismet-api";
import z from "zod";
import { getTemplateBodyForAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import compareReminderBodyToTemplate from "@calcom/features/ee/workflows/lib/compareReminderBodyToTemplate";
import { lockUser, LockReason } from "@calcom/lib/autoLock";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewer/workflows/util";
export const scanWorkflowBodySchema = z.object({
userId: z.number(),
workflowStepIds: z.array(z.number()),
});
const log = logger.getSubLogger({ prefix: ["[tasker] scanWorkflowBody"] });
export async function scanWorkflowBody(payload: string) {
if (!process.env.AKISMET_API_KEY) {
log.info("AKISMET_API_KEY not set, skipping scan");
return;
}
const { workflowStepIds, userId } = scanWorkflowBodySchema.parse(JSON.parse(payload));
const workflowSteps = await prisma.workflowStep.findMany({
where: {
id: {
in: workflowStepIds,
},
},
include: {
workflow: {
select: {
user: {
select: {
locale: true,
timeFormat: true,
whitelistWorkflows: true,
},
},
},
},
},
});
const client = new AkismetClient({ key: process.env.AKISMET_API_KEY, blog: WEBAPP_URL });
for (const workflowStep of workflowSteps) {
if (!workflowStep.reminderBody) {
await prisma.workflowStep.update({
where: {
id: workflowStep.id,
},
data: {
verifiedAt: new Date(),
},
});
continue;
}
const timeFormat = getTimeFormatStringFromUserTimeFormat(workflowStep.workflow.user?.timeFormat);
// Determine if body is a template
const defaultTemplate = getTemplateBodyForAction({
action: workflowStep.action,
locale: workflowStep.workflow.user?.locale ?? "en",
t: await getTranslation(workflowStep.workflow.user?.locale ?? "en", "common"),
template: workflowStep.template,
timeFormat,
});
if (!defaultTemplate) {
log.error(`Template not found for action ${workflowStep.action}, template ${workflowStep.template}`);
continue;
}
if (
compareReminderBodyToTemplate({ reminderBody: workflowStep.reminderBody, template: defaultTemplate })
) {
await prisma.workflowStep.update({
where: {
id: workflowStep.id,
},
data: {
verifiedAt: new Date(),
},
});
continue;
}
const comment: Comment = {
user_ip: "127.0.0.1",
content: workflowStep.reminderBody,
};
const isSpam = await client.checkSpam(comment);
if (isSpam) {
if (workflowStep.workflow.user?.whitelistWorkflows) {
log.warn(
`For whitelisted user, workflow step ${workflowStep.id} is spam with body ${workflowStep.reminderBody}`
);
return;
}
// We won't delete the workflow step incase it is flagged as a false positive
log.warn(`Workflow step ${workflowStep.id} is spam with body ${workflowStep.reminderBody}`);
await lockUser("userId", userId.toString(), LockReason.SPAM_WORKFLOW_BODY);
// Return early if spam is detected
return;
} else {
await prisma.workflowStep.update({
where: {
id: workflowStep.id,
},
data: {
verifiedAt: new Date(),
},
});
}
}
const workflow = await prisma.workflow.findFirst({
where: {
steps: {
some: {
id: {
in: workflowStepIds,
},
},
},
},
include: {
activeOn: true,
team: true,
},
});
if (!workflow) {
log.warn(`Workflow with steps ${workflowStepIds} not found`);
return;
}
const isOrg = !!workflow?.team?.isOrganization;
await scheduleWorkflowNotifications({
activeOn: workflow.activeOn.map((activeOn) => activeOn.eventTypeId) ?? [],
isOrg,
workflowSteps,
time: workflow.time,
timeUnit: workflow.timeUnit,
trigger: workflow.trigger,
userId,
teamId: workflow.team?.id || null,
});
}