refactor: Use Iffy for workflow body scanning (#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
This commit is contained in:
+1
-1
@@ -425,7 +425,7 @@ DIRECTORY_IDS_TO_LOG=
|
||||
# Read more about it in the README.md
|
||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=
|
||||
|
||||
AKISMET_API_KEY=
|
||||
IFFY_API_KEY=
|
||||
|
||||
## Env variables related to avoiding booking failures
|
||||
# Request for checking reservation would be attempted to send every these seconds if the request is stale at that time
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
@@ -7,21 +7,6 @@ import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewe
|
||||
|
||||
import { scanWorkflowBody } from "./scanWorkflowBody";
|
||||
|
||||
const mockAkismetCheckSpam = vi.fn();
|
||||
|
||||
// Mock the entire module
|
||||
vi.mock("akismet-api", () => {
|
||||
return {
|
||||
AkismetClient: class {
|
||||
constructor() {
|
||||
return {
|
||||
checkSpam: mockAkismetCheckSpam,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/lib/autoLock", async (importActual) => {
|
||||
const actual = await importActual<typeof import("@calcom/lib/autoLock")>();
|
||||
return {
|
||||
@@ -54,15 +39,18 @@ const mockWorkflow = {
|
||||
};
|
||||
|
||||
describe("scanWorkflowBody", () => {
|
||||
const mockFetch = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
process.env.AKISMET_API_KEY = "test-key";
|
||||
vi.stubGlobal("fetch", mockFetch);
|
||||
process.env.IFFY_API_KEY = "test-key";
|
||||
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
|
||||
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
|
||||
});
|
||||
|
||||
it("should skip scan if AKISMET_API_KEY is not set", async () => {
|
||||
process.env.AKISMET_API_KEY = "";
|
||||
it("should skip scan if IFFY_API_KEY is not set", async () => {
|
||||
process.env.IFFY_API_KEY = "";
|
||||
const payload = JSON.stringify({
|
||||
userId: 1,
|
||||
workflowStepIds: [1],
|
||||
@@ -98,13 +86,25 @@ describe("scanWorkflowBody", () => {
|
||||
|
||||
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
|
||||
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
|
||||
mockAkismetCheckSpam.mockResolvedValue(false);
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve({ flagged: false }),
|
||||
});
|
||||
|
||||
await scanWorkflowBody(payload);
|
||||
|
||||
expect(mockAkismetCheckSpam).toHaveBeenCalledWith({
|
||||
user_ip: "127.0.0.1",
|
||||
content: mockWorkflowStep.reminderBody,
|
||||
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer test-key`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientId: "Workflow step - 1",
|
||||
name: "Workflow",
|
||||
entity: "WorkflowBody",
|
||||
content: "Test reminder body",
|
||||
passthrough: true,
|
||||
}),
|
||||
});
|
||||
expect(prismaMock.workflowStep.update).toHaveBeenCalledWith({
|
||||
where: { id: 1 },
|
||||
@@ -119,11 +119,26 @@ describe("scanWorkflowBody", () => {
|
||||
});
|
||||
|
||||
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
|
||||
mockAkismetCheckSpam.mockResolvedValue(true);
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve({ flagged: true }),
|
||||
});
|
||||
|
||||
await scanWorkflowBody(payload);
|
||||
|
||||
expect(mockAkismetCheckSpam).toHaveBeenCalled();
|
||||
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer test-key`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientId: "Workflow step - 1",
|
||||
name: "Workflow",
|
||||
entity: "WorkflowBody",
|
||||
content: "Test reminder body",
|
||||
passthrough: true,
|
||||
}),
|
||||
});
|
||||
expect(prismaMock.workflowStep.update).not.toHaveBeenCalled();
|
||||
expect(lockUser).toHaveBeenCalledWith("userId", "1", LockReason.SPAM_WORKFLOW_BODY);
|
||||
});
|
||||
@@ -136,7 +151,6 @@ describe("scanWorkflowBody", () => {
|
||||
|
||||
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
|
||||
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
|
||||
mockAkismetCheckSpam.mockResolvedValue(false);
|
||||
|
||||
await scanWorkflowBody(payload);
|
||||
|
||||
@@ -166,7 +180,6 @@ describe("scanWorkflowBody", () => {
|
||||
|
||||
prismaMock.workflowStep.findMany.mockResolvedValue([mockWorkflowStep]);
|
||||
prismaMock.workflow.findFirst.mockResolvedValue(null);
|
||||
mockAkismetCheckSpam.mockResolvedValue(false);
|
||||
|
||||
await scanWorkflowBody(payload);
|
||||
|
||||
@@ -183,11 +196,26 @@ describe("scanWorkflowBody", () => {
|
||||
{ ...mockWorkflowStep, workflow: { user: { whitelistWorkflows: true } } },
|
||||
]);
|
||||
prismaMock.workflow.findFirst.mockResolvedValue(mockWorkflow);
|
||||
mockAkismetCheckSpam.mockResolvedValue(true);
|
||||
mockFetch.mockResolvedValue({
|
||||
json: () => Promise.resolve({ flagged: true }),
|
||||
});
|
||||
|
||||
await scanWorkflowBody(payload);
|
||||
|
||||
expect(mockAkismetCheckSpam).toHaveBeenCalled();
|
||||
expect(mockFetch).toHaveBeenCalledWith("https://api.iffy.com/api/v1/moderate", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer test-key`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientId: "Workflow step - 1",
|
||||
name: "Workflow",
|
||||
entity: "WorkflowBody",
|
||||
content: "Test reminder body",
|
||||
passthrough: true,
|
||||
}),
|
||||
});
|
||||
expect(prismaMock.workflowStep.update).not.toHaveBeenCalled();
|
||||
expect(lockUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
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";
|
||||
@@ -20,8 +17,8 @@ export const scanWorkflowBodySchema = z.object({
|
||||
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");
|
||||
if (!process.env.IFFY_API_KEY) {
|
||||
log.info("IFFY_API_KEY not set, skipping scan");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,8 +45,6 @@ export async function scanWorkflowBody(payload: string) {
|
||||
},
|
||||
});
|
||||
|
||||
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({
|
||||
@@ -93,12 +88,7 @@ export async function scanWorkflowBody(payload: string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const comment: Comment = {
|
||||
user_ip: "127.0.0.1",
|
||||
content: workflowStep.reminderBody,
|
||||
};
|
||||
|
||||
const isSpam = await client.checkSpam(comment);
|
||||
const isSpam = await iffyScanBody(workflowStep.reminderBody, workflowStep.id);
|
||||
|
||||
if (isSpam) {
|
||||
if (workflowStep.workflow.user?.whitelistWorkflows) {
|
||||
@@ -160,3 +150,27 @@ export async function scanWorkflowBody(payload: string) {
|
||||
teamId: workflow.team?.id || null,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,7 +208,7 @@ export const GOOGLE_CALENDAR_SCOPES = [
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
];
|
||||
export const DIRECTORY_IDS_TO_LOG = process.env.DIRECTORY_IDS_TO_LOG?.split(",") || [];
|
||||
export const SCANNING_WORKFLOW_STEPS = !IS_SELF_HOSTED && process.env.AKISMET_API_KEY;
|
||||
export const SCANNING_WORKFLOW_STEPS = !!(!IS_SELF_HOSTED && process.env.IFFY_API_KEY);
|
||||
|
||||
export const IS_PLAIN_CHAT_ENABLED =
|
||||
!!process.env.NEXT_PUBLIC_PLAIN_CHAT_ID && process.env.NEXT_PUBLIC_PLAIN_CHAT_ID !== "";
|
||||
|
||||
+1
-1
@@ -241,7 +241,6 @@
|
||||
"globalEnv": [
|
||||
"ALLOWED_HOSTNAMES",
|
||||
"ANALYZE",
|
||||
"AKISMET_API_KEY",
|
||||
"API_KEY_PREFIX",
|
||||
"APP_USER_NAME",
|
||||
"BASECAMP3_CLIENT_ID",
|
||||
@@ -315,6 +314,7 @@
|
||||
"HEROKU_APP_NAME",
|
||||
"HUBSPOT_CLIENT_ID",
|
||||
"HUBSPOT_CLIENT_SECRET",
|
||||
"IFFY_API_KEY",
|
||||
"INTEGRATION_TEST_MODE",
|
||||
"IS_E2E",
|
||||
"INTERCOM_SECRET",
|
||||
|
||||
Reference in New Issue
Block a user