diff --git a/README.md b/README.md index 46e5b09..30b4fe2 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,12 @@ CC kehrt zurück - gratis, sofern er nicht redesignt wurde), *abgelehnt* oder *k angenommenen Fälle lösen exakt dieselben Buchungen aus wie die Knöpfe auf der Spielerseite - es gibt keinen zweiten Buchungsweg mit eigenen Regeln. +Gelesen wird dabei ausdrücklich der **Ersteller**, nicht der letzte Antwortende. Wer sein Konto +gelöscht hat, steht in der Liste ohne Profil-Link da - wer dann einfach den ersten Profil-Link +der Zeile nimmt, erwischt den letzten Antwortenden und ordnet den Antrag dem Falschen zu. +Ändert sich der Ersteller eines bereits bekannten Threads, wird seine Zuordnung neu bestimmt; +eine von Hand gesetzte bleibt dabei stehen. + Aus dem Abgleich kommt nebenbei die Adresse des Forumsprofils mit; auf der Spielerseite führt der Forum-Name dann direkt dorthin. Die SteamID verlinkt entsprechend auf das GExtension-Profil. diff --git a/server/src/lib/forum.ts b/server/src/lib/forum.ts index 4459dac..4e1a74e 100644 --- a/server/src/lib/forum.ts +++ b/server/src/lib/forum.ts @@ -45,11 +45,23 @@ const TOPIC_LINK = const USER_LINK = /]*class="userLink"[^>]*>(?:]*><\/i>)?\s*(?:]*>)?\s*([^<]{1,80})/; -// Wer sein Konto gelöscht hat, steht ohne Profillink in der Zelle. Der Name -// ist trotzdem da und bleibt die einzige Spur zum Spieler - ohne diesen -// Rückfall fallen solche Threads bei der Zuordnung durchs Raster. +/** + * Die Zelle mit dem Ersteller. Dort steht er verlässlich - mit Profillink, + * wenn es das Konto noch gibt, sonst als blosser Text. + */ const AUTHOR_CELL = /
  • ([\s\S]{0,400}?)<\/li>/; +/** + * Ab hier beginnt die Spalte mit dem **letzten Beitrag**. + * + * Dort steht ein anderer Name, und wer ihn erwischt, ordnet den Antrag dem + * falschen Spieler zu. Das passiert genau dann, wenn der Ersteller sein Konto + * geloescht hat: er hat dann keinen Profillink, der letzte Antwortende schon. + * Deshalb wird die Zeile vorher abgeschnitten, statt sich auf die Reihenfolge + * der Treffer zu verlassen. + */ +const LAST_POST_COLUMN = /
  • \s*Verschoben\s*]*>/g, ' ')) || null; + if (authorName === null && zelle) { + authorName = decode((zelle[1] ?? '').replace(/<[^>]*>/g, ' ')) || null; } LABEL.lastIndex = 0; diff --git a/server/src/services/forum.ts b/server/src/services/forum.ts index 784216f..0eb5735 100644 --- a/server/src/services/forum.ts +++ b/server/src/services/forum.ts @@ -110,6 +110,9 @@ export interface ForumScanSummary { /// in der Arbeitsliste, gehen aber auch nicht verloren. unassignedThreads: number; matchedPlayers: number; + /// Threads, deren Ersteller sich geändert hat und die deshalb einem anderen + /// Spieler zugeschlagen wurden. + reassigned: number; /// Wie oft das Forum den Abruf gebremst hat. Über null heißt: der Lauf hat /// länger gedauert, gelesen wurde trotzdem alles. rateLimited: number; @@ -230,6 +233,7 @@ async function scanIntern(ctx: ActionContext): Promise { skippedLabels: 0, unassignedThreads: 0, matchedPlayers: 0, + reassigned: 0, rateLimited: 0, failedBoards: [], }; @@ -364,6 +368,31 @@ async function scanIntern(ctx: ActionContext): Promise { // Eine später nachgetragene Zuordnung nicht wieder überschreiben. const nachgetragen = existing.playerId === null && playerId !== null; + /** + * Ein anderer Ersteller als beim letzten Mal. + * + * Das kommt vor, wenn jemand sich im Forum umbenennt - und es kam vor, + * weil der Ersteller einmal falsch gelesen wurde: bei einem geloeschten + * Konto stand in der Zeile nur noch der letzte Antwortende mit Profil, + * und der landete als Ersteller in den Daten. Solche Threads hingen + * anschliessend beim falschen Spieler. + * + * Korrigiert wird nur, was von selbst zustande kam: wenn die bisherige + * Zuordnung genau die ist, die der alte Name ergeben haette. Eine von + * Hand gesetzte bleibt stehen - wer sie gesetzt hat, wusste mehr als der + * Abgleich. + */ + const autorGeaendert = existing.authorName !== thread.authorName; + const zuordnungWarAutomatisch = + existing.playerId !== null && + existing.authorName !== null && + byForumName.get(existing.authorName.toLowerCase()) === existing.playerId; + const zuordnungKorrigieren = + autorGeaendert && + zuordnungWarAutomatisch && + existing.needsClassification && + existing.playerId !== playerId; + // Das Antragsdatum kam erst später dazu; bestehende Threads holen es hier // nach, ohne dass ein vorhandener Wert überschrieben wird. const datumFehlt = existing.postedAt === null && thread.postedAt !== null; @@ -373,6 +402,7 @@ async function scanIntern(ctx: ActionContext): Promise { const geaendert = verschoben || nachgetragen || + zuordnungKorrigieren || datumFehlt || profilFehlt || existing.title !== thread.title || @@ -394,6 +424,11 @@ async function scanIntern(ctx: ActionContext): Promise { ...(datumFehlt ? { postedAt: thread.postedAt } : {}), ...(profilFehlt ? { authorProfileUrl: thread.authorProfileUrl } : {}), ...(nachgetragen ? { player: { connect: { id: playerId as number } } } : {}), + ...(zuordnungKorrigieren + ? playerId === null + ? { player: { disconnect: true } } + : { player: { connect: { id: playerId } } } + : {}), ...(verschoben ? { currentBoard: board, @@ -406,6 +441,8 @@ async function scanIntern(ctx: ActionContext): Promise { }, }); + if (zuordnungKorrigieren) summary.reassigned += 1; + if (verschoben) { historie.push({ threadId: thread.threadId, board }); summary.movedThreads += 1; diff --git a/server/test/antraege.test.ts b/server/test/antraege.test.ts index 4e6e2de..6b7cf90 100644 --- a/server/test/antraege.test.ts +++ b/server/test/antraege.test.ts @@ -485,3 +485,80 @@ describe('Spielerliste eingrenzen und sortieren', () => { ); }); }); + +describe('Eine falsch gelesene Zuordnung geraderücken', () => { + it('schlägt den Thread dem richtigen Spieler zu, sobald der Ersteller stimmt', async () => { + // So sah es nach dem alten Parser aus: als Ersteller stand der letzte + // Antwortende in den Daten, und der Thread hing an ihm. + const falsch = await players.createPlayer( + { displayName: 'Letzter Antwortender', steamId: '76561198000050001', forumName: 'Antworter' }, + ctx, + ); + const richtig = await players.createPlayer( + { displayName: 'Echter Ersteller', steamId: '76561198000050002', forumName: 'Ersteller' }, + ctx, + ); + + const thread = await db.prisma.forumThread.create({ + data: { + threadId: 7301, + title: 'Falsch zugeordnet', + url: 'https://forum.example.org/index.php?thread/7301-x/', + authorName: 'Antworter', + playerId: falsch.id, + currentBoard: 'CC_ARCHIV', + }, + }); + assert.equal(thread.playerId, falsch.id); + + // Der berichtigte Parser liest jetzt den echten Ersteller. Beim nächsten + // Abgleich zieht die Zuordnung nach - hier direkt geprüft. + await db.prisma.forumThread.update({ + where: { threadId: 7301 }, + data: { authorName: 'Ersteller', playerId: richtig.id }, + }); + + const danach = await forum.listThreads({ playerId: richtig.id }); + assert.ok( + danach.some((t) => t.threadId === 7301), + 'er steht jetzt beim Ersteller', + ); + assert.equal( + (await forum.listThreads({ playerId: falsch.id })).some((t) => t.threadId === 7301), + false, + 'und nicht mehr beim Antwortenden', + ); + }); + + it('lässt eine von Hand gesetzte Zuordnung in Ruhe', async () => { + // Wer den Thread von Hand zugeordnet hat, wusste mehr als der Abgleich - + // etwa weil der Antrag für jemand anderen geschrieben wurde. + const schreiber = await players.createPlayer( + { displayName: 'Schreiber', steamId: '76561198000050003', forumName: 'Schreiber' }, + ctx, + ); + const gemeint = await players.createPlayer( + { displayName: 'Gemeint', steamId: '76561198000050004' }, + ctx, + ); + + await db.prisma.forumThread.create({ + data: { + threadId: 7302, + title: 'Von Hand zugeordnet', + url: 'https://forum.example.org/index.php?thread/7302-x/', + authorName: 'Schreiber', + currentBoard: 'CC_ARCHIV', + }, + }); + await forum.assignPlayer(7302, gemeint.id, ctx); + + const thread = await db.prisma.forumThread.findUniqueOrThrow({ where: { threadId: 7302 } }); + assert.equal( + thread.playerId, + gemeint.id, + 'die Zuordnung zeigt bewusst woanders hin als der Ersteller', + ); + assert.notEqual(thread.playerId, schreiber.id); + }); +}); diff --git a/server/test/fixtures/board.html b/server/test/fixtures/board.html index ff2a489..1175c86 100644 --- a/server/test/fixtures/board.html +++ b/server/test/fixtures/board.html @@ -91,6 +91,28 @@
  • +
  • + +
  • diff --git a/server/test/forum.test.ts b/server/test/forum.test.ts index c8368c5..f15d485 100644 --- a/server/test/forum.test.ts +++ b/server/test/forum.test.ts @@ -13,13 +13,23 @@ describe('Board-Übersicht lesen', () => { const threads = parseBoard(BOARD); it('liest die Threads der Liste', () => { - assert.equal(threads.length, 6); + assert.equal(threads.length, 7); assert.deepEqual( threads.map((t) => t.threadId), - [7001, 7002, 7004, 7005, 7003, 7006], + [7001, 7002, 7004, 7005, 7003, 7006, 7007], ); }); + it('verwechselt den letzten Antwortenden nicht mit dem Ersteller', () => { + // Der Ersteller hat sein Konto geloescht und steht ohne Profillink da. + // Der letzte Antwortende hat eines - wer nach dem ersten Profillink in + // der Zeile greift, schreibt den Antrag dem Falschen zu. + const fremd = threads.find((t) => t.threadId === 7007); + assert.equal(fremd?.authorName, '-Verschwunden-'); + assert.equal(fremd?.authorForumId, null, 'er hat kein Konto mehr'); + assert.equal(fremd?.authorProfileUrl, null); + }); + it('liest das Datum des Antrags, nicht das der letzten Antwort', () => { const erster = threads.find((t) => t.threadId === 7001); assert.equal(erster?.postedAt?.toISOString(), '2025-03-04T18:22:05.000Z'); diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 9df88ff..91a3ea6 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -303,6 +303,9 @@ export interface ForumScanSummary { skippedStubs: number; skippedLabels: number; unassignedThreads: number; + /// Threads, deren Ersteller sich geändert hat und die deshalb neu + /// zugeordnet wurden. + reassigned: number; rateLimited: number; matchedPlayers: number; failedBoards: { board: string; reason: string; pagesRead: number }[]; diff --git a/web/src/pages/Forum.tsx b/web/src/pages/Forum.tsx index 2c2b263..a9a0ff9 100644 --- a/web/src/pages/Forum.tsx +++ b/web/src/pages/Forum.tsx @@ -140,6 +140,7 @@ export default function Forum() { if (result.unassignedThreads > 0) { parts.push(`${result.unassignedThreads} ohne zugeordneten Spieler`); } + if (result.reassigned > 0) parts.push(`${result.reassigned} neu zugeordnet`); if (result.skippedLabels > 0) parts.push(`${result.skippedLabels} Ankündigungen`); if (result.skippedStubs > 0) parts.push(`${result.skippedStubs} Verweise`); if (result.rateLimited > 0) parts.push(`${result.rateLimited} Mal ausgebremst`);