Skip to content

Commit 5017365

Browse files
authored
refactor: Use Iffy for workflow body scanning (calcom#21170)
* Add Iffy API key variable * WIP using Iffy to scan comments * Use Iffy for workflow body scanning * Update entity * Clean up * Update test * Fix client id * Fix test
1 parent dddbd7d commit 5017365

6 files changed

Lines changed: 88 additions & 45 deletions

File tree

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ DIRECTORY_IDS_TO_LOG=
425425
# Read more about it in the README.md
426426
NEXT_PUBLIC_SINGLE_ORG_SLUG=
427427

428-
AKISMET_API_KEY=
428+
IFFY_API_KEY=
429429

430430
## Env variables related to avoiding booking failures
431431
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time

packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
"use client"
1+
"use client";
2+
23
import { useState } from "react";
34

45
import { useLocale } from "@calcom/lib/hooks/useLocale";

packages/features/tasker/tasks/scanWorkflowBody.test.ts

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,6 @@ import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewe
77

88
import { scanWorkflowBody } from "./scanWorkflowBody";
99

10-
const mockAkismetCheckSpam = vi.fn();
11-
12-
// Mock the entire module
13-
vi.mock("akismet-api", () => {
14-
return {
15-
AkismetClient: class {
16-
constructor() {
17-
return {
18-
checkSpam: mockAkismetCheckSpam,
19-
};
20-
}
21-
},
22-
};
23-
});
24-
2510
vi.mock("@calcom/lib/autoLock", async (importActual) => {
2611
const actual = await importActual<typeof import("@calcom/lib/autoLock")>();
2712
return {
@@ -54,15 +39,18 @@ const mockWorkflow = {
5439
};
5540

5641
describe("scanWorkflowBody", () => {
42+
const mockFetch = vi.fn();
43+
5744
beforeEach(() => {
5845
vi.resetAllMocks();
59-
process.env.AKISMET_API_KEY = "test-key";
46+
vi.stubGlobal("fetch", mockFetch);
47+
process.env.IFFY_API_KEY = "test-key";
6048
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
6149
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
6250
});
6351

64-
it("should skip scan if AKISMET_API_KEY is not set", async () => {
65-
process.env.AKISMET_API_KEY = "";
52+
it("should skip scan if IFFY_API_KEY is not set", async () => {
53+
process.env.IFFY_API_KEY = "";
6654
const payload = JSON.stringify({
6755
userId: 1,
6856
workflowStepIds: [1],
@@ -98,13 +86,25 @@ describe("scanWorkflowBody", () => {
9886

9987
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
10088
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
101-
mockAkismetCheckSpam.mockResolvedValue(false);
89+
mockFetch.mockResolvedValue({
90+
json: () => Promise.resolve({ flagged: false }),
91+
});
10292

10393
await scanWorkflowBody(payload);
10494

105-
expect(mockAkismetCheckSpam).toHaveBeenCalledWith({
106-
user_ip: "127.0.0.1",
107-
content: mockWorkflowStep.reminderBody,
95+
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
96+
method: "POST",
97+
headers: {
98+
"Content-Type": "application/json",
99+
Authorization: `Bearer test-key`,
100+
},
101+
body: JSON.stringify({
102+
clientId: "Workflow step - 1",
103+
name: "Workflow",
104+
entity: "WorkflowBody",
105+
content: "Test reminder body",
106+
passthrough: true,
107+
}),
108108
});
109109
expect(prismaMock.workflowStep.update).toHaveBeenCalledWith({
110110
where: { id: 1 },
@@ -119,11 +119,26 @@ describe("scanWorkflowBody", () => {
119119
});
120120

121121
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
122-
mockAkismetCheckSpam.mockResolvedValue(true);
122+
mockFetch.mockResolvedValue({
123+
json: () => Promise.resolve({ flagged: true }),
124+
});
123125

124126
await scanWorkflowBody(payload);
125127

126-
expect(mockAkismetCheckSpam).toHaveBeenCalled();
128+
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
129+
method: "POST",
130+
headers: {
131+
"Content-Type": "application/json",
132+
Authorization: `Bearer test-key`,
133+
},
134+
body: JSON.stringify({
135+
clientId: "Workflow step - 1",
136+
name: "Workflow",
137+
entity: "WorkflowBody",
138+
content: "Test reminder body",
139+
passthrough: true,
140+
}),
141+
});
127142
expect(prismaMock.workflowStep.update).not.toHaveBeenCalled();
128143
expect(lockUser).toHaveBeenCalledWith("userId", "1", LockReason.SPAM_WORKFLOW_BODY);
129144
});
@@ -136,7 +151,6 @@ describe("scanWorkflowBody", () => {
136151

137152
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
138153
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
139-
mockAkismetCheckSpam.mockResolvedValue(false);
140154

141155
await scanWorkflowBody(payload);
142156

@@ -166,7 +180,6 @@ describe("scanWorkflowBody", () => {
166180

167181
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
168182
prismaMock.workflow.findFirst.mockResolvedValue(null);
169-
mockAkismetCheckSpam.mockResolvedValue(false);
170183

171184
await scanWorkflowBody(payload);
172185

@@ -183,11 +196,26 @@ describe("scanWorkflowBody", () => {
183196
{ ...mockWorkflowStep, workflow: { user: { whitelistWorkflows: true } } },
184197
]);
185198
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
186-
mockAkismetCheckSpam.mockResolvedValue(true);
199+
mockFetch.mockResolvedValue({
200+
json: () => Promise.resolve({ flagged: true }),
201+
});
187202

188203
await scanWorkflowBody(payload);
189204

190-
expect(mockAkismetCheckSpam).toHaveBeenCalled();
205+
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
206+
method: "POST",
207+
headers: {
208+
"Content-Type": "application/json",
209+
Authorization: `Bearer test-key`,
210+
},
211+
body: JSON.stringify({
212+
clientId: "Workflow step - 1",
213+
name: "Workflow",
214+
entity: "WorkflowBody",
215+
content: "Test reminder body",
216+
passthrough: true,
217+
}),
218+
});
191219
expect(prismaMock.workflowStep.update).not.toHaveBeenCalled();
192220
expect(lockUser).not.toHaveBeenCalled();
193221
});

packages/features/tasker/tasks/scanWorkflowBody.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import { AkismetClient } from "akismet-api";
2-
import type { Comment } from "akismet-api";
31
import z from "zod";
42

53
import { getTemplateBodyForAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
64
import compareReminderBodyToTemplate from "@calcom/features/ee/workflows/lib/compareReminderBodyToTemplate";
75
import { lockUser, LockReason } from "@calcom/lib/autoLock";
8-
import { WEBAPP_URL } from "@calcom/lib/constants";
96
import logger from "@calcom/lib/logger";
107
import { getTranslation } from "@calcom/lib/server/i18n";
118
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
@@ -20,8 +17,8 @@ export const scanWorkflowBodySchema = z.object({
2017
const log = logger.getSubLogger({ prefix: ["[tasker] scanWorkflowBody"] });
2118

2219
export async function scanWorkflowBody(payload: string) {
23-
if (!process.env.AKISMET_API_KEY) {
24-
log.info("AKISMET_API_KEY not set, skipping scan");
20+
if (!process.env.IFFY_API_KEY) {
21+
log.info("IFFY_API_KEY not set, skipping scan");
2522
return;
2623
}
2724

@@ -48,8 +45,6 @@ export async function scanWorkflowBody(payload: string) {
4845
},
4946
});
5047

51-
const client = new AkismetClient({ key: process.env.AKISMET_API_KEY, blog: WEBAPP_URL });
52-
5348
for (const workflowStep of workflowSteps) {
5449
if (!workflowStep.reminderBody) {
5550
await prisma.workflowStep.update({
@@ -93,12 +88,7 @@ export async function scanWorkflowBody(payload: string) {
9388
continue;
9489
}
9590

96-
const comment: Comment = {
97-
user_ip: "127.0.0.1",
98-
content: workflowStep.reminderBody,
99-
};
100-
101-
const isSpam = await client.checkSpam(comment);
91+
const isSpam = await iffyScanBody(workflowStep.reminderBody, workflowStep.id);
10292

10393
if (isSpam) {
10494
if (workflowStep.workflow.user?.whitelistWorkflows) {
@@ -160,3 +150,27 @@ export async function scanWorkflowBody(payload: string) {
160150
teamId: workflow.team?.id || null,
161151
});
162152
}
153+
154+
const iffyScanBody = async (body: string, workflowStepId: number) => {
155+
try {
156+
const response = await fetch("https://api.iffy.com/api/v1/moderate", {
157+
method: "POST",
158+
headers: {
159+
"Content-Type": "application/json",
160+
Authorization: `Bearer ${process.env.IFFY_API_KEY}`,
161+
},
162+
body: JSON.stringify({
163+
clientId: `Workflow step - ${workflowStepId}`,
164+
name: "Workflow",
165+
entity: "WorkflowBody",
166+
content: body,
167+
passthrough: true,
168+
}),
169+
});
170+
171+
const data = await response.json();
172+
return data.flagged;
173+
} catch (error) {
174+
log.error(`Error scanning workflow body for workflow step ${workflowStepId}:`, error);
175+
}
176+
};

packages/lib/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export const GOOGLE_CALENDAR_SCOPES = [
208208
"https://www.googleapis.com/auth/calendar.readonly",
209209
];
210210
export const DIRECTORY_IDS_TO_LOG = process.env.DIRECTORY_IDS_TO_LOG?.split(",") || [];
211-
export const SCANNING_WORKFLOW_STEPS = !IS_SELF_HOSTED && process.env.AKISMET_API_KEY;
211+
export const SCANNING_WORKFLOW_STEPS = !!(!IS_SELF_HOSTED && process.env.IFFY_API_KEY);
212212

213213
export const IS_PLAIN_CHAT_ENABLED =
214214
!!process.env.NEXT_PUBLIC_PLAIN_CHAT_ID && process.env.NEXT_PUBLIC_PLAIN_CHAT_ID !== "";

turbo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,6 @@
241241
"globalEnv": [
242242
"ALLOWED_HOSTNAMES",
243243
"ANALYZE",
244-
"AKISMET_API_KEY",
245244
"API_KEY_PREFIX",
246245
"APP_USER_NAME",
247246
"BASECAMP3_CLIENT_ID",
@@ -315,6 +314,7 @@
315314
"HEROKU_APP_NAME",
316315
"HUBSPOT_CLIENT_ID",
317316
"HUBSPOT_CLIENT_SECRET",
317+
"IFFY_API_KEY",
318318
"INTEGRATION_TEST_MODE",
319319
"IS_E2E",
320320
"INTERCOM_SECRET",

0 commit comments

Comments
 (0)