Files
calendar/packages/features/tasker/tasks/scanWorkflowBody.ts
T
2025-07-15 17:45:20 +00:00

207 lines
5.6 KiB
TypeScript

import z from "zod";
import { getTemplateBodyForAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import compareReminderBodyToTemplate from "@calcom/features/ee/workflows/lib/compareReminderBodyToTemplate";
import { Task } from "@calcom/features/tasker/repository";
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(),
// deprecated: use workflowStepId instead
workflowStepIds: z.array(z.number()).optional(),
workflowStepId: z.number().optional(),
createdAt: z.string().optional(),
});
const log = logger.getSubLogger({ prefix: ["[tasker] scanWorkflowBody"] });
export async function scanWorkflowBody(payload: string) {
const { workflowStepIds, userId, createdAt, workflowStepId } = scanWorkflowBodySchema.parse(
JSON.parse(payload)
);
const stepIdsToScan: number[] = workflowStepIds ? workflowStepIds : [];
if (workflowStepId && !workflowStepIds) {
if (createdAt) {
const hasNewerTask = await Task.hasNewerScanTaskForStepId(workflowStepId, createdAt);
if (hasNewerTask) return;
stepIdsToScan.push(workflowStepId);
} else {
stepIdsToScan.push(workflowStepId);
}
}
if (stepIdsToScan.length === 0) return;
const workflowSteps = await prisma.workflowStep.findMany({
where: {
id: {
in: stepIdsToScan,
},
},
include: {
workflow: {
select: {
user: {
select: {
locale: true,
timeFormat: true,
whitelistWorkflows: true,
},
},
},
},
},
});
if (process.env.IFFY_API_KEY) {
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 isSpam = await iffyScanBody(workflowStep.reminderBody, workflowStep.id);
if (isSpam) {
if (!workflowStep.workflow.user?.whitelistWorkflows) {
// 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}`);
// Return early if spam is detected
return;
}
log.warn(`For whitelisted user, workflow step ${workflowStep.id} marked as spam`);
}
await prisma.workflowStep.update({
where: {
id: workflowStep.id,
},
data: {
verifiedAt: new Date(),
},
});
}
}
if (!process.env.IFFY_API_KEY) {
log.info("IFFY_API_KEY not set, skipping scan");
await prisma.workflowStep.updateMany({
where: {
id: {
in: stepIdsToScan,
},
},
data: {
verifiedAt: new Date(),
},
});
}
const workflow = await prisma.workflow.findFirst({
where: {
steps: {
some: {
id: {
in: stepIdsToScan,
},
},
},
},
include: {
activeOn: true,
team: true,
},
});
if (!workflow) {
log.warn(`Workflow with steps ${stepIdsToScan} not found`);
return;
}
const isOrg = !!workflow?.team?.isOrganization;
await scheduleWorkflowNotifications({
activeOn: workflow.activeOn.map((activeOn) => activeOn.eventTypeId) ?? [],
isOrg,
workflowSteps: workflowSteps.map((step) => ({
...step,
verifiedAt: new Date(),
})),
time: workflow.time,
timeUnit: workflow.timeUnit,
trigger: workflow.trigger,
userId,
teamId: workflow.team?.id || null,
});
}
export const iffyScanBody = async (body: string, workflowStepId: number) => {
try {
const response = await fetch("https://api.iffy.com/api/v1/moderate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.IFFY_API_KEY}`,
},
body: JSON.stringify({
clientId: `Workflow step - ${workflowStepId}`,
name: "Workflow",
entity: "WorkflowBody",
content: body,
passthrough: true,
}),
});
const data = await response.json();
return data.flagged;
} catch (error) {
log.error(`Error scanning workflow body for workflow step ${workflowStepId}:`, error);
}
};