feat: add rate limiting and more error handling to Cal.ai (#11898)
Co-authored-by: tedspare <ted.spare@gmail.com>
This commit is contained in:
parent
4b8bdeba74
commit
9e927af813
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/ai",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"private": true,
|
||||
"author": "Cal.com Inc.",
|
||||
"dependencies": {
|
||||
|
|
|
@ -40,6 +40,13 @@ export const POST = async (request: NextRequest) => {
|
|||
|
||||
return new NextResponse("ok");
|
||||
} catch (error) {
|
||||
await sendEmail({
|
||||
subject: `Re: ${subject}`,
|
||||
text: "Thanks for using Cal.ai! We're experiencing high demand and can't currently process your request. Please try again later.",
|
||||
to: user.email,
|
||||
from: agentEmail,
|
||||
});
|
||||
|
||||
return new NextResponse(
|
||||
(error as Error).message || "Something went wrong. Please try again or reach out for help.",
|
||||
{ status: 500 }
|
||||
|
|
|
@ -3,6 +3,7 @@ import { simpleParser } from "mailparser";
|
|||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { env } from "../../../env.mjs";
|
||||
|
@ -31,18 +32,37 @@ export const POST = async (request: NextRequest) => {
|
|||
|
||||
const formData = await request.formData();
|
||||
const body = Object.fromEntries(formData);
|
||||
|
||||
// body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
|
||||
const signature = (body.dkim as string).includes(" : pass");
|
||||
|
||||
const envelope = JSON.parse(body.envelope as string);
|
||||
|
||||
const aiEmail = envelope.to[0];
|
||||
const subject = body.subject || "";
|
||||
|
||||
try {
|
||||
await checkRateLimitAndThrowError({
|
||||
identifier: `ai:email:${envelope.from}`,
|
||||
rateLimitingType: "ai",
|
||||
});
|
||||
} catch (error) {
|
||||
await sendEmail({
|
||||
subject: `Re: ${subject}`,
|
||||
text: "Thanks for using Cal.ai! You've reached your daily limit. Please try again tomorrow.",
|
||||
to: envelope.from,
|
||||
from: aiEmail,
|
||||
});
|
||||
|
||||
return new NextResponse("Exceeded rate limit", { status: 200 }); // Don't return 429 to avoid triggering retry logic in SendGrid
|
||||
}
|
||||
|
||||
// Parse email from mixed MIME type
|
||||
const parsed: ParsedMail = await simpleParser(body.email as Source);
|
||||
|
||||
if (!parsed.text && !parsed.subject) {
|
||||
await sendEmail({
|
||||
subject: `Re: ${subject}`,
|
||||
text: "Thanks for using Cal.ai! It looks like you forgot to include a message. Please try again.",
|
||||
to: envelope.from,
|
||||
from: aiEmail,
|
||||
});
|
||||
return new NextResponse("Email missing text and subject", { status: 400 });
|
||||
}
|
||||
|
||||
|
@ -62,11 +82,14 @@ export const POST = async (request: NextRequest) => {
|
|||
where: { email: envelope.from },
|
||||
});
|
||||
|
||||
// body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
|
||||
const signature = (body.dkim as string).includes(" : pass");
|
||||
|
||||
// User is not a cal.com user or is using an unverified email.
|
||||
if (!signature || !user) {
|
||||
await sendEmail({
|
||||
html: `Thanks for your interest in Cal.ai! To get started, Make sure you have a <a href="https://cal.com/signup" target="_blank">cal.com</a> account with this email address.`,
|
||||
subject: `Re: ${body.subject}`,
|
||||
subject: `Re: ${subject}`,
|
||||
text: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
|
||||
to: envelope.from,
|
||||
from: aiEmail,
|
||||
|
@ -83,7 +106,7 @@ export const POST = async (request: NextRequest) => {
|
|||
|
||||
await sendEmail({
|
||||
html: `Thanks for using Cal.ai! To get started, the app must be installed. <a href=${url} target="_blank">Click this link</a> to install it.`,
|
||||
subject: `Re: ${body.subject}`,
|
||||
subject: `Re: ${subject}`,
|
||||
text: `Thanks for using Cal.ai! To get started, the app must be installed. Click this link to install the Cal.ai app: ${url}`,
|
||||
to: envelope.from,
|
||||
from: aiEmail,
|
||||
|
@ -110,7 +133,7 @@ export const POST = async (request: NextRequest) => {
|
|||
|
||||
if ("error" in availability) {
|
||||
await sendEmail({
|
||||
subject: `Re: ${body.subject}`,
|
||||
subject: `Re: ${subject}`,
|
||||
text: "Sorry, there was an error fetching your availability. Please try again.",
|
||||
to: user.email,
|
||||
from: aiEmail,
|
||||
|
@ -121,7 +144,7 @@ export const POST = async (request: NextRequest) => {
|
|||
|
||||
if ("error" in eventTypes) {
|
||||
await sendEmail({
|
||||
subject: `Re: ${body.subject}`,
|
||||
subject: `Re: ${subject}`,
|
||||
text: "Sorry, there was an error fetching your event types. Please try again.",
|
||||
to: user.email,
|
||||
from: aiEmail,
|
||||
|
@ -139,8 +162,8 @@ export const POST = async (request: NextRequest) => {
|
|||
body: JSON.stringify({
|
||||
apiKey,
|
||||
userId: user.id,
|
||||
message: parsed.text,
|
||||
subject: parsed.subject,
|
||||
message: parsed.text || "",
|
||||
subject: parsed.subject || "",
|
||||
replyTo: aiEmail,
|
||||
user: {
|
||||
email: user.email,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { TRPCError } from "@calcom/trpc";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
|
||||
import type { RateLimitHelper } from "./rateLimit";
|
||||
import { rateLimiter } from "./rateLimit";
|
||||
|
|
|
@ -7,7 +7,7 @@ import logger from "./logger";
|
|||
const log = logger.getChildLogger({ prefix: ["RateLimit"] });
|
||||
|
||||
export type RateLimitHelper = {
|
||||
rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api";
|
||||
rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api" | "ai";
|
||||
identifier: string;
|
||||
};
|
||||
|
||||
|
@ -75,6 +75,12 @@ export function rateLimiter() {
|
|||
prefix: "ratelimit:api",
|
||||
limiter: Ratelimit.fixedWindow(10, "60s"),
|
||||
}),
|
||||
ai: new Ratelimit({
|
||||
redis,
|
||||
analytics: true,
|
||||
prefix: "ratelimit",
|
||||
limiter: Ratelimit.fixedWindow(20, "1d"),
|
||||
}),
|
||||
};
|
||||
|
||||
async function rateLimit({ rateLimitingType = "core", identifier }: RateLimitHelper) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user