Abgleich in der README beschreiben und Adresse konfigurierbar machen

Der Abgleich liest GExtension-Profile und ist auf deren Seitenaufbau gebaut.
Damit niemand in einer anderen Community lange sucht, warum nichts passiert,
steht das jetzt als Warnhinweis am Anfang des Abschnitts: bei einer anderen
GExtension-Instanz reicht GEXTENSION_BASE_URL, bei einem voellig anderen
Shop-System muesste der Parser angepasst werden. Der Rest der App laeuft
unabhaengig davon.

Die Adresse war bisher fest im Code verdrahtet und kommt jetzt aus der
Umgebung. Der Parser selbst bleibt frei von Konfiguration und Datenbank,
damit er ohne beides testbar ist - die Adresse reicht der Service durch.
This commit is contained in:
2026-09-18 15:48:21 +02:00
parent c242e73c38
commit 7876e29b90
6 changed files with 76 additions and 5 deletions
+4
View File
@@ -27,6 +27,10 @@ const schema = z.object({
.optional()
.transform((v) => (v === undefined ? undefined : v === 'true' || v === '1')),
// Basis-Adresse der GExtension-Instanz. Der Parser ist auf deren Aufbau
// zugeschnitten; eine voellig andere Software liesse sich damit nicht lesen.
GEXTENSION_BASE_URL: z.string().url().default('https://gextension.senseigaming.de'),
// Aufbewahrung der Audit-Eintraege in Tagen. 0 schaltet das Aufraeumen ab.
// Die Guthaben-Historie ist davon nicht betroffen, die bleibt dauerhaft.
AUDIT_RETENTION_DAYS: z.coerce.number().int().min(0).default(365),
+16 -3
View File
@@ -132,7 +132,9 @@ export function classifyPackage(pkg: ScrapedPackage): Classification {
return { kind: 'ignore' };
}
const PROFILE_URL = 'https://gextension.senseigaming.de/index.php?t=user&id=';
// Standard ist die Instanz, fuer die der Parser gebaut wurde. Laeuft GExtension
// bei einer anderen Community unter eigener Domain, reicht ein anderer Wert.
export const DEFAULT_BASE_URL = 'https://gextension.senseigaming.de';
// Gemeinsam genutzte Community-Infrastruktur: hoechstens eine Anfrage pro Sekunde.
const MIN_REQUEST_GAP_MS = 1000;
@@ -144,17 +146,28 @@ async function throttle(): Promise<void> {
lastRequestAt = Date.now();
}
export async function fetchProfile(steamId: string, timeoutMs = 15000): Promise<ScrapedProfile> {
export interface FetchOptions {
baseUrl?: string;
timeoutMs?: number;
}
export async function fetchProfile(
steamId: string,
options: FetchOptions = {},
): Promise<ScrapedProfile> {
const steamId64 = normalizeSteamId(steamId);
if (!steamId64) throw badRequest('INVALID_STEAMID', `Keine gueltige SteamID: ${steamId}`);
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
const timeoutMs = options.timeoutMs ?? 15000;
await throttle();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${PROFILE_URL}${steamId64}`, {
const response = await fetch(`${baseUrl}/index.php?t=user&id=${steamId64}`, {
signal: controller.signal,
headers: {
// Ehrliche Kennzeichnung statt Browser-Tarnung.
+2 -1
View File
@@ -1,5 +1,6 @@
import { ImportStatus, type Prisma } from '@prisma/client';
import { prisma } from '../db.js';
import { env } from '../env.js';
import { badRequest } from '../errors.js';
import {
classifyPackage,
@@ -59,7 +60,7 @@ export async function scrapePlayers(
for (const target of targets) {
let profile: ScrapedProfile;
try {
profile = await fetchProfile(target.steamId);
profile = await fetchProfile(target.steamId, { baseUrl: env.GEXTENSION_BASE_URL });
summary.scannedPlayers += 1;
} catch (error) {
summary.failedPlayers.push({