mirror of
https://github.com/Pandipipas/scoreko-dev.git
synced 2026-06-06 03:32:06 +00:00
Merge pull request #93 from Pandipipas/refactor-startgg-function
refactor(startgg): limpiar flujo OAuth y parsing de payloads
This commit is contained in:
@@ -275,13 +275,13 @@ const checkOAuthStatus = async () => {
|
|||||||
oauthLoading.value = false;
|
oauthLoading.value = false;
|
||||||
clearOAuthPolling();
|
clearOAuthPolling();
|
||||||
oauthSessionId.value = '';
|
oauthSessionId.value = '';
|
||||||
tournamentsError.value = status.error || 'No se pudo completar el login OAuth con start.gg.';
|
tournamentsError.value = status.error || 'Could not complete OAuth login with start.gg.';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
oauthLoading.value = false;
|
oauthLoading.value = false;
|
||||||
clearOAuthPolling();
|
clearOAuthPolling();
|
||||||
oauthSessionId.value = '';
|
oauthSessionId.value = '';
|
||||||
tournamentsError.value = error instanceof Error ? error.message : 'No se pudo verificar el estado OAuth.';
|
tournamentsError.value = error instanceof Error ? error.message : 'Could not verify OAuth status.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ const connectWithStartGGOAuth = async () => {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
oauthLoading.value = false;
|
oauthLoading.value = false;
|
||||||
tournamentsError.value = error instanceof Error ? error.message : 'No se pudo iniciar OAuth con start.gg.';
|
tournamentsError.value = error instanceof Error ? error.message : 'Could not start OAuth with start.gg.';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,7 +325,7 @@ const saveManualToken = () => {
|
|||||||
const loadRecentTournaments = async () => {
|
const loadRecentTournaments = async () => {
|
||||||
const token = startGGToken.value.trim();
|
const token = startGGToken.value.trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
tournamentsError.value = 'Añade tu token de start.gg para cargar torneos.';
|
tournamentsError.value = 'Add your start.gg token to load tournaments.';
|
||||||
recentTournaments.value = [];
|
recentTournaments.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -338,10 +338,10 @@ const loadRecentTournaments = async () => {
|
|||||||
});
|
});
|
||||||
recentTournaments.value = tournaments;
|
recentTournaments.value = tournaments;
|
||||||
if (!tournaments.length) {
|
if (!tournaments.length) {
|
||||||
tournamentsError.value = 'No hay torneos recientes para esta cuenta.';
|
tournamentsError.value = 'There are no recent tournaments for this account.';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
tournamentsError.value = error instanceof Error ? error.message : 'No se pudieron cargar torneos.';
|
tournamentsError.value = error instanceof Error ? error.message : 'Could not load tournaments.';
|
||||||
recentTournaments.value = [];
|
recentTournaments.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
loadingTournaments.value = false;
|
loadingTournaments.value = false;
|
||||||
@@ -365,7 +365,7 @@ const openStartGGImportDialog = async (tournament: StartGGTournament) => {
|
|||||||
startGGPlayers.value = importedPlayers;
|
startGGPlayers.value = importedPlayers;
|
||||||
selectedStartGGPlayerIds.value = importedPlayers.map((player) => player.id);
|
selectedStartGGPlayerIds.value = importedPlayers.map((player) => player.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'No se pudieron cargar jugadores';
|
const message = error instanceof Error ? error.message : 'Could not load players';
|
||||||
window.alert(message);
|
window.alert(message);
|
||||||
isImportDialogOpen.value = false;
|
isImportDialogOpen.value = false;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -599,7 +599,7 @@ onBeforeUnmount(() => {
|
|||||||
<span>StartGG</span>
|
<span>StartGG</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-caption q-mb-md">
|
<div class="text-caption q-mb-md">
|
||||||
Conecta por OAuth (recomendado) o pega tu token personal para cargar tus torneos creados o donde eres admin. Si aparece "Client authentication failed", revisa que en config uses el Client ID/Secret de un OAuth App de start.gg.
|
Connect via OAuth (recommended) or paste your personal token to load tournaments you created or administrate. If you see "Client authentication failed", verify your config uses the Client ID/Secret from a start.gg OAuth App.
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-sm items-center">
|
<div class="row q-col-gutter-sm items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
@@ -607,7 +607,7 @@ onBeforeUnmount(() => {
|
|||||||
v-if="!hasStartGGTokenConfigured"
|
v-if="!hasStartGGTokenConfigured"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="login"
|
icon="login"
|
||||||
label="Conectar con start.gg"
|
label="Connect with start.gg"
|
||||||
:loading="oauthLoading"
|
:loading="oauthLoading"
|
||||||
@click="connectWithStartGGOAuth"
|
@click="connectWithStartGGOAuth"
|
||||||
/>
|
/>
|
||||||
@@ -616,7 +616,7 @@ onBeforeUnmount(() => {
|
|||||||
outline
|
outline
|
||||||
color="positive"
|
color="positive"
|
||||||
icon="check_circle"
|
icon="check_circle"
|
||||||
label="Conectado"
|
label="Connected"
|
||||||
class="startgg-connected-btn"
|
class="startgg-connected-btn"
|
||||||
@click="openManualTokenDialog"
|
@click="openManualTokenDialog"
|
||||||
/>
|
/>
|
||||||
@@ -626,7 +626,7 @@ onBeforeUnmount(() => {
|
|||||||
outline
|
outline
|
||||||
color="white"
|
color="white"
|
||||||
icon="vpn_key"
|
icon="vpn_key"
|
||||||
label="Usar API personal"
|
label="Use personal API"
|
||||||
@click="openManualTokenDialog"
|
@click="openManualTokenDialog"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -663,7 +663,7 @@ onBeforeUnmount(() => {
|
|||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
clearable
|
clearable
|
||||||
dense
|
dense
|
||||||
label="Torneo"
|
label="Tournament"
|
||||||
class="players-underlined-field"
|
class="players-underlined-field"
|
||||||
@filter="filterTournaments"
|
@filter="filterTournaments"
|
||||||
>
|
>
|
||||||
@@ -686,9 +686,13 @@ onBeforeUnmount(() => {
|
|||||||
<QBtn
|
<QBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
unelevated
|
unelevated
|
||||||
label="Importar jugadores"
|
round
|
||||||
|
icon="person_add"
|
||||||
|
aria-label="Import players"
|
||||||
@click="openSelectedTournamentImportDialog"
|
@click="openSelectedTournamentImportDialog"
|
||||||
/>
|
>
|
||||||
|
<QTooltip>Import players</QTooltip>
|
||||||
|
</QBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</QCard>
|
</QCard>
|
||||||
@@ -700,24 +704,24 @@ onBeforeUnmount(() => {
|
|||||||
<QCard class="players-dialog">
|
<QCard class="players-dialog">
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
API personal de start.gg
|
Personal start.gg API
|
||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
<QSeparator />
|
<QSeparator />
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-body2 q-mb-sm">
|
<div class="text-body2 q-mb-sm">
|
||||||
Si OAuth falla, puedes crear tu token personal manualmente con estos pasos:
|
If OAuth fails, you can create your personal token manually with these steps:
|
||||||
</div>
|
</div>
|
||||||
<ol class="q-pl-md q-mb-md manual-token-steps">
|
<ol class="q-pl-md q-mb-md manual-token-steps">
|
||||||
<li>Ir a https://start.gg/admin/profile/developer</li>
|
<li>Go to https://start.gg/admin/profile/developer</li>
|
||||||
<li>Iniciar sesión con tu cuenta</li>
|
<li>Sign in with your account</li>
|
||||||
<li>De los 3 access tokens, clicar en <strong>Third Party</strong></li>
|
<li>From the 3 access tokens, click <strong>Third Party</strong></li>
|
||||||
<li>Crear uno nuevo y cubrir la descripción con el nombre que quieras</li>
|
<li>Create a new one and fill the description with any name you want</li>
|
||||||
<li>Copiar el token generado y pegarlo en Scoreko</li>
|
<li>Copy the generated token and paste it into Scoreko</li>
|
||||||
</ol>
|
</ol>
|
||||||
<QInput
|
<QInput
|
||||||
v-model="manualTokenDraft"
|
v-model="manualTokenDraft"
|
||||||
label="Pega tu token personal"
|
label="Paste your personal token"
|
||||||
dense
|
dense
|
||||||
outlined
|
outlined
|
||||||
type="password"
|
type="password"
|
||||||
@@ -727,19 +731,19 @@ onBeforeUnmount(() => {
|
|||||||
<QCardActions align="right">
|
<QCardActions align="right">
|
||||||
<QBtn
|
<QBtn
|
||||||
flat
|
flat
|
||||||
label="Cancelar"
|
label="Cancel"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@click="isManualTokenDialogOpen = false"
|
@click="isManualTokenDialogOpen = false"
|
||||||
/>
|
/>
|
||||||
<QBtn
|
<QBtn
|
||||||
flat
|
flat
|
||||||
color="negative"
|
color="negative"
|
||||||
label="Borrar token"
|
label="Delete token"
|
||||||
@click="manualTokenDraft = ''; saveManualToken()"
|
@click="manualTokenDraft = ''; saveManualToken()"
|
||||||
/>
|
/>
|
||||||
<QBtn
|
<QBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Guardar token"
|
label="Save token"
|
||||||
@click="saveManualToken"
|
@click="saveManualToken"
|
||||||
/>
|
/>
|
||||||
</QCardActions>
|
</QCardActions>
|
||||||
@@ -750,7 +754,7 @@ onBeforeUnmount(() => {
|
|||||||
<QCard class="players-dialog">
|
<QCard class="players-dialog">
|
||||||
<QCardSection>
|
<QCardSection>
|
||||||
<div class="text-h6">
|
<div class="text-h6">
|
||||||
Importar desde {{ importingTournament?.name || 'start.gg' }}
|
Import from {{ importingTournament?.name || 'start.gg' }}
|
||||||
</div>
|
</div>
|
||||||
</QCardSection>
|
</QCardSection>
|
||||||
<QSeparator />
|
<QSeparator />
|
||||||
@@ -760,7 +764,7 @@ onBeforeUnmount(() => {
|
|||||||
class="row items-center q-gutter-sm"
|
class="row items-center q-gutter-sm"
|
||||||
>
|
>
|
||||||
<QSpinner />
|
<QSpinner />
|
||||||
<span>Cargando inscritos...</span>
|
<span>Loading participants...</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<QOptionGroup
|
<QOptionGroup
|
||||||
@@ -777,13 +781,13 @@ onBeforeUnmount(() => {
|
|||||||
<QCardActions align="right">
|
<QCardActions align="right">
|
||||||
<QBtn
|
<QBtn
|
||||||
flat
|
flat
|
||||||
label="Cancelar"
|
label="Cancel"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@click="isImportDialogOpen = false"
|
@click="isImportDialogOpen = false"
|
||||||
/>
|
/>
|
||||||
<QBtn
|
<QBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
label="Importar seleccionados"
|
label="Import selected"
|
||||||
:disable="!selectedStartGGPlayerIds.length"
|
:disable="!selectedStartGGPlayerIds.length"
|
||||||
@click="importSelectedStartGGPlayers"
|
@click="importSelectedStartGGPlayers"
|
||||||
/>
|
/>
|
||||||
|
|||||||
+61
-65
@@ -1,4 +1,4 @@
|
|||||||
import { createServer, type Server } from 'node:http';
|
import { createServer, type Server, type ServerResponse } from 'node:http';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { getData, type CountryRecord } from 'country-list';
|
import { getData, type CountryRecord } from 'country-list';
|
||||||
import { nodecg } from './util/nodecg.js';
|
import { nodecg } from './util/nodecg.js';
|
||||||
@@ -64,6 +64,27 @@ interface OAuthTokenResponse {
|
|||||||
const oauthSessions = new Map<string, OAuthSession>();
|
const oauthSessions = new Map<string, OAuthSession>();
|
||||||
let oauthCallbackServer: Server | null = null;
|
let oauthCallbackServer: Server | null = null;
|
||||||
|
|
||||||
|
const getStringProp = (payload: unknown, key: string): string => {
|
||||||
|
if (typeof payload !== 'object' || payload === null || !(key in payload)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = (payload as Record<string, unknown>)[key];
|
||||||
|
return typeof value === 'string' ? value.trim() : String(value || '').trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOAuthSession = (sessionId: string, update: Partial<OAuthSession>) => {
|
||||||
|
const session = oauthSessions.get(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
oauthSessions.set(sessionId, {
|
||||||
|
...session,
|
||||||
|
...update,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const requestStartGG = async <T>(query: string, variables: Record<string, unknown>, token: string): Promise<T> => {
|
const requestStartGG = async <T>(query: string, variables: Record<string, unknown>, token: string): Promise<T> => {
|
||||||
const response = await fetch(STARTGG_ENDPOINT, {
|
const response = await fetch(STARTGG_ENDPOINT, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -75,10 +96,16 @@ const requestStartGG = async <T>(query: string, variables: Record<string, unknow
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`start.gg responded with ${response.status}`);
|
throw new Error(`start.gg responded with ${response.status} ${response.statusText}`.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: StartGGGraphQLResponse<T>;
|
||||||
|
try {
|
||||||
|
payload = (await response.json()) as StartGGGraphQLResponse<T>;
|
||||||
|
} catch {
|
||||||
|
throw new Error('Invalid JSON response from start.gg');
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = (await response.json()) as StartGGGraphQLResponse<T>;
|
|
||||||
if (payload.errors?.length) {
|
if (payload.errors?.length) {
|
||||||
throw new Error(payload.errors[0]?.message || 'Unknown start.gg error');
|
throw new Error(payload.errors[0]?.message || 'Unknown start.gg error');
|
||||||
}
|
}
|
||||||
@@ -137,16 +164,19 @@ const getCallbackUrl = (callbackPort: number) => `http://127.0.0.1:${callbackPor
|
|||||||
|
|
||||||
const cleanupExpiredOAuthSessions = () => {
|
const cleanupExpiredOAuthSessions = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
oauthSessions.forEach((session) => {
|
oauthSessions.forEach((session, sessionId) => {
|
||||||
if (session.expiresAt <= now && session.status === 'pending') {
|
if (session.expiresAt <= now && session.status === 'pending') {
|
||||||
oauthSessions.set(session.sessionId, {
|
updateOAuthSession(sessionId, { status: 'expired' });
|
||||||
...session,
|
|
||||||
status: 'expired',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const respondWithCallbackHtml = (res: ServerResponse, statusCode: number, title: string, message: string) => {
|
||||||
|
res.statusCode = statusCode;
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.end(renderCallbackHtml(title, message));
|
||||||
|
};
|
||||||
|
|
||||||
const renderCallbackHtml = (title: string, message: string) => `<!doctype html>
|
const renderCallbackHtml = (title: string, message: string) => `<!doctype html>
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
@@ -163,7 +193,7 @@ const renderCallbackHtml = (title: string, message: string) => `<!doctype html>
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<h2>${title}</h2>
|
<h2>${title}</h2>
|
||||||
<p>${message}</p>
|
<p>${message}</p>
|
||||||
<p>Puedes cerrar esta pestaña y volver a Scoreko.</p>
|
<p>You can close this tab and return to Scoreko.</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
@@ -252,68 +282,41 @@ const ensureOAuthCallbackServer = async (oauthConfig: OAuthConfig) => {
|
|||||||
|
|
||||||
const session = Array.from(oauthSessions.values()).find((candidate) => candidate.state === state);
|
const session = Array.from(oauthSessions.values()).find((candidate) => candidate.state === state);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
res.statusCode = 400;
|
respondWithCallbackHtml(res, 400, 'Invalid OAuth', 'No active session was found for this authorization.');
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
res.end(renderCallbackHtml('OAuth inválido', 'No se encontró una sesión activa para esta autorización.'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.expiresAt <= Date.now()) {
|
if (session.expiresAt <= Date.now()) {
|
||||||
oauthSessions.set(session.sessionId, {
|
updateOAuthSession(session.sessionId, { status: 'expired' });
|
||||||
...session,
|
respondWithCallbackHtml(res, 400, 'Session expired', 'The OAuth session expired. Start the process again from Scoreko.');
|
||||||
status: 'expired',
|
|
||||||
});
|
|
||||||
res.statusCode = 400;
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
res.end(renderCallbackHtml('Sesión expirada', 'La sesión OAuth expiró. Vuelve a iniciar el proceso desde Scoreko.'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
oauthSessions.set(session.sessionId, {
|
updateOAuthSession(session.sessionId, { status: 'error', error });
|
||||||
...session,
|
respondWithCallbackHtml(res, 400, 'OAuth canceled', `start.gg returned this error: ${error}`);
|
||||||
status: 'error',
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
res.statusCode = 400;
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
res.end(renderCallbackHtml('OAuth cancelado', `start.gg devolvió el error: ${error}`));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
oauthSessions.set(session.sessionId, {
|
updateOAuthSession(session.sessionId, {
|
||||||
...session,
|
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: 'Missing authorization code',
|
error: 'Missing authorization code',
|
||||||
});
|
});
|
||||||
res.statusCode = 400;
|
respondWithCallbackHtml(res, 400, 'Incomplete OAuth', 'No authorization code was received.');
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
res.end(renderCallbackHtml('OAuth incompleto', 'No se recibió un código de autorización.'));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void exchangeOAuthCodeForToken(code, callbackUrl, oauthConfig)
|
void exchangeOAuthCodeForToken(code, callbackUrl, oauthConfig)
|
||||||
.then((token) => {
|
.then((token) => {
|
||||||
oauthSessions.set(session.sessionId, {
|
updateOAuthSession(session.sessionId, { status: 'completed', token, error: undefined });
|
||||||
...session,
|
|
||||||
status: 'completed',
|
|
||||||
token,
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((exchangeError) => {
|
.catch((exchangeError) => {
|
||||||
const message = exchangeError instanceof Error ? exchangeError.message : 'Failed to exchange authorization code';
|
const message = exchangeError instanceof Error ? exchangeError.message : 'Failed to exchange authorization code';
|
||||||
oauthSessions.set(session.sessionId, {
|
updateOAuthSession(session.sessionId, { status: 'error', error: message });
|
||||||
...session,
|
|
||||||
status: 'error',
|
|
||||||
error: message,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
res.statusCode = 200;
|
respondWithCallbackHtml(res, 200, 'Authorization received', 'Your authorization was received. Finishing sign-in in the background...');
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
res.end(renderCallbackHtml('Autorización recibida', 'Se recibió tu autorización. Finalizando el login en segundo plano...'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -330,14 +333,14 @@ const ensureOAuthCallbackServer = async (oauthConfig: OAuthConfig) => {
|
|||||||
nodecg.listenFor('startgg:createOAuthSession', async (_payload: unknown, ack) => {
|
nodecg.listenFor('startgg:createOAuthSession', async (_payload: unknown, ack) => {
|
||||||
const oauthConfig = getOAuthConfig();
|
const oauthConfig = getOAuthConfig();
|
||||||
if (!oauthConfig) {
|
if (!oauthConfig) {
|
||||||
sendAck(ack, 'OAuth no está configurado en esta instalación (faltan startggClientId/startggClientSecret). Usa el Client ID y Client Secret del OAuth app de start.gg.');
|
sendAck(ack, 'OAuth is not configured in this installation (missing startggClientId/startggClientSecret). Use the Client ID and Client Secret from a start.gg OAuth app.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureOAuthCallbackServer(oauthConfig);
|
await ensureOAuthCallbackServer(oauthConfig);
|
||||||
} catch (serverError) {
|
} catch (serverError) {
|
||||||
const message = serverError instanceof Error ? serverError.message : 'No se pudo iniciar el callback OAuth local';
|
const message = serverError instanceof Error ? serverError.message : 'Could not start the local OAuth callback';
|
||||||
sendAck(ack, message);
|
sendAck(ack, message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -371,9 +374,7 @@ nodecg.listenFor('startgg:createOAuthSession', async (_payload: unknown, ack) =>
|
|||||||
nodecg.listenFor('startgg:getOAuthSessionStatus', (payload: unknown, ack) => {
|
nodecg.listenFor('startgg:getOAuthSessionStatus', (payload: unknown, ack) => {
|
||||||
cleanupExpiredOAuthSessions();
|
cleanupExpiredOAuthSessions();
|
||||||
|
|
||||||
const sessionId = typeof payload === 'object' && payload !== null && 'sessionId' in payload
|
const sessionId = getStringProp(payload, 'sessionId');
|
||||||
? String((payload as { sessionId?: string }).sessionId || '').trim()
|
|
||||||
: '';
|
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
sendAck(ack, 'Missing OAuth session id');
|
sendAck(ack, 'Missing OAuth session id');
|
||||||
@@ -394,9 +395,7 @@ nodecg.listenFor('startgg:getOAuthSessionStatus', (payload: unknown, ack) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack) => {
|
nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack) => {
|
||||||
const token = typeof payload === 'object' && payload !== null && 'token' in payload
|
const token = getStringProp(payload, 'token');
|
||||||
? String((payload as { token?: string }).token || '').trim()
|
|
||||||
: '';
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
sendAck(ack, 'Missing start.gg API token');
|
sendAck(ack, 'Missing start.gg API token');
|
||||||
@@ -443,12 +442,8 @@ nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack)
|
|||||||
});
|
});
|
||||||
|
|
||||||
nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack) => {
|
nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack) => {
|
||||||
const candidate = typeof payload === 'object' && payload !== null ? payload as {
|
const token = getStringProp(payload, 'token');
|
||||||
token?: string;
|
const slug = getStringProp(payload, 'slug');
|
||||||
slug?: string;
|
|
||||||
} : {};
|
|
||||||
const token = String(candidate.token || '').trim();
|
|
||||||
const slug = String(candidate.slug || '').trim();
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
sendAck(ack, 'Missing start.gg API token');
|
sendAck(ack, 'Missing start.gg API token');
|
||||||
@@ -485,7 +480,7 @@ nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack)
|
|||||||
try {
|
try {
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let totalPages = 1;
|
let totalPages = 1;
|
||||||
const playersMap: Record<string, ImportedPlayer> = {};
|
const playersMap = new Map<string, ImportedPlayer>();
|
||||||
|
|
||||||
while (currentPage <= totalPages) {
|
while (currentPage <= totalPages) {
|
||||||
const data = await requestStartGG<{
|
const data = await requestStartGG<{
|
||||||
@@ -514,7 +509,8 @@ nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack)
|
|||||||
throw new Error('Tournament not found');
|
throw new Error('Tournament not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages = Math.max(data.tournament.participants.pageInfo.totalPages || 1, 1);
|
const apiTotalPages = Number(data.tournament.participants.pageInfo.totalPages);
|
||||||
|
totalPages = Number.isFinite(apiTotalPages) ? Math.max(apiTotalPages, 1) : 1;
|
||||||
|
|
||||||
data.tournament.participants.nodes.forEach((participant) => {
|
data.tournament.participants.nodes.forEach((participant) => {
|
||||||
const playerId = String(participant.id);
|
const playerId = String(participant.id);
|
||||||
@@ -523,20 +519,20 @@ nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const country = resolveCountryCodeFromStartGG(participant.user?.location?.country);
|
const country = resolveCountryCodeFromStartGG(participant.user?.location?.country);
|
||||||
playersMap[playerId] = {
|
playersMap.set(playerId, {
|
||||||
id: playerId,
|
id: playerId,
|
||||||
gamertag,
|
gamertag,
|
||||||
name: gamertag,
|
name: gamertag,
|
||||||
team: (participant.prefix || '').trim(),
|
team: (participant.prefix || '').trim(),
|
||||||
country,
|
country,
|
||||||
twitter: '',
|
twitter: '',
|
||||||
};
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
currentPage += 1;
|
currentPage += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAck(ack, null, Object.values(playersMap));
|
sendAck(ack, null, Array.from(playersMap.values()));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error while importing players';
|
const message = error instanceof Error ? error.message : 'Unknown error while importing players';
|
||||||
sendAck(ack, message);
|
sendAck(ack, message);
|
||||||
|
|||||||
Reference in New Issue
Block a user