Skip to content

Commit a6dabfd

Browse files
committed
Harden Discord chat agent routing, validation, and runtime error handling.
This scopes prompt handling to the configured channel, adds stronger prompt and event-update validation, and improves gateway startup failure responses for safer production behavior.
1 parent 7b62968 commit a6dabfd

File tree

4 files changed

+82
-29
lines changed

4 files changed

+82
-29
lines changed

app/src/app/api/discord/gateway/route.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,25 @@ export async function GET(request: Request): Promise<Response> {
2525
return new Response("Unauthorized", { status: 401 });
2626
}
2727

28-
const bot = getDiscordReviewBot();
29-
const discordAdapter = bot.getAdapter("discord");
30-
const durationMs = 600 * 1000;
31-
const webhookUrl = `${mainConfig.instance.origin}/api/webhooks/discord`;
28+
try {
29+
const bot = getDiscordReviewBot();
30+
const discordAdapter = bot.getAdapter("discord");
31+
const durationMs = 600 * 1000;
32+
const webhookUrl = `${mainConfig.instance.origin}/api/webhooks/discord`;
3233

33-
return discordAdapter.startGatewayListener(
34-
{ waitUntil: (task) => after(() => task) },
35-
durationMs,
36-
undefined,
37-
webhookUrl,
38-
);
34+
return discordAdapter.startGatewayListener(
35+
{ waitUntil: (task) => after(() => task) },
36+
durationMs,
37+
undefined,
38+
webhookUrl,
39+
);
40+
} catch (error) {
41+
const message = error instanceof Error ? error.message : "Unknown error";
42+
return new Response(
43+
`Failed to start Discord gateway listener: ${message}`,
44+
{
45+
status: 500,
46+
},
47+
);
48+
}
3949
}

app/src/lib/discord/prompt-agent.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { db } from "@/lib/db";
22
import { mainConfig } from "@/lib/config";
33
import { eventsTable, type InsertEvent } from "@/lib/schema";
44
import { createLumaClient } from "@/lib/luma";
5-
import { and, eq } from "drizzle-orm";
5+
import { eq } from "drizzle-orm";
66
import { createGateway, generateText, tool } from "ai";
77
import { z } from "zod";
88

@@ -71,25 +71,32 @@ function normalizeUpdateData(
7171
if (typeof eventData.attendeeLimit === "number") {
7272
normalized.attendeeLimit = eventData.attendeeLimit;
7373
}
74-
if (typeof eventData.tagline === "string") normalized.tagline = eventData.tagline;
74+
if (typeof eventData.tagline === "string")
75+
normalized.tagline = eventData.tagline;
7576
if (typeof eventData.startDate === "string") {
7677
normalized.startDate = toDate(eventData.startDate);
7778
}
7879
if (typeof eventData.endDate === "string") {
7980
normalized.endDate = toDate(eventData.endDate);
8081
}
81-
if ("lumaEventId" in eventData) normalized.lumaEventId = eventData.lumaEventId;
82-
if (typeof eventData.isDraft === "boolean") normalized.isDraft = eventData.isDraft;
82+
if ("lumaEventId" in eventData)
83+
normalized.lumaEventId = eventData.lumaEventId;
84+
if (typeof eventData.isDraft === "boolean")
85+
normalized.isDraft = eventData.isDraft;
8386
if (typeof eventData.isHackathon === "boolean") {
8487
normalized.isHackathon = eventData.isHackathon;
8588
}
8689
if (typeof eventData.highlightOnLandingPage === "boolean") {
8790
normalized.highlightOnLandingPage = eventData.highlightOnLandingPage;
8891
}
89-
if ("fullAddress" in eventData) normalized.fullAddress = eventData.fullAddress;
90-
if ("shortLocation" in eventData) normalized.shortLocation = eventData.shortLocation;
91-
if ("streetAddress" in eventData) normalized.streetAddress = eventData.streetAddress;
92-
if ("recordingUrl" in eventData) normalized.recordingUrl = eventData.recordingUrl;
92+
if ("fullAddress" in eventData)
93+
normalized.fullAddress = eventData.fullAddress;
94+
if ("shortLocation" in eventData)
95+
normalized.shortLocation = eventData.shortLocation;
96+
if ("streetAddress" in eventData)
97+
normalized.streetAddress = eventData.streetAddress;
98+
if ("recordingUrl" in eventData)
99+
normalized.recordingUrl = eventData.recordingUrl;
93100

94101
return normalized;
95102
}
@@ -104,17 +111,23 @@ async function getEventBySlug(slug: string) {
104111
return event ?? null;
105112
}
106113

