Files
calendar/packages/features/tasker/tasks/scanWorkflowBody.ts
T

253 lines
6.9 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 { scheduleWorkflowNotifications } from "@calcom/features/ee/workflows/lib/scheduleWorkflowNotifications";
import { Task } from "@calcom/features/tasker/repository";
import { URL_SCANNING_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/i18n/server";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { submitWorkflowStepForUrlScanning } from "./scanWorkflowUrls";
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"] });
/**
* Helper function to handle URL scanning or verification for a workflow step.
* Extracts the duplicated logic from both Iffy-enabled and Iffy-disabled paths.
*/
async function handleUrlScanningForStep(
workflowStep: {
id: number;
reminderBody: string | null;
workflow: { user: { whitelistWorkflows: boolean } | null };
},
userId: number
): Promise<void> {
if (URL_SCANNING_ENABLED && workflowStep.reminderBody) {
await submitWorkflowStepForUrlScanning(
workflowStep.id,
workflowStep.reminderBody,
userId,
workflowStep.workflow.user?.whitelistWorkflows ?? false
);
} else {
await prisma.workflowStep.update({
where: { id: workflowStep.id },
data: { verifiedAt: new Date() },
});
}
}
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`);
}
// Handle URL scanning or mark as verified
await handleUrlScanningForStep(workflowStep, userId);
}
}
if (!process.env.IFFY_API_KEY) {
log.info("IFFY_API_KEY not set, skipping Iffy spam scan");
if (!URL_SCANNING_ENABLED) {
await prisma.workflowStep.updateMany({
where: {
id: {
in: stepIdsToScan,
},
},
data: {
verifiedAt: new Date(),
},
});
} else {
for (const workflowStep of workflowSteps) {
await handleUrlScanningForStep(workflowStep, userId);
}
}
}
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;
const updatedWorkflowSteps = await prisma.workflowStep.findMany({
where: {
id: {
in: stepIdsToScan,
},
},
select: {
id: true,
action: true,
sendTo: true,
emailSubject: true,
reminderBody: true,
template: true,
sender: true,
verifiedAt: true,
},
});
await scheduleWorkflowNotifications({
activeOn: workflow.activeOn.map((activeOn) => activeOn.eventTypeId) ?? [],
isOrg,
workflowSteps: updatedWorkflowSteps,
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);
}
};