Der Forums-Abgleich las von 25 Archivseiten nur zwei. WoltLab verlinkt in der Blätterleiste nur die Nachbarseiten und führt die Gesamtzahl in einem eigenen Element; ausgewertet wurden aber die Links. Alles jenseits von Seite zwei war damit unsichtbar - etwa ein Antrag auf Seite vier, der deshalb nie ankam. Die Rollen verlaufen jetzt zwischen erfassen und gewähren statt zwischen Alltag und Verwaltung: Editoren erfassen Käufe und führen Datei-Import, GExtension- und Forums-Abgleich samt Entscheidung über Anträge durch. Kulanz zu vergeben bleibt Administratoren, ebenso das Auflösen unklarer Pakete, weil dabei Kulanz entstehen kann. Dazu lassen sich frühere Steam- und Forumskonten am Spieler hinterlegen. Abgleich und Import prüfen sie mit, sodass ein Kontowechsel die Zuordnung nicht abreißen lässt. Steam-Kennungen werden dabei auf dieselbe Form gebracht wie die Hauptangabe, und eine Kennung kann nicht zwei Spielern gehören.
122 lines
4.2 KiB
TypeScript
122 lines
4.2 KiB
TypeScript
/**
|
|
* 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;
|
|
/// Labels des Threads, etwa "Wichtig" oder "SCP-RP".
|
|
labels: string[];
|
|
}
|
|
|
|
/// Angepinnte Ankündigungen liegen mit in den CC-Boards - Changelogs,
|
|
/// Regelwerke, Bewerbungsphasen. Sie tragen diese Labels und sind keine Anträge.
|
|
export const IGNORED_LABELS = ['wichtig', 'information'];
|
|
|
|
export function hasIgnoredLabel(thread: ForumThread): boolean {
|
|
return thread.labels.some((label) => IGNORED_LABELS.includes(label.toLowerCase()));
|
|
}
|
|
|
|
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;
|
|
|
|
// Labels hängen als eigene Links über dem Titel, nicht als span.
|
|
const LABEL = /<a[^>]*class="[^"]*\bbadge label\b[^"]*"[^>]*>([^<]{1,40})<\/a>/g;
|
|
|
|
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);
|
|
|
|
LABEL.lastIndex = 0;
|
|
const labels = [...block.matchAll(LABEL)].map((m) => decode(m[1] ?? '')).filter(Boolean);
|
|
|
|
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),
|
|
labels,
|
|
});
|
|
}
|
|
|
|
return threads;
|
|
}
|
|
|
|
/**
|
|
* Anzahl der Seiten eines Boards.
|
|
*
|
|
* WoltLab führt die Gesamtzahl in einem eigenen Element, nicht in den Links:
|
|
* die Blätterleiste zeigt nur die Nachbarseiten. Wer allein die pageNo-Links
|
|
* auswertet, hält ein Board mit 25 Seiten für zweiseitig.
|
|
*/
|
|
export function parsePageCount(html: string): number {
|
|
const element = /<woltlab-core-pagination[^>]*\scount="(\d+)"/.exec(html);
|
|
if (element) return Math.max(1, Number(element[1]));
|
|
|
|
// Ältere Fassungen ohne das Element: wenigstens die Links auswerten.
|
|
const numbers = [...html.matchAll(/pageNo=(\d+)/g)].map((m) => Number(m[1]));
|
|
return numbers.length > 0 ? Math.max(1, ...numbers) : 1;
|
|
}
|
|
|
|
/**
|
|
* Zieht die Thread-ID aus einer Forums-URL. Angenommen werden der volle Link,
|
|
* der Pfad oder die blanke Nummer - beim Eintippen von Hand landet mal das
|
|
* eine, mal das andere im Feld.
|
|
*/
|
|
export function parseThreadId(input: string): number | null {
|
|
const value = input.trim();
|
|
if (value.length === 0) return null;
|
|
|
|
const fromUrl = /thread\/(\d+)/.exec(value);
|
|
if (fromUrl) return Number(fromUrl[1]);
|
|
|
|
return /^\d+$/.test(value) ? Number(value) : null;
|
|
}
|