Add start.gg tournament and player import integration

This commit is contained in:
Pandipipas
2026-02-15 23:57:31 +01:00
parent 45fd2e2deb
commit a83f633506
3 changed files with 454 additions and 1 deletions
+225
View File
@@ -0,0 +1,225 @@
import { getData, type CountryRecord } from 'country-list';
import { nodecg } from './util/nodecg.js';
const STARTGG_ENDPOINT = 'https://api.start.gg/gql/alpha';
const RECENT_TOURNAMENTS_LIMIT = 12;
const PARTICIPANTS_PAGE_SIZE = 120;
interface StartGGGraphQLResponse<T> {
data?: T;
errors?: Array<{ message?: string }>;
}
interface RecentTournament {
id: number;
name: string;
slug: string;
startAt: number | null;
}
interface ImportedPlayer {
id: string;
gamertag: string;
name: string;
team: string;
country: string;
twitter: string;
}
const requestStartGG = async <T>(query: string, variables: Record<string, unknown>, token: string): Promise<T> => {
const response = await fetch(STARTGG_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`start.gg responded with ${response.status}`);
}
const payload = (await response.json()) as StartGGGraphQLResponse<T>;
if (payload.errors?.length) {
throw new Error(payload.errors[0]?.message || 'Unknown start.gg error');
}
if (!payload.data) {
throw new Error('No data returned by start.gg');
}
return payload.data;
};
const countryByCode = new Set(getData().map((country: CountryRecord) => country.code.toUpperCase()));
const countryByName = new Map(getData().map((country: CountryRecord) => [country.name.toLowerCase(), country.code.toUpperCase()]));
const resolveCountryCodeFromStartGG = (country: string | null | undefined): string => {
const raw = (country || '').trim();
if (!raw) {
return '';
}
const upper = raw.toUpperCase();
if (countryByCode.has(upper)) {
return upper;
}
return countryByName.get(raw.toLowerCase()) ?? '';
};
const sendAck = (ack: unknown, error: string | null, response?: unknown) => {
if (typeof ack !== 'function') {
return;
}
ack(error, response);
};
nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack) => {
const token = typeof payload === 'object' && payload !== null && 'token' in payload
? String((payload as { token?: string }).token || '').trim()
: '';
if (!token) {
sendAck(ack, 'Missing start.gg API token');
return;
}
const query = `
query RecentTournaments($perPage: Int!) {
currentUser {
tournaments(query: { perPage: $perPage }) {
nodes {
id
name
slug
startAt
}
}
}
}
`;
try {
const data = await requestStartGG<{
currentUser: { tournaments: { nodes: RecentTournament[] } } | null;
}>(query, { perPage: RECENT_TOURNAMENTS_LIMIT }, token);
const tournaments = data.currentUser?.tournaments.nodes
.filter((item) => item.slug)
.sort((a, b) => (b.startAt ?? 0) - (a.startAt ?? 0))
.map((item) => ({
id: item.id,
name: item.name,
slug: item.slug,
startAt: item.startAt,
})) ?? [];
sendAck(ack, null, tournaments);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error while loading tournaments';
sendAck(ack, message);
}
});
nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack) => {
const candidate = typeof payload === 'object' && payload !== null ? payload as {
token?: string;
slug?: string;
} : {};
const token = String(candidate.token || '').trim();
const slug = String(candidate.slug || '').trim();
if (!token) {
sendAck(ack, 'Missing start.gg API token');
return;
}
if (!slug) {
sendAck(ack, 'Missing tournament slug');
return;
}
const query = `
query TournamentParticipants($slug: String!, $page: Int!, $perPage: Int!) {
tournament(slug: $slug) {
participants(query: { page: $page, perPage: $perPage }) {
pageInfo {
totalPages
}
nodes {
id
gamerTag
prefix
user {
location {
country
}
}
}
}
}
}
`;
try {
let currentPage = 1;
let totalPages = 1;
const playersMap: Record<string, ImportedPlayer> = {};
while (currentPage <= totalPages) {
const data = await requestStartGG<{
tournament: {
participants: {
pageInfo: { totalPages: number };
nodes: Array<{
id: number;
gamerTag: string | null;
prefix: string | null;
user: {
location: {
country: string | null;
} | null;
} | null;
}>;
};
} | null;
}>(query, {
slug,
page: currentPage,
perPage: PARTICIPANTS_PAGE_SIZE,
}, token);
if (!data.tournament) {
throw new Error('Tournament not found');
}
totalPages = Math.max(data.tournament.participants.pageInfo.totalPages || 1, 1);
data.tournament.participants.nodes.forEach((participant) => {
const playerId = String(participant.id);
const gamertag = (participant.gamerTag || '').trim();
if (!gamertag) {
return;
}
const country = resolveCountryCodeFromStartGG(participant.user?.location?.country);
playersMap[playerId] = {
id: playerId,
gamertag,
name: gamertag,
team: (participant.prefix || '').trim(),
country,
twitter: '',
};
});
currentPage += 1;
}
sendAck(ack, null, Object.values(playersMap));
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error while importing players';
sendAck(ack, message);
}
});