Skip to content

Commit 378f980

Browse files
committed
Enhance URL safety checks in redirect handler and improve DM worker polling mechanism
1 parent eb0b1fe commit 378f980

8 files changed

Lines changed: 220 additions & 103 deletions

File tree

app/api/webhook/route.ts

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,22 @@ export async function POST(request: NextRequest) {
8484
);
8585
const queue = getDMQueue();
8686

87-
for (const event of commentEvents) {
88-
const account = await prisma.instagramAccount.findUnique({
89-
where: { instagramId: event.instagramAccountId },
90-
select: { workspaceId: true },
91-
});
87+
const commentAccountIds = [
88+
...new Set(commentEvents.map((e) => e.instagramAccountId)),
89+
];
90+
const commentAccounts =
91+
commentAccountIds.length > 0
92+
? await prisma.instagramAccount.findMany({
93+
where: { instagramId: { in: commentAccountIds } },
94+
select: { instagramId: true, workspaceId: true },
95+
})
96+
: [];
97+
const commentAccountMap = new Map(
98+
commentAccounts.map((a) => [a.instagramId, a.workspaceId])
99+
);
92100

101+
let firstCommentWorkspaceId: string | null = null;
102+
for (const event of commentEvents) {
93103
await queue.add(
94104
"process-comment",
95105
{
@@ -106,14 +116,19 @@ export async function POST(request: NextRequest) {
106116
}
107117
);
108118

109-
if (account) {
110-
await prisma.webhookEvent.update({
111-
where: { id: webhookEvent.id },
112-
data: { workspaceId: account.workspaceId },
113-
});
119+
const wsId = commentAccountMap.get(event.instagramAccountId);
120+
if (wsId && !firstCommentWorkspaceId) {
121+
firstCommentWorkspaceId = wsId;
114122
}
115123
}
116124

125+
if (firstCommentWorkspaceId) {
126+
await prisma.webhookEvent.update({
127+
where: { id: webhookEvent.id },
128+
data: { workspaceId: firstCommentWorkspaceId },
129+
});
130+
}
131+
117132
// Button taps from opening DMs → deliver the reveal message.
118133
const postbackEvents = parsePostbackEvents(
119134
payload as Parameters<typeof parsePostbackEvents>[0]
@@ -143,12 +158,22 @@ export async function POST(request: NextRequest) {
143158
payload as Parameters<typeof parseMessageEvents>[0]
144159
);
145160

146-
for (const event of messageEvents) {
147-
const account = await prisma.instagramAccount.findUnique({
148-
where: { instagramId: event.instagramAccountId },
149-
select: { workspaceId: true },
150-
});
161+
const messageAccountIds = [
162+
...new Set(messageEvents.map((e) => e.instagramAccountId)),
163+
];
164+
const messageAccounts =
165+
messageAccountIds.length > 0
166+
? await prisma.instagramAccount.findMany({
167+
where: { instagramId: { in: messageAccountIds } },
168+
select: { instagramId: true, workspaceId: true },
169+
})
170+
: [];
171+
const messageAccountMap = new Map(
172+
messageAccounts.map((a) => [a.instagramId, a.workspaceId])
173+
);
151174

175+
let firstMessageWorkspaceId: string | null = null;
176+
for (const event of messageEvents) {
152177
await queue.add(
153178
MESSAGE_JOB_NAME,
154179
{
@@ -168,35 +193,59 @@ export async function POST(request: NextRequest) {
168193
}
169194
);
170195

171-
if (account) {
172-
await prisma.webhookEvent.update({
173-
where: { id: webhookEvent.id },
174-
data: { workspaceId: account.workspaceId },
175-
});
196+
const wsId = messageAccountMap.get(event.instagramAccountId);
197+
if (wsId && !firstMessageWorkspaceId) {
198+
firstMessageWorkspaceId = wsId;
176199
}
177200
}
178201

202+
if (firstMessageWorkspaceId) {
203+
await prisma.webhookEvent.update({
204+
where: { id: webhookEvent.id },
205+
data: { workspaceId: firstMessageWorkspaceId },
206+
});
207+
}
208+
179209
// If a user reads the opening DM and never taps the button, deliver the
180210
// same next-step DM after five minutes. The worker no-ops this delayed job
181211
// if a real button tap has already delivered the reveal.
182212
const readEvents = parseReadEvents(
183213
payload as Parameters<typeof parseReadEvents>[0]
184214
);
185215

216+
// Batch: group read events by instagramAccountId to avoid N+1 queries.
217+
const readEventsByAccount = new Map<
218+
string,
219+
{ userId: string; dedupeKey: string }[]
220+
>();
186221
for (const event of readEvents) {
222+
const key = event.instagramAccountId;
223+
const list = readEventsByAccount.get(key) ?? [];
224+
list.push({
225+
userId: event.userId,
226+
dedupeKey: `${event.userId}:${event.instagramAccountId}`,
227+
});
228+
readEventsByAccount.set(key, list);
229+
}
230+
231+
// One query per distinct instagramAccountId (typically 1) instead of one
232+
// per read event.
233+
for (const [igAccountId, events] of readEventsByAccount) {
234+
const userIds = [...new Set(events.map((e) => e.userId))];
187235
const openingLogs = await prisma.dmLog.findMany({
188236
where: {
189-
commenterId: event.userId,
237+
commenterId: { in: userIds },
190238
status: "SENT",
191239
automation: {
192240
isActive: true,
193241
openingDmEnabled: true,
194242
instagramAccount: {
195-
instagramId: event.instagramAccountId,
243+
instagramId: igAccountId,
196244
},
197245
},
198246
},
199247
select: {
248+
commenterId: true,
200249
automation: {
201250
select: {
202251
id: true,
@@ -205,25 +254,34 @@ export async function POST(request: NextRequest) {
205254
},
206255
});
207256

208-
const scheduledAutomationIds = new Set<string>();
257+
// Build a map of userId → Set<automationId> for dedup.
258+
const scheduledByUser = new Map<string, Set<string>>();
209259
for (const log of openingLogs) {
210-
const automation = log.automation;
211-
if (scheduledAutomationIds.has(automation.id)) continue;
212-
scheduledAutomationIds.add(automation.id);
213-
214-
await queue.add(
215-
POSTBACK_JOB_NAME,
216-
{
217-
instagramAccountId: event.instagramAccountId,
218-
userId: event.userId,
219-
payload: `reveal:${automation.id}`,
220-
fallback: true,
221-
},
222-
{
223-
delay: OPENING_DM_READ_FALLBACK_DELAY_MS,
224-
jobId: `read_fallback_${event.instagramAccountId}_${event.userId}_${automation.id}`,
225-
}
226-
);
260+
const userId = log.commenterId;
261+
const automationId = log.automation.id;
262+
const set = scheduledByUser.get(userId) ?? new Set();
263+
set.add(automationId);
264+
scheduledByUser.set(userId, set);
265+
}
266+
267+
// Schedule fallback jobs only for automations not already scheduled.
268+
for (const event of events) {
269+
const scheduled = scheduledByUser.get(event.userId) ?? new Set();
270+
for (const automationId of scheduled) {
271+
await queue.add(
272+
POSTBACK_JOB_NAME,
273+
{
274+
instagramAccountId: igAccountId,
275+
userId: event.userId,
276+
payload: `reveal:${automationId}`,
277+
fallback: true,
278+
},
279+
{
280+
delay: OPENING_DM_READ_FALLBACK_DELAY_MS,
281+
jobId: `read_fallback_${igAccountId}_${event.userId}_${automationId}`,
282+
}
283+
);
284+
}
227285
}
228286
}
229287

0 commit comments

Comments
 (0)