107-
async function updateEventBySlug(input: z.infer<typeof updateEventInputSchema>) {
114+
async function updateEventBySlug(
115+
input: z.infer<typeof updateEventInputSchema>,
116+
) {
108117
const existingEvent = await getEventBySlug(input.slug);
109118
if (!existingEvent) {
110119
throw new Error(`Event not found for slug: ${input.slug}`);
111120
}
112121

113122
const normalized = normalizeUpdateData(input.eventData);
123+
if (Object.keys(normalized).length === 0) {
124+
throw new Error("No valid fields were provided for update");
125+
}
126+
114127
const [updated] = await db
115128
.update(eventsTable)
116129
.set(normalized)
117-
.where(and(eq(eventsTable.id, existingEvent.id)))
130+
.where(eq(eventsTable.id, existingEvent.id))
118131
.returning();
119132

120133
return updated ?? null;
@@ -152,15 +165,21 @@ function buildTools() {
152165
export async function runDiscordPromptAgent(input: {
153166
prompt: string;
154167
}): Promise<string> {
168+
const prompt = input.prompt.trim();
169+
if (!prompt) {
170+
return "Please provide a prompt.";
171+
}
172+
155173
const { text } = await generateText({
156174
model: getGatewayModel(),
157175
tools: buildTools(),
158176
system: `You are the AllThingsWeb Discord ops assistant.
159177
You can inspect and update AllThingsWeb event data and fetch Luma event payloads.
160178
Use tools whenever the answer depends on current data.
161179
Before updating event data, confirm intent from the user message and summarize what changed.
180+
If user intent is ambiguous, ask a clarifying question instead of calling update tools.
162181
Be concise and action-oriented.`,
163-
prompt: input.prompt,
182+
prompt,
164183
});
165184

166185
return text.trim();

app/src/lib/discord/review-bot.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ type ReviewCardInput = {
2727
isDraft: boolean;
2828
};
2929

30-
let reviewBot: Chat<{ discord: ReturnType<typeof createDiscordAdapter> }> | null =
31-
null;
30+
let reviewBot: Chat<{
31+
discord: ReturnType<typeof createDiscordAdapter>;
32+
}> | null = null;
3233
let handlersRegistered = false;
3334

3435
function requireDiscordConfig() {
@@ -94,22 +95,35 @@ function registerHandlers(
9495
await event.thread.post("No pending review session found for this event.");
9596
});
9697

98+
const isPromptChannelMessage = (thread: Thread): boolean => {
99+
const { reviewChannelId } = requireDiscordConfig();
100+
const discordAdapter = bot.getAdapter("discord");
101+
const decodedThread = discordAdapter.decodeThreadId(thread.id);
102+
return decodedThread.channelId === reviewChannelId;
103+
};
104+
97105
const promptFromMessage = async (thread: Thread, message: Message) => {
106+
if (!isPromptChannelMessage(thread)) {
107+
return;
108+
}
109+
98110
const userPrompt = message.text?.trim();
99-
if (!userPrompt) {
111+
if (!userPrompt || userPrompt.length < 2) {
100112
return;
101113
}
102114

103115
try {
104116
await thread.startTyping();
105117
const response = await runDiscordPromptAgent({
106-
prompt: userPrompt,
118+
prompt: userPrompt.slice(0, 4000),
107119
});
108120
await thread.post(response || "I couldn't generate a response.");
109121
} catch (error) {
110122
const errorMessage =
111123
error instanceof Error ? error.message : "Unknown error";
112-
await thread.post(`I hit an error while processing that request: ${errorMessage}`);
124+
await thread.post(
125+
`I hit an error while processing that request: ${errorMessage}`,
126+
);
113127
}
114128
};
115129

@@ -164,7 +178,9 @@ export async function postEventReviewCard(
164178
Card({
165179
title: `New Luma event draft: ${input.name}`,
166180
children: [
167-
CardText("Review this event draft and approve when it is ready to publish."),
181+
CardText(
182+
"Review this event draft and approve when it is ready to publish.",
183+
),
168184
Fields([
169185
Field({ label: "Event ID", value: input.eventId }),
170186
Field({ label: "Slug", value: input.slug }),
@@ -178,7 +194,10 @@ export async function postEventReviewCard(
178194
label: "End",
179195
value: new Date(input.endDate).toISOString().slice(0, 16),
180196
}),
181-
Field({ label: "Attendee Limit", value: String(input.attendeeLimit) }),
197+
Field({
198+
label: "Attendee Limit",
199+
value: String(input.attendeeLimit),
200+
}),
182201
]),
183202
CardText(input.tagline),
184203
Actions([

app/src/lib/review/approval.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ export async function approveDiscordReviewSessionByEventId(input: {
5656
.set({
5757
isDraft: false,
5858
})
59-
.where(and(eq(eventsTable.id, pendingSession.eventId), eq(eventsTable.isDraft, true)));
59+
.where(
60+
and(
61+
eq(eventsTable.id, pendingSession.eventId),
62+
eq(eventsTable.isDraft, true),
63+
),
64+
);
6065

6166
await db
6267
.update(eventReviewSessionsTable)

0 commit comments

Comments
 (0)