Swadhin Biswas Feature/ai code reviewer (#79) · 8/16/2026
cfd400d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* @mention parsing and notification dispatch (WS2-10)
*
* Extracts `@username` references from free text and creates in-app
* notifications for the mentioned users. Mentions are resolved against
* the users table (case-insensitive), and self-mentions are skipped.
*/
import { getDatabase, schema } from "@/db";
import { eq, inArray, and, ne } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { generateId } from "@/lib/utils";
import { logger } from "@/lib/logger";
// Negative lookbehind excludes emails ([email protected]) and markdown links
const MENTION_RE = /(?<![A-Za-z0-9_.-])@([A-Za-z0-9_](?:[A-Za-z0-9_-]{0,38}[A-Za-z0-9_])?)/g;
export function extractMentions(text: string): string[] {
if (!text) return [];
const seen = new Set<string>();
const matches = text.match(MENTION_RE) || [];
for (const m of matches) {
seen.add(m.slice(1));
}
return [...seen];
}
export interface MentionNotifyOptions {
text: string;
actorId: string;
repositoryId: string;
subjectType: "issue" | "pull_request" | "comment";
subjectId: string;
url: string;
titlePrefix?: string;
}
/**
* Notify all users mentioned in `text` (excluding the actor).
* Returns the number of notifications created.
*/
export async function notifyMentionedUsers(
options: MentionNotifyOptions,
): Promise<number> {
const usernames = extractMentions(options.text);
if (usernames.length === 0) return 0;
const db = getDatabase() as NodePgDatabase<typeof schema>;
const [mentioned, actor] = await Promise.all([
db.query.users.findMany({
where: and(
inArray(schema.users.username, usernames),
ne(schema.users.id, options.actorId),
),
columns: { id: true, username: true },
}),
db.query.users.findFirst({
where: eq(schema.users.id, options.actorId),
columns: { username: true },
}),
]);
if (mentioned.length === 0) return 0;
const actorName = actor?.username || "Someone";
const prefix = options.titlePrefix || "mentioned you";
const now = new Date();
const snippet = options.text.replace(/\s+/g, " ").slice(0, 140);
try {
await db.insert(schema.notifications).values(
mentioned.map((u) => ({
id: generateId(),
userId: u.id,
repositoryId: options.repositoryId,
type: "mention",
title: `@${actorName} ${prefix}`,
body: `@${actorName} mentioned you in ${options.subjectType}: ${snippet}`,
url: options.url,
actorId: options.actorId,
subjectType: options.subjectType,
subjectId: options.subjectId,
reason: "mention",
isRead: false,
isArchived: false,
createdAt: now,
updatedAt: now,
})),
);
} catch (err) {
logger.error({ err, usernames: mentioned.map((u) => u.username) }, "Failed to create mention notifications");
return 0;
}
return mentioned.length;
}