diff --git a/.env.example b/.env.example index 64f0c01bc3..87f7a3d327 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx index 04f17f3d4e..9e499828e8 100644 --- a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx +++ b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx @@ -1,4 +1,5 @@ -"use client" +"use client"; + import { useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; diff --git a/packages/features/tasker/tasks/scanWorkflowBody.test.ts b/packages/features/tasker/tasks/scanWorkflowBody.test.ts index dcf36920f4..65503b2d4e 100644 --- a/packages/features/tasker/tasks/scanWorkflowBody.test.ts +++ b/packages/features/tasker/tasks/scanWorkflowBody.test.ts @@ -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(); 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(); }); diff --git a/packages/features/tasker/tasks/scanWorkflowBody.ts b/packages/features/tasker/tasks/scanWorkflowBody.ts index 49ab241d52..37183456fd 100644 --- a/packages/features/tasker/tasks/scanWorkflowBody.ts +++ b/packages/features/tasker/tasks/scanWorkflowBody.ts @@ -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); + } +}; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 895d82ae36..99b9293d20 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -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 !== ""; diff --git a/turbo.json b/turbo.json index eaf40a91fe..f0397b7b6b 100644 --- a/turbo.json +++ b/turbo.json @@ -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",