In KANAELE stand "@hasidog00804". Der Account heisst "@hasidog0804". Nachgemessen bei TikTok selbst: der richtige hat 65 Follower, der andere liefert "Couldn't find this account". Das war nicht kosmetisch. `kanalVonHandle` ordnet ein hochgeladenes Video ueber genau diesen Text einem Kanal zu. Ein echtes HasiDog-Video waere mit 403 abgelehnt worden -- und die Begruendung haette gelautet: "ist keiner deiner Kanaele. Moeglich sind: @dogfather0804, @hasidog00804, @dogis.modi.gang." Eine Sackgasse, die zusaetzlich eine Adresse nennt, die es nicht gibt. WARUM ES KEINE PRUEFUNG GEFUNDEN HAT: pruef-kanalzeile und pruef-teilen hatten denselben Handle abgeschrieben. Zwei Abschriften desselben Tippfehlers bestaetigen sich gegenseitig -- beide waren gruen. Sie leiten ihn jetzt aus KANAELE ab und koennen ihn damit gar nicht mehr eigenstaendig falsch haben. Gefunden hat es ein Vergleich: Die oeffentliche Website verlinkt dieselben Accounts und hatte die richtige Schreibweise. Genau das ist jetzt Abschnitt 7 von pruef-kanaele -- alle Handles muessen auch draussen vorkommen, mit Gegenprobe, dass eine falsche Adresse durchfaellt. Ohne Netz, damit eine Stoerung bei TikTok nicht zu einem roten Alarm im Haus wird. Co-Authored-By: Claude Opus 5 <[email protected]>
275 lines
12 KiB
JavaScript
275 lines
12 KiB
JavaScript
/* =====================================================================
|
|
DIE KANALZEILE (17.09.2026) -- Schritt 3 aus dem Video-Plan
|
|
|
|
Filipe: "fuer meinen Account, dogfather, dann dogfather clips und
|
|
hasidog."
|
|
|
|
Geprueft wird nicht nur, DASS die Zeile da ist:
|
|
|
|
* Bei weniger als zwei Kanaelen darf sie NICHT erscheinen. Ein
|
|
Filter, der nichts zu filtern hat, macht misstrauisch -- und
|
|
wer einmal einen Knopf gedrueckt hat, bei dem nichts passiert,
|
|
drueckt den naechsten nicht mehr.
|
|
* Ein Klick muss wirklich weniger Karten uebrig lassen, und zwar
|
|
die richtigen.
|
|
* Ein zweiter Klick auf denselben Knopf hebt den Filter auf.
|
|
* Kanal- und Art-Filter wirken ZUSAMMEN -- das war der Grund, den
|
|
Kanal als zweite Dimension zu bauen statt als weiteren Wert in
|
|
derselben Leiste.
|
|
|
|
Aufrufen mit: node server/pruef-kanalzeile.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createServer } from "node:http";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kz-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "das Bildwerkzeug");
|
|
const TIKTOK = await eigenerPort(import.meta, "nachgebautes TikTok", 1);
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
process.env.TIKTOK_OEMBED_BASIS = `http://127.0.0.1:${TIKTOK}/oembed`;
|
|
|
|
/* DIE HANDLES WERDEN ABGELEITET, NICHT ABGESCHRIEBEN (21.09.2026).
|
|
|
|
Hier standen sie als Text im Pruefcode -- und einer davon war falsch:
|
|
"@hasidog00804" mit einer Null zu viel. Er stand genauso falsch in
|
|
KANAELE, weshalb die Pruefung gruen war: Zwei Abschriften desselben
|
|
Tippfehlers bestaetigen sich gegenseitig. Nachgemessen bei TikTok
|
|
selbst gibt es nur "@hasidog0804" (65 Follower); die andere Adresse
|
|
liefert "Couldn't find this account".
|
|
|
|
Jetzt kommen sie aus KANAELE. Damit kann diese Datei den Wert gar
|
|
nicht mehr eigenstaendig falsch haben -- sie misst den echten. */
|
|
const { KANAELE } = await import("./workspace.js");
|
|
const HANDLES = KANAELE.map((k) => k.handle.replace(/^@/, ""));
|
|
|
|
const PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
|
|
"base64");
|
|
const tiktok = createServer((q, a) => {
|
|
const u = new URL(q.url, `http://127.0.0.1:${TIKTOK}`);
|
|
if (u.pathname === "/oembed") {
|
|
const ziel = u.searchParams.get("url") || "";
|
|
const wer = (ziel.match(/@([^/?]+)/) || [])[1] || "";
|
|
if (!wer) { a.writeHead(404); a.end("{}"); return; }
|
|
const TITEL = ["Casper klaut die Fernbedienung 😄",
|
|
"Casper schläft im Wäschekorb",
|
|
"Der Chat entscheidet: Horror oder Koop?"];
|
|
const titel = TITEL[HANDLES.indexOf(wer)] || "Video";
|
|
a.writeHead(200, { "content-type": "application/json" });
|
|
a.end(JSON.stringify({
|
|
version: "1.0", type: "video", title: titel,
|
|
author_url: `https://www.tiktok.com/@${wer}`,
|
|
thumbnail_url: `http://127.0.0.1:${TIKTOK}/cover.jpg`,
|
|
thumbnail_width: 576, thumbnail_height: 1024,
|
|
}));
|
|
return;
|
|
}
|
|
if (u.pathname === "/cover.jpg") {
|
|
a.writeHead(200, { "content-type": "image/jpeg" }); a.end(PNG); return;
|
|
}
|
|
a.writeHead(404); a.end("{}");
|
|
});
|
|
await new Promise((r) => tiktok.listen(TIKTOK, "127.0.0.1", r));
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
await new Promise((r) => setTimeout(r, 1200));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const t = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { t.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { t.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const j = new Date().toISOString();
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", salt, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name,rolle,code_hash,code_salt,code_n,aktiv,erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run("Filipe", "admin", hash, salt, 32768, j);
|
|
d.close();
|
|
|
|
const an = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
|
|
});
|
|
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
|
|
async function video(handle, nr) {
|
|
const a = await fetch(`${BASIS}/workspace/api/bereich/highlight/video`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", cookie: keks, origin: BASIS },
|
|
body: JSON.stringify({ url: `http://127.0.0.1:${TIKTOK}/@${handle}/video/${nr}` }),
|
|
});
|
|
return a.status;
|
|
}
|
|
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1200, height: 900 }, deviceScaleFactor: 2 });
|
|
await ctx.addCookies(keks.split("; ").map((k) => {
|
|
const [name, ...rest] = k.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await ctx.newPage();
|
|
const konsolenfehler = [];
|
|
seite.on("console", (m) => { if (m.type() === "error") konsolenfehler.push(m.text()); });
|
|
seite.on("pageerror", (f) => konsolenfehler.push(String(f)));
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const zeileDa = async () => !!(await seite.$(".filter__kanaele"));
|
|
/* DER SELEKTOR HEISST .eintrag-karte -- nachgesehen in bereich.js
|
|
(el("article", "eintrag-karte")), nicht geraten. Mein erster
|
|
Versuch traf nichts, und die Messung meldete brav "0 -> 0": Das
|
|
sah aus wie ein Ergebnis und war keins.
|
|
|
|
BEIM BERICHTIGEN bin ich dann in die naechste Falle gelaufen und
|
|
habe daraus "undefined -> undefined" gemacht: Das Patchskript
|
|
benutzte String.replace, und dort bedeutet "$$" im ERSATZTEXT ein
|
|
einzelnes "$". Aus seite.$$(...) wurde seite.$(...). Der Fehler
|
|
steht woertlich in meinen eigenen Regeln -- und hat trotzdem
|
|
zugeschlagen. Aufgefallen ist er nur, weil die Ausgabe gelesen
|
|
wurde: "undefined" ist wenigstens ehrlich, "0" waere es nicht
|
|
gewesen. */
|
|
const karten = async () => (await seite.$$(".eintrag-karte")).length;
|
|
|
|
/* ---- 1. EIN Kanal: die Zeile darf NICHT da sein --------------------- */
|
|
console.log("\n=== 1. Mit nur einem Kanal ===");
|
|
console.log(" Video 1:", await video("dogfather0804", "101"));
|
|
await seite.goto(BASIS + "/workspace/bereich.html?b=highlight", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
ok(!(await zeileDa()), "bei einem einzigen Kanal steht dort KEINE Filterzeile");
|
|
|
|
/* ---- 2. Drei Kanäle -------------------------------------------------- */
|
|
console.log("\n=== 2. Mit drei Kanaelen ===");
|
|
console.log(" Video 2:", await video(HANDLES[1], "102"));
|
|
console.log(" Video 3:", await video(HANDLES[2], "103"));
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1800);
|
|
ok(await zeileDa(), "sobald drei Kanaele da sind, erscheint sie");
|
|
const knoepfe = await seite.$$eval(".filter__kanal", (ns) => ns.map((n) => ({
|
|
text: n.textContent.trim().replace(/\s+/g, " "),
|
|
kanal: n.dataset.kanal || "(alle)",
|
|
gewaehlt: n.getAttribute("aria-pressed"),
|
|
})));
|
|
for (const k of knoepfe) console.log(` ${k.gewaehlt === "true" ? "[x]" : "[ ]"} ${k.text}`);
|
|
ok(knoepfe.length === 4, `vier Knoepfe: Alle Kanaele und die drei (${knoepfe.length})`);
|
|
ok(knoepfe[0]?.gewaehlt === "true", "und „Alle Kanaele“ ist die Vorgabe");
|
|
|
|
const zeile = await seite.$(".filter__kanaele");
|
|
if (zeile) {
|
|
const leiste = await seite.$("#filter");
|
|
await (leiste || zeile).scrollIntoViewIfNeeded();
|
|
await seite.waitForTimeout(400);
|
|
await (leiste || zeile).screenshot({ path: "server/bild-kanalzeile.png" });
|
|
}
|
|
|
|
/* ---- 3. Ein Klick filtert wirklich ----------------------------------- */
|
|
console.log("\n=== 3. Ein Klick ===");
|
|
const vorher = await karten();
|
|
ok(vorher === 3, `ungefiltert liegen drei Videos da (${vorher})`);
|
|
|
|
const hasi = await seite.$('.filter__kanal[data-kanal="hasidog"]');
|
|
ok(!!hasi, "der HasiDog-Knopf ist da");
|
|
if (hasi) {
|
|
await hasi.click();
|
|
await seite.waitForTimeout(1500);
|
|
const nachher = await karten();
|
|
ok(nachher === 1, `nach einem Klick auf HasiDog bleibt eines uebrig (${nachher})`);
|
|
|
|
/* UND ZWAR DAS RICHTIGE. "Weniger" allein waere auch erfuellt, wenn
|
|
der Filter irgendetwas wegwirft -- das ist der Unterschied
|
|
zwischen filtern und kaputt sein. */
|
|
const titel = await seite.$$eval(".eintrag-karte",
|
|
/* /\s+/ und nicht /s+/ -- beim Erzeugen dieser Datei ist der
|
|
Backslash durch eine Escaping-Ebene verloren gegangen, und der
|
|
Ausdruck ersetzte danach JEDES "s" durch ein Leerzeichen. In der
|
|
Ausgabe stand "Ca per chläft im Wä chekorb". Das war kein Fehler
|
|
im Filter, sondern einer in der Pruefung -- und er hat sie rot
|
|
gemacht, was genau richtig ist: Eine Pruefung, die ihren eigenen
|
|
Text zerstoert, darf nicht gruen sein. */
|
|
(ns) => ns.map((n) => n.textContent.replace(/\s+/g, " ").trim()));
|
|
for (const t of titel) console.log(" " + t.slice(0, 60));
|
|
ok(titel.length === 1 && /Wäschekorb/.test(titel[0]),
|
|
"und es ist wirklich das HasiDog-Video");
|
|
|
|
const leiste2 = await seite.$("#filter");
|
|
if (leiste2) await leiste2.screenshot({ path: "server/bild-kanalzeile-gefiltert.png" });
|
|
|
|
/* Der gewaehlte Knopf sagt es auch. */
|
|
const gedrueckt = await seite.$$eval(".filter__kanal",
|
|
(ns) => ns.filter((n) => n.getAttribute("aria-pressed") === "true")
|
|
.map((n) => n.dataset.kanal || "(alle)"));
|
|
ok(gedrueckt.length === 1 && gedrueckt[0] === "hasidog",
|
|
`genau ein Knopf ist gewaehlt (${gedrueckt.join(", ")})`);
|
|
|
|
/* ---- Und ein zweiter Klick hebt ihn auf ---------------------------
|
|
Ohne das sucht man den Weg zurueck zu "Alle Kanaele" und findet
|
|
ihn nicht, weil man nicht mehr weiss, dass man gefiltert hat. */
|
|
const nochmal = await seite.$('.filter__kanal[data-kanal="hasidog"]');
|
|
await nochmal.click();
|
|
await seite.waitForTimeout(1500);
|
|
ok((await karten()) === vorher,
|
|
`ein zweiter Klick hebt den Filter auf (wieder ${await karten()})`);
|
|
}
|
|
|
|
/* ---- 4. Kanal UND Art zusammen --------------------------------------
|
|
Der eigentliche Grund fuer zwei getrennte Reihen. Waere der Kanal
|
|
nur ein weiterer Wert in derselben Leiste, koennte man das hier
|
|
nicht fragen -- und die Pruefung waere trotzdem gruen geblieben. */
|
|
console.log("\n=== 4. Beide Filter zusammen ===");
|
|
{
|
|
const df = await seite.$('.filter__kanal[data-kanal="dogfather"]');
|
|
await df.click();
|
|
await seite.waitForTimeout(1200);
|
|
const nurKanal = await karten();
|
|
ok(nurKanal === 1, `nur DogFather: ${nurKanal}`);
|
|
|
|
/* Jetzt zusaetzlich eine Art, die dieses Video NICHT hat. */
|
|
const arten = await seite.$$eval("#filter > .schritt",
|
|
(ns) => ns.map((n) => n.textContent.trim()));
|
|
console.log(" Arten in der ersten Reihe: " + arten.join(", "));
|
|
const bild = (await seite.$$("#filter > .schritt"))
|
|
.find ? null : null;
|
|
const alleKnoepfe = await seite.$$("#filter > .schritt");
|
|
let getroffen = false;
|
|
for (const k of alleKnoepfe) {
|
|
const t = (await k.textContent()).trim();
|
|
if (t !== "Bild") continue;
|
|
await k.click();
|
|
await seite.waitForTimeout(1200);
|
|
getroffen = true;
|
|
ok((await karten()) === 0,
|
|
`DogFather + Art "Bild" ergibt nichts (${await karten()}) -- beide Filter wirken`);
|
|
break;
|
|
}
|
|
ok(getroffen, "die Art „Bild“ war zum Gegenprobieren da");
|
|
void bild;
|
|
}
|
|
|
|
console.log("");
|
|
ok(konsolenfehler.length === 0,
|
|
konsolenfehler.length ? konsolenfehler.join(" | ") : "keine Fehler in der Browserkonsole");
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
await browser.close();
|
|
await new Promise((r) => tiktok.close(r));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|