fix: canceling workflow reminders (#21416)

* fix deleting reminders

* schedule reminders if iffy is disabled

* fix unit test

* clean up code

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Carina Wollendorfer
2025-05-21 12:35:31 +00:00
committed by GitHub
co-authored by CarinaWolli
parent 21a4711f07
commit 4ff366cd99
3 changed files with 89 additions and 79 deletions
@@ -5,7 +5,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
import { lockUser, LockReason } from "@calcom/lib/autoLock";
import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewer/workflows/util";
import { scanWorkflowBody } from "./scanWorkflowBody";
import { scanWorkflowBody, iffyScanBody } from "./scanWorkflowBody";
vi.mock("@calcom/lib/autoLock", async (importActual) => {
const actual = await importActual<typeof import("@calcom/lib/autoLock")>();
@@ -19,6 +19,14 @@ vi.mock("@calcom/trpc/server/routers/viewer/workflows/util", () => ({
scheduleWorkflowNotifications: vi.fn(),
}));
vi.mock("./scanWorkflowBody", async (importActual) => {
const actual = await importActual<typeof import("./scanWorkflowBody")>();
return {
...actual,
iffyScanBody: vi.fn(),
};
});
const mockWorkflowStep = {
id: 1,
reminderBody: "Test reminder body",
@@ -58,7 +66,7 @@ describe("scanWorkflowBody", () => {
await scanWorkflowBody(payload);
expect(prismaMock.workflowStep.findMany).not.toHaveBeenCalled();
expect(iffyScanBody).not.toHaveBeenCalled();
});
it("should mark workflow step as safe if no reminder body", async () => {
@@ -19,21 +19,6 @@ const log = logger.getSubLogger({ prefix: ["[tasker] scanWorkflowBody"] });
export async function scanWorkflowBody(payload: string) {
const { workflowStepIds, userId } = scanWorkflowBodySchema.parse(JSON.parse(payload));
if (!process.env.IFFY_API_KEY) {
log.info("IFFY_API_KEY not set, skipping scan");
await prisma.workflowStep.updateMany({
where: {
id: {
in: workflowStepIds,
},
},
data: {
verifiedAt: new Date(),
},
});
return;
}
const workflowSteps = await prisma.workflowStep.findMany({
where: {
id: {
@@ -55,68 +40,84 @@ export async function scanWorkflowBody(payload: string) {
},
});
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}`);
await lockUser("userId", userId.toString(), LockReason.SPAM_WORKFLOW_BODY);
// Return early if spam is detected
return;
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;
}
log.warn(
`For whitelisted user, workflow step ${workflowStep.id} is spam with body ${workflowStep.reminderBody}`
);
}
await prisma.workflowStep.update({
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}`);
await lockUser("userId", userId.toString(), LockReason.SPAM_WORKFLOW_BODY);
// Return early if spam is detected
return;
}
log.warn(
`For whitelisted user, workflow step ${workflowStep.id} is spam with body ${workflowStep.reminderBody}`
);
}
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: workflowStep.id,
id: {
in: workflowStepIds,
},
},
data: {
verifiedAt: new Date(),
@@ -159,7 +160,7 @@ export async function scanWorkflowBody(payload: string) {
});
}
const iffyScanBody = async (body: string, workflowStepId: number) => {
export const iffyScanBody = async (body: string, workflowStepId: number) => {
try {
const response = await fetch("https://api.iffy.com/api/v1/moderate", {
method: "POST",
@@ -164,8 +164,9 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
const remindersToDelete = await prisma.workflowReminder.findMany({
where: {
booking: {
eventTypeId: eventTypeId,
userId: ctx.user.id,
eventTypeId: {
in: Array.from(activeOnEventTypes.keys()),
},
},
workflowStepId: {
in: eventTypeWorkflow.steps.map((step) => {