Skip to content

Commit

Permalink
Merge pull request #282 from elie222/webhook
Browse files Browse the repository at this point in the history
Add call webhook action
  • Loading branch information
elie222 authored Dec 30, 2024
2 parents e942f3e + 227e34d commit ea6ab6a
Show file tree
Hide file tree
Showing 24 changed files with 295 additions and 22 deletions.
1 change: 1 addition & 0 deletions apps/web/__tests__/ai-choose-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ function getAction(action: Partial<Action> = {}): Action {
to: null,
cc: null,
bcc: null,
url: null,
labelPrompt: null,
subjectPrompt: null,
contentPrompt: null,
Expand Down
1 change: 1 addition & 0 deletions apps/web/__tests__/ai-choose-rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe.skipIf(!isAiTest)("aiChooseRule", () => {
to: null,
cc: null,
bcc: null,
url: null,

labelPrompt: null,
subjectPrompt: null,
Expand Down
27 changes: 27 additions & 0 deletions apps/web/app/(app)/automation/RuleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,18 @@ export function RuleForm({ rule }: { rule: CreateRuleBody & { id?: string } }) {
trigger("conditions");
}, [conditions]);

const actionErrors = useMemo(() => {
const actionErrors: string[] = [];
watch("actions")?.forEach((_, index) => {
const actionError =
errors?.actions?.[index]?.url?.root?.message ||
errors?.actions?.[index]?.label?.root?.message ||
errors?.actions?.[index]?.to?.root?.message;
if (actionError) actionErrors.push(actionError);
});
return actionErrors;
}, [errors, watch]);

return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mt-4">
Expand Down Expand Up @@ -443,6 +455,21 @@ export function RuleForm({ rule }: { rule: CreateRuleBody & { id?: string } }) {

<TypographyH3 className="mt-6">Actions</TypographyH3>

{actionErrors.length > 0 && (
<div className="mt-4">
<AlertError
title="Error"
description={
<ul className="list-inside list-disc">
{actionErrors.map((error, index) => (
<li key={`action-${index}`}>{error}</li>
))}
</ul>
}
/>
</div>
)}

<div className="mt-4 space-y-4">
{watch("actions")?.map((action, i) => {
return (
Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/(app)/automation/TestRules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,9 @@ export function TestResultDisplay({
.filter(
([key, value]) =>
value &&
["label", "subject", "content", "to", "cc", "bcc"].includes(key),
["label", "subject", "content", "to", "cc", "bcc", "url"].includes(
key,
),
)
.map(([key, value]) => (
<div key={key} className="flex text-sm text-gray-800">
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/(app)/automation/rule/[ruleId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default async function RulePage({
to: { value: action.to },
cc: { value: action.cc },
bcc: { value: action.bcc },
url: { value: action.url },
})),
categoryFilters: rule.categoryFilters.map((category) => category.id),
conditions: getConditions(rule),
Expand Down
23 changes: 23 additions & 0 deletions apps/web/app/(app)/settings/WebhookGenerate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"use client";

import { Button } from "@/components/ui/button";
import { regenerateWebhookSecretAction } from "@/utils/actions/webhook";
import { isActionError } from "@/utils/error";
import { toastError, toastSuccess } from "@/components/Toast";

export function RegenerateSecretButton({ hasSecret }: { hasSecret: boolean }) {
const handleRegenerateSecret = async () => {
const result = await regenerateWebhookSecretAction();
if (isActionError(result)) {
toastError({ title: "Error", description: result.error });
} else {
toastSuccess({ description: "Webhook secret regenerated" });
}
};

return (
<Button variant="outline" type="button" onClick={handleRegenerateSecret}>
{hasSecret ? "Regenerate Secret" : "Generate Secret"}
</Button>
);
}
36 changes: 36 additions & 0 deletions apps/web/app/(app)/settings/WebhookSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { FormSection, FormSectionLeft } from "@/components/Form";
import prisma from "@/utils/prisma";
import { Card } from "@/components/ui/card";
import { CopyInput } from "@/components/CopyInput";
import { RegenerateSecretButton } from "@/app/(app)/settings/WebhookGenerate";

export async function WebhookSection() {
const session = await auth();
const userId = session?.user.id;
if (!userId) throw new Error("Not authenticated");

const user = await prisma.user.findUnique({
where: { id: userId },
select: { webhookSecret: true },
});

return (
<FormSection>
<FormSectionLeft
title="Webhooks (Developers)"
description="API webhook secret for request verification. Include this in the X-Webhook-Secret header when setting up webhook endpoints."
/>

<div className="col-span-2">
<Card className="p-6">
<div className="space-y-4">
{!!user?.webhookSecret && <CopyInput value={user?.webhookSecret} />}

<RegenerateSecretButton hasSecret={!!user?.webhookSecret} />
</div>
</Card>
</div>
</FormSection>
);
}
6 changes: 4 additions & 2 deletions apps/web/app/(app)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import { AboutSection } from "@/app/(app)/settings/AboutSection";
// import { LabelsSection } from "@/app/(app)/settings/LabelsSection";
import { DeleteSection } from "@/app/(app)/settings/DeleteSection";
import { ModelSection } from "@/app/(app)/settings/ModelSection";
import { EmailUpdatesSection } from "@/app/(app)/settings/EmailUpdatesSection";
// import { EmailUpdatesSection } from "@/app/(app)/settings/EmailUpdatesSection";
import { MultiAccountSection } from "@/app/(app)/settings/MultiAccountSection";
import { ApiKeysSection } from "@/app/(app)/settings/ApiKeysSection";
import { WebhookSection } from "@/app/(app)/settings/WebhookSection";

export default function Settings() {
return (
<FormWrapper>
<AboutSection />
{/* <LabelsSection /> */}
<ModelSection />
<EmailUpdatesSection />
{/* <EmailUpdatesSection /> */}
<MultiAccountSection />
<WebhookSection />
<ApiKeysSection />
<DeleteSection />
</FormWrapper>
Expand Down
5 changes: 4 additions & 1 deletion apps/web/app/api/google/webhook/process-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ export async function processHistoryForUser(
);

// couldn't refresh the token
if (!gmail) return NextResponse.json({ ok: true });
if (!gmail) {
logger.error("Failed to refresh token", { email });
return NextResponse.json({ ok: true });
}

const startHistoryId =
options?.startHistoryId ||
Expand Down
2 changes: 2 additions & 0 deletions apps/web/components/CopyInput.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { CopyIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
Expand Down
6 changes: 6 additions & 0 deletions apps/web/components/PlanBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export function ActionBadgeExpanded({ action }: { action: ExecutedAction }) {
);
case ActionType.MARK_SPAM:
return <ActionBadge type={ActionType.MARK_SPAM} />;
case ActionType.CALL_WEBHOOK:
return <ActionBadge type={ActionType.CALL_WEBHOOK} />;
default:
return <ActionBadge type={action.type} />;
}
Expand Down Expand Up @@ -145,6 +147,10 @@ function getActionLabel(type: ActionType) {
return "Send";
case ActionType.DRAFT_EMAIL:
return "Draft";
case ActionType.CALL_WEBHOOK:
return "Webhook";
case ActionType.MARK_SPAM:
return "Mark as spam";
default:
return capitalCase(type);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterEnum
ALTER TYPE "ActionType" ADD VALUE 'CALL_WEBHOOK';

-- AlterTable
ALTER TABLE "User" ADD COLUMN "webhookSecret" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Action" ADD COLUMN "url" TEXT;

-- AlterTable
ALTER TABLE "ExecutedAction" ADD COLUMN "url" TEXT;
2 changes: 1 addition & 1 deletion apps/web/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
5 changes: 4 additions & 1 deletion apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ model User {
coldEmailBlocker ColdEmailSetting?
coldEmailPrompt String?
rulesPrompt String?
webhookSecret String?
// categorization
autoCategorizeSenders Boolean @default(false)
Expand Down Expand Up @@ -218,6 +219,7 @@ model Action {
to String?
cc String?
bcc String?
url String?
labelPrompt String? // deprecated
subjectPrompt String? // deprecated
Expand Down Expand Up @@ -267,6 +269,7 @@ model ExecutedAction {
to String?
cc String?
bcc String?
url String?
}

model Group {
Expand Down Expand Up @@ -367,10 +370,10 @@ enum ActionType {
FORWARD
DRAFT_EMAIL
MARK_SPAM
CALL_WEBHOOK
// SUMMARIZE
// SNOOZE
// ADD_TO_DO
// CALL_WEBHOOK
// INTEGRATION // for example, add to Notion
}

Expand Down
12 changes: 11 additions & 1 deletion apps/web/utils/actionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const actionInputs: Record<
ActionType,
{
fields: {
name: "label" | "subject" | "content" | "to" | "cc" | "bcc";
name: "label" | "subject" | "content" | "to" | "cc" | "bcc" | "url";
label: string;
textArea?: boolean;
}[];
Expand Down Expand Up @@ -108,6 +108,14 @@ export const actionInputs: Record<
],
},
[ActionType.MARK_SPAM]: { fields: [] },
[ActionType.CALL_WEBHOOK]: {
fields: [
{
name: "url",
label: "URL",
},
],
},
};

export function getActionFields(fields: Action | ExecutedAction | undefined) {
Expand All @@ -118,6 +126,7 @@ export function getActionFields(fields: Action | ExecutedAction | undefined) {
to?: string;
cc?: string;
bcc?: string;
url?: string;
} = {};

// only return fields with a value
Expand All @@ -127,6 +136,7 @@ export function getActionFields(fields: Action | ExecutedAction | undefined) {
if (fields?.to) res.to = fields.to;
if (fields?.cc) res.cc = fields.cc;
if (fields?.bcc) res.bcc = fields.bcc;
if (fields?.url) res.url = fields.url;

return res;
}
5 changes: 4 additions & 1 deletion apps/web/utils/actions/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const createRuleAction = withActionInstrumentation(
? {
createMany: {
data: body.actions.map(
({ type, label, subject, content, to, cc, bcc }) => {
({ type, label, subject, content, to, cc, bcc, url }) => {
return {
type,
label: label?.value,
Expand All @@ -50,6 +50,7 @@ export const createRuleAction = withActionInstrumentation(
to: to?.value,
cc: cc?.value,
bcc: bcc?.value,
url: url?.value,
};
},
),
Expand Down Expand Up @@ -159,6 +160,7 @@ export const updateRuleAction = withActionInstrumentation(
to: a.to?.value,
cc: a.cc?.value,
bcc: a.bcc?.value,
url: a.url?.value,
},
});
}),
Expand All @@ -175,6 +177,7 @@ export const updateRuleAction = withActionInstrumentation(
to: a.to?.value,
cc: a.cc?.value,
bcc: a.bcc?.value,
url: a.url?.value,
})),
}),
]
Expand Down
36 changes: 25 additions & 11 deletions apps/web/utils/actions/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const zodActionType = z.enum([
ActionType.MARK_SPAM,
ActionType.REPLY,
ActionType.SEND_EMAIL,
ActionType.CALL_WEBHOOK,
]);

const zodField = z
Expand All @@ -50,18 +51,31 @@ const zodAction = z
to: zodField,
cc: zodField,
bcc: zodField,
url: zodField,
})
.refine(
(data) => {
if (data.type === ActionType.LABEL) return !!data.label?.value?.trim();
if (data.type === ActionType.FORWARD) return !!data.to?.value?.trim();
return true;
},
{
message: "Forward action requires a 'to' field",
path: ["to"],
},
);
.superRefine((data, ctx) => {
if (data.type === ActionType.LABEL && !data.label?.value?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Please enter a label name for the Label action",
path: ["label"],
});
}
if (data.type === ActionType.FORWARD && !data.to?.value?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Please enter an email address to forward to",
path: ["to"],
});
}
if (data.type === ActionType.CALL_WEBHOOK && !data.url?.value?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Please enter a webhook URL",
path: ["url"],
});
}
});

export const zodRuleType = z.enum([
RuleType.AI,
Expand Down
Loading

1 comment on commit ea6ab6a

@vercel
Copy link

@vercel vercel bot commented on ea6ab6a Dec 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.