From d096be3357f2d9afc5d29f395081420c0f29f153 Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Sun, 22 Jun 2025 13:10:43 +0200 Subject: [PATCH] fix duplicate displayname/nickname matching issue the matching logic now handles cases where there are multiple people with the same nickname or global name --- src/app/api/guess/dataProvider.ts | 73 +++++++++++++++++++++++-------- src/app/api/guess/route.ts | 4 +- src/app/components/HomeClient.tsx | 15 ++++--- src/app/types.ts | 1 + 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/app/api/guess/dataProvider.ts b/src/app/api/guess/dataProvider.ts index 9c3c6f2..56a1ef4 100644 --- a/src/app/api/guess/dataProvider.ts +++ b/src/app/api/guess/dataProvider.ts @@ -81,7 +81,7 @@ function memberMatchesInput(member: DiscordGuildMember, normalizedInput: string) return username === normalizedInput || globalName === normalizedInput || nickname === normalizedInput; } -export async function findDiscordMember(input: string): Promise { +export async function findDiscordMember(input: string, guessedIds: string[] = []): Promise { const payingMemberRoleId = process.env.DISCORD_REQUIRED_ROLE_ID; if (!payingMemberRoleId) { @@ -96,31 +96,25 @@ export async function findDiscordMember(input: string): Promise { try { const members = await getDiscordMembers(); - const payingMembers = members.filter((member) => member.roles.includes(payingMemberRoleId)); const normalizedInput = normalizeInput(input); - // First check if any member matches the input (regardless of role) - const anyMatchingMember = members.find((member) => memberMatchesInput(member, normalizedInput)); + const allMatchingMembers = members.filter((member) => memberMatchesInput(member, normalizedInput)); - // If member exists but doesn't have the paying member role - if (anyMatchingMember && !anyMatchingMember.roles.includes(payingMemberRoleId)) { - return { - input, - name: getDisplayName(anyMatchingMember), - description: undefined, - translationKey: "notPayingMember", - correct: false, - }; + if (allMatchingMembers.length === 0) { + return { input, name: undefined, description: undefined, correct: false }; } - // Find matching member among paying members - const matchingMember = payingMembers.find((member) => memberMatchesInput(member, normalizedInput)); + const payingMembers = allMatchingMembers.filter((member) => member.roles.includes(payingMemberRoleId)); + const nonPayingMembers = allMatchingMembers.filter((member) => !member.roles.includes(payingMemberRoleId)); - if (matchingMember) { - const displayName = getDisplayName(matchingMember); - const username = matchingMember.user.username; + const unguessedPayingMember = payingMembers.find((member) => !guessedIds.includes(member.user.id)); + + if (unguessedPayingMember) { + const displayName = getDisplayName(unguessedPayingMember); + const username = unguessedPayingMember.user.username; return { + id: unguessedPayingMember.user.id, input, name: displayName, description: displayName === username ? undefined : username, @@ -128,6 +122,49 @@ export async function findDiscordMember(input: string): Promise { }; } + const unguessedNonPayingMember = nonPayingMembers.find((member) => !guessedIds.includes(member.user.id)); + + if (unguessedNonPayingMember) { + return { + id: unguessedNonPayingMember.user.id, + input, + name: getDisplayName(unguessedNonPayingMember), + description: undefined, + translationKey: "notPayingMember", + correct: false, + }; + } + + // If we are here, all matching members (paying or not) have been guessed. + // Return the first paying member match to signal a duplicate to the client. + if (payingMembers.length > 0) { + const firstPayingMatch = payingMembers[0]; + const displayName = getDisplayName(firstPayingMatch); + const username = firstPayingMatch.user.username; + + return { + id: firstPayingMatch.user.id, + input, + name: displayName, + description: displayName === username ? undefined : username, + correct: true, // It is a correct member, but already guessed. + }; + } + + // If no paying members matched, return the first non-paying member. + if (nonPayingMembers.length > 0) { + const firstNonPayingMatch = nonPayingMembers[0]; + return { + id: firstNonPayingMatch.user.id, + input, + name: getDisplayName(firstNonPayingMatch), + description: undefined, + translationKey: "notPayingMember", + correct: false, + }; + } + + // Should not be reached if allMatchingMembers.length > 0 return { input, name: undefined, diff --git a/src/app/api/guess/route.ts b/src/app/api/guess/route.ts index 21896aa..593732c 100644 --- a/src/app/api/guess/route.ts +++ b/src/app/api/guess/route.ts @@ -9,7 +9,7 @@ export async function POST(request: Request) { if (!session) { return new Response("Unauthorized", { status: 401 }); } - const { input } = await request.json(); - const match: Match = await findDiscordMember(input); + const { input, guessedIds } = await request.json(); + const match: Match = await findDiscordMember(input, guessedIds); return NextResponse.json(match); } diff --git a/src/app/components/HomeClient.tsx b/src/app/components/HomeClient.tsx index 7dbe499..1a04745 100644 --- a/src/app/components/HomeClient.tsx +++ b/src/app/components/HomeClient.tsx @@ -73,7 +73,7 @@ export default function HomeClient({ session }: { session: Session }) { e.preventDefault(); const trimmedInput = input.trim(); if (!trimmedInput) return; - if (matches.some((m) => m.input.toLowerCase() === trimmedInput.toLowerCase())) { + if (matches.some((m) => m.input.toLowerCase() === trimmedInput.toLowerCase() && !m.name)) { setRepeatMessage(t.alreadySubmitted); return; } @@ -81,19 +81,24 @@ export default function HomeClient({ session }: { session: Session }) { setLoading(true); try { + const guessedIds = matches.filter((m) => m.correct && m.id).map((m) => m.id!); + const res = await fetch("/api/guess", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ input: trimmedInput }), + body: JSON.stringify({ input: trimmedInput, guessedIds }), }); + if (res.ok) { - const data = await res.json(); - if (data.name && matches.some((m) => m.name && m.name.toLowerCase() === data.name.toLowerCase())) { + const data = (await res.json()) as Match; + + if (data.id && matches.some((m) => m.id && m.id === data.id)) { setRepeatMessage(t.alreadySubmitted); setInput(""); return; } - setMatches((prev) => [...prev, { input: trimmedInput, ...data }]); + setMatches((prev) => [...prev, data]); + if (data.correct && !timerStarted) { startTimer(); } diff --git a/src/app/types.ts b/src/app/types.ts index e071f3d..7484b0a 100644 --- a/src/app/types.ts +++ b/src/app/types.ts @@ -1,4 +1,5 @@ export interface Match { + id?: string; input: string; name: string | undefined; description: string | undefined;