Forums-Anträge erfassen und von Hand entscheiden
Der GExtension-Abgleich liefert die Käufe, der Verbrauch steht im Forum. Die App liest jetzt die drei CC-Boards und sammelt die Anträge. Gelesen wird nur die Board-Übersicht: sie führt bereits den Ersteller, nicht nur den letzten Antwortenden. Damit bleibt ein Lauf bei wenigen Anfragen statt einer je Thread. Unterforen und globale Kästen tragen ebenfalls Thread-Links, deshalb liest der Parser strikt entlang der Listeneinträge - sonst landen Changelogs und Bewerbungen in der Antragsliste. Klassifiziert wird ausschließlich von Hand. Weder Titel noch Board sind verlässlich genug: Threads werden nicht immer verschoben, ein genehmigter Antrag kann weiter im Antrags-Board liegen. Das Board steht deshalb nur als Hinweis an der Zeile. Angenommene Anträge rufen dieselben Aktionen wie die Knöpfe auf der Spielerseite, es gibt keinen zweiten Buchungsweg mit eigenen Regeln. Wandert ein bereits entschiedener Thread später in ein anderes Board, erscheint das als Hinweis zum Abhaken - der CC-Status ändert sich nie von allein. Die Thread-ID bleibt beim Verschieben stabil und traegt deshalb die Wiedererkennung; die Board-Historie wird mitgeschrieben. Zurückgelassene Verweise erkennt der Parser und zählt sie nicht als eigenen Antrag.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Liest Thread-Listen eines WoltLab-Forums (Burning Board).
|
||||
*
|
||||
* Gelesen wird ausschließlich die Übersichtsseite: sie enthält neben Titel und
|
||||
* Thread-ID auch den Ersteller, sodass die Threads nicht einzeln geöffnet
|
||||
* werden müssen. Das hält einen Lauf bei drei Anfragen statt einer je Thread.
|
||||
*
|
||||
* Wichtig: Eine Boardseite enthält neben der Thread-Liste auch Unterforen und
|
||||
* globale Kästen ("letzte Beiträge"). Beides trägt ebenfalls Thread-Links, ist
|
||||
* aber kein Thread dieses Boards - deshalb wird strikt entlang der
|
||||
* tabularListRow-Einträge gelesen und nicht über alle Links im Dokument.
|
||||
*/
|
||||
|
||||
export interface ForumThread {
|
||||
threadId: number;
|
||||
title: string;
|
||||
url: string;
|
||||
authorName: string | null;
|
||||
/// Stabiler als der Name: Forumsnamen ändern sich, die ID nicht.
|
||||
authorForumId: number | null;
|
||||
/// WoltLab lässt beim Verschieben optional einen Verweis am alten Ort zurück.
|
||||
isMovedStub: boolean;
|
||||
}
|
||||
|
||||
const ROW_SPLIT = /<li class="tabularListRow">/;
|
||||
|
||||
const TOPIC_LINK =
|
||||
/<a\s+href="([^"]*thread\/(\d+)-[^"]*)"[^>]*class="[^"]*wbbTopicLink[^"]*"[^>]*>\s*([^<]{1,200})/;
|
||||
const USER_LINK =
|
||||
/<a\s+href="[^"]*user\/(\d+)-[^"]*"[^>]*class="userLink"[^>]*>(?:<i[^>]*><\/i>)?\s*(?:<span[^>]*>)?\s*([^<]{1,80})/;
|
||||
|
||||
// Ein zurückgelassener Verweis ist kein eigener Thread.
|
||||
const MOVED_MARKER = /wbbTopicLinkMoved|class="[^"]*movedTopic|>\s*Verschoben\s*</i;
|
||||
|
||||
function decode(value: string): string {
|
||||
return value
|
||||
.replace(/"/g, '"')
|
||||
.replace(/�?39;/g, "'")
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function parseBoard(html: string): ForumThread[] {
|
||||
const threads: ForumThread[] = [];
|
||||
const seen = new Set<number>();
|
||||
|
||||
for (const block of html.split(ROW_SPLIT).slice(1)) {
|
||||
// Nur bis zum Ende der Zeile schauen, sonst greift der nächste Eintrag rein.
|
||||
const topic = TOPIC_LINK.exec(block);
|
||||
if (!topic) continue;
|
||||
|
||||
const threadId = Number(topic[2]);
|
||||
if (!Number.isInteger(threadId) || seen.has(threadId)) continue;
|
||||
seen.add(threadId);
|
||||
|
||||
const user = USER_LINK.exec(block);
|
||||
|
||||
threads.push({
|
||||
threadId,
|
||||
title: decode(topic[3] ?? ''),
|
||||
url: decode(topic[1] ?? ''),
|
||||
authorName: user ? decode(user[2] ?? '') || null : null,
|
||||
authorForumId: user ? Number(user[1]) : null,
|
||||
isMovedStub: MOVED_MARKER.test(block),
|
||||
});
|
||||
}
|
||||
|
||||
return threads;
|
||||
}
|
||||
|
||||
/** Höchste Seitenzahl der Blätterleiste, mindestens 1. */
|
||||
export function parsePageCount(html: string): number {
|
||||
const numbers = [...html.matchAll(/pageNo=(\d+)/g)].map((m) => Number(m[1]));
|
||||
return numbers.length > 0 ? Math.max(1, ...numbers) : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user