mirror of
https://github.com/UiO-Gaming/name-100-uio-gamers.git
synced 2026-11-05 13:56:53 +01:00
add discord member match data provider
This commit is contained in:
4 files changed
+141
-51
No files matched your search
@@ -1,54 +1,141 @@
|
||||
import type { Match, NorwegianPersonItem } from "@/types";
|
||||
import type { DiscordGuildMember, Match } from "@/types";
|
||||
|
||||
export async function findNorwegianPerson(input: string): Promise<Match> {
|
||||
const apiUrl = `https://snl.no/api/v1/search?query=${encodeURIComponent(input)}&limit=5`;
|
||||
const response = await fetch(apiUrl);
|
||||
const data = await response.json();
|
||||
interface CacheEntry {
|
||||
data: DiscordGuildMember[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
function isNorwegianPerson(item: NorwegianPersonItem): boolean {
|
||||
// Must be a biography
|
||||
if (item.article_type_id !== 2) return false;
|
||||
// In-memory cache for Discord API responses
|
||||
let discordMembersCache: CacheEntry | null = null;
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
|
||||
if (typeof item.rank !== "number" || item.rank <= 100) return false;
|
||||
async function getDiscordMembers(): Promise<DiscordGuildMember[]> {
|
||||
const guildId = process.env.DISCORD_GUILD_ID;
|
||||
const botToken = process.env.DISCORD_BOT_TOKEN;
|
||||
|
||||
// Must mention Norwegian-ness in any relevant field
|
||||
const combinedText = [
|
||||
item.taxonomy_title || "",
|
||||
item.subject_title || "",
|
||||
item.first_sentences || "",
|
||||
item.snippet || "",
|
||||
].join(" ");
|
||||
return /(norsk|norske|norges|norge)/i.test(combinedText);
|
||||
if (!guildId || !botToken) {
|
||||
throw new Error("Missing required Discord environment variables");
|
||||
}
|
||||
|
||||
// Filter for Norwegian biographies with strong match
|
||||
const people = (data.hits || data).filter(isNorwegianPerson);
|
||||
// Check if cache is valid
|
||||
if (discordMembersCache && Date.now() - discordMembersCache.timestamp < CACHE_TTL) {
|
||||
return discordMembersCache.data;
|
||||
}
|
||||
|
||||
if (people.length > 0) {
|
||||
const person = people[0];
|
||||
const description = await (async () => {
|
||||
try {
|
||||
const articleJsonRes = await fetch(person.article_url_json);
|
||||
const articleJson = await articleJsonRes.json();
|
||||
// Fetch fresh data from Discord API with pagination
|
||||
const allMembers: DiscordGuildMember[] = [];
|
||||
let after: string | undefined = undefined;
|
||||
const limit = 1000; // Maximum allowed by Discord API
|
||||
|
||||
if (articleJson.metadata && articleJson.metadata.occupation) {
|
||||
return articleJson.metadata.occupation;
|
||||
} else if (articleJson.subject_title) {
|
||||
return articleJson.subject_title;
|
||||
}
|
||||
} catch {
|
||||
// fallback to undefined
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
do {
|
||||
const url = new URL(`https://discord.com/api/v10/guilds/${guildId}/members`);
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
if (after) {
|
||||
url.searchParams.set("after", after);
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bot ${botToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch guild members: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const members: DiscordGuildMember[] = await response.json();
|
||||
allMembers.push(...members);
|
||||
|
||||
// Update 'after' for next page (use the last member's user ID)
|
||||
if (members.length === limit) {
|
||||
after = members[members.length - 1].user.id;
|
||||
} else {
|
||||
after = undefined; // No more pages
|
||||
}
|
||||
} while (after);
|
||||
|
||||
// Update cache
|
||||
discordMembersCache = {
|
||||
data: allMembers,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
return allMembers;
|
||||
}
|
||||
|
||||
function getDisplayName(member: DiscordGuildMember): string {
|
||||
return member.nick || member.user.global_name || member.user.username;
|
||||
}
|
||||
|
||||
function normalizeInput(input: string): string {
|
||||
return input.toLowerCase().trim();
|
||||
}
|
||||
|
||||
function memberMatchesInput(member: DiscordGuildMember, normalizedInput: string): boolean {
|
||||
const username = member.user.username.toLowerCase();
|
||||
const globalName = member.user.global_name?.toLowerCase() || "";
|
||||
const nickname = member.nick?.toLowerCase() || "";
|
||||
|
||||
return username === normalizedInput || globalName === normalizedInput || nickname === normalizedInput;
|
||||
}
|
||||
|
||||
export async function findDiscordMember(input: string): Promise<Match> {
|
||||
const payingMemberRoleId = process.env.DISCORD_REQUIRED_ROLE_ID;
|
||||
|
||||
if (!payingMemberRoleId) {
|
||||
console.error("Missing required Discord environment variables");
|
||||
return {
|
||||
input,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
correct: false,
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
// Find matching member among paying members
|
||||
const matchingMember = payingMembers.find((member) => memberMatchesInput(member, normalizedInput));
|
||||
|
||||
if (matchingMember) {
|
||||
const displayName = getDisplayName(matchingMember);
|
||||
const username = matchingMember.user.username;
|
||||
|
||||
return {
|
||||
input,
|
||||
name: displayName,
|
||||
description: displayName === username ? undefined : username,
|
||||
correct: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
input,
|
||||
name: person.title || person.headword,
|
||||
description,
|
||||
correct: true,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
correct: false,
|
||||
};
|
||||
} else {
|
||||
} catch (error) {
|
||||
console.error("Error finding Discord member:", error);
|
||||
return {
|
||||
input,
|
||||
name: undefined,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { authOptions } from "@/api/auth/[...nextauth]/route";
|
||||
import type { Match } from "@/types";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { NextResponse } from "next/server";
|
||||
import { findNorwegianPerson } from "./dataProvider";
|
||||
import { findDiscordMember } from "./dataProvider";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -10,6 +10,6 @@ export async function POST(request: Request) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
const { input } = await request.json();
|
||||
const match: Match = await findNorwegianPerson(input);
|
||||
const match: Match = await findDiscordMember(input);
|
||||
return NextResponse.json(match);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export const translations = {
|
||||
accessDenied: "Access Denied",
|
||||
accessDeniedMessage: "You don't have the paying member role in the UiO Gaming Discord server.",
|
||||
tryAgain: "Try again logging in again",
|
||||
notPayingMember: "not a paying member",
|
||||
},
|
||||
no: {
|
||||
submittedAnswers: "Dine svar",
|
||||
@@ -40,5 +41,6 @@ export const translations = {
|
||||
accessDenied: "Tilgang nektet",
|
||||
accessDeniedMessage: 'Du har ikke rollen "Betalende medlem" i UiO Gaming sin Discord-server.',
|
||||
tryAgain: "Logg på igjen",
|
||||
notPayingMember: "ikke betalende medlem",
|
||||
},
|
||||
};
|
||||
+11
-10
@@ -6,14 +6,15 @@ export interface Match {
|
||||
correct: boolean;
|
||||
}
|
||||
|
||||
export interface NorwegianPersonItem {
|
||||
article_type_id: number;
|
||||
rank: number;
|
||||
taxonomy_title?: string;
|
||||
subject_title?: string;
|
||||
first_sentences?: string;
|
||||
snippet?: string;
|
||||
title?: string;
|
||||
headword?: string;
|
||||
article_url_json?: string;
|
||||
export interface DiscordGuildMember {
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
global_name: string | null;
|
||||
discriminator: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
nick: string | null;
|
||||
roles: string[];
|
||||
joined_at: string;
|
||||
}
|
||||
Reference in new issue
Block a user