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]>
349 lines
16 KiB
JavaScript
349 lines
16 KiB
JavaScript
/* =====================================================================
|
|
DREI ACCOUNTS, EINE AUFGABENLISTE (17.09.2026)
|
|
|
|
Filipe: "ich hab ja auch 2 neben account, hasidog und dogfather
|
|
clips." Bis heute wusste der Arbeitsplatz davon nichts.
|
|
|
|
WARUM DAS EIN ECHTER MANGEL WAR: "Schneide drei Ausschnitte" ist bei
|
|
drei Accounts keine Aufgabe, sondern eine Frage. Wer sie bekommt,
|
|
muss nachfragen -- oder raet. Raet er falsch, ist die Arbeit nicht
|
|
halb getan, sondern am falschen Ort, und das faellt erst auf, wenn
|
|
jemand hinsieht.
|
|
|
|
---------------------------------------------------------------------
|
|
VIER FRAGEN, UND DIE DRITTE WIRD GERN VERGESSEN
|
|
|
|
1. Kann jemand aus dem Haus einen Kanal setzen und wiederfinden?
|
|
2. Faellt ein erfundener Kanal auf?
|
|
3. Erfaehrt jemand, der ihn NICHT benutzen darf, dass es ihn gibt?
|
|
4. Heisst "leer" wirklich "gilt fuer alles" -- und laesst sich ein
|
|
einmal gesetzter Kanal wieder entfernen?
|
|
|
|
Die dritte ist dieselbe Regel wie bei den Kategorien: Eine Absage
|
|
("Unbekannter Kanal") waere die Auskunft, dass es das Feld gibt --
|
|
also muss der Wert bei ihm stillschweigend fallen. Wer ihn benutzen
|
|
darf, bekommt bei einem Tippfehler dagegen sehr wohl eine Absage.
|
|
|
|
Die vierte klingt nach einer Kleinigkeit und ist der Grund, warum
|
|
viele Felder sich nicht mehr leeren lassen: Wer `""` als
|
|
"unveraendert" behandelt, macht aus einem Loeschversuch ein
|
|
Nichts-Tun -- und niemand merkt es, weil auch keine Fehlermeldung
|
|
kommt.
|
|
|
|
Aufrufen mit: node server/pruef-kanaele.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-kanaele");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kanal-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = String(PORT);
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
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 }); };
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-kanaele");
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const schluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", schluessel, jetzt);
|
|
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, 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, code_kennung, aktiv, erstellt)"
|
|
+ " VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
const idHand = anlegen("VanVan", "hand", "CODE-HAND-0001");
|
|
const idModi = anlegen("Marina", "modi", "CODE-MODI-0001");
|
|
const idCrea = anlegen("Tili", "creator", "CODE-CREA-0001");
|
|
void idDogi; void idHand;
|
|
|
|
/* DIE SPALTE MUSS ES GEBEN -- und zwar durch die Umstellung, nicht
|
|
durch den Bauplan allein. Steht sie nicht da, sind alle Pruefungen
|
|
darunter Aussagen ueber ein Feld, das es nicht gibt. */
|
|
const spalten = d.prepare("PRAGMA table_info(aufgaben)").all().map((s) => s.name);
|
|
d.close();
|
|
|
|
async function anmelden(kachel, code) {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: kachel, code }),
|
|
});
|
|
return (a.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
async function ruf(keks, pfad, aufruf = {}) {
|
|
const a = await fetch(BASIS + pfad, {
|
|
...aufruf,
|
|
headers: { ...(aufruf.headers || {}), cookie: keks },
|
|
});
|
|
return { status: a.status, daten: await a.json().catch(() => null) };
|
|
}
|
|
const jsonKopf = { "Content-Type": "application/json" };
|
|
|
|
const dogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const hand = await anmelden("creator", "CODE-HAND-0001");
|
|
const modi = await anmelden("creator", "CODE-MODI-0001");
|
|
const crea = await anmelden("creator", "CODE-CREA-0001");
|
|
|
|
/* =======================================================================
|
|
0. DIE SPALTE
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 0. Der Platz dafuer ===");
|
|
ok(spalten.includes("kanal"), `die Aufgabe hat eine Spalte 'kanal' (${spalten.length} Spalten)`);
|
|
|
|
/* =======================================================================
|
|
1. WER DIE KANAELE BEKOMMT
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 1. Wer sie ueberhaupt sieht ===");
|
|
{
|
|
const a = await ruf(dogi, "/workspace/api/aufgaben");
|
|
const namen = (a.daten?.kanaele || []).map((k) => k.name);
|
|
ok(namen.length === 3, `DogFather bekommt drei Kanaele (${namen.join(", ")})`);
|
|
ok(namen.includes("HasiDog") && namen.includes("DogFather Clips"),
|
|
"darunter die beiden Nebenaccounts");
|
|
/* JEDER TRAEGT EINE ERKLAERUNG. Ein Auswahlfeld mit drei Namen und
|
|
ohne ein Wort dazu ist eine Ratefrage fuer jemanden, der neu ist. */
|
|
ok((a.daten?.kanaele || []).every((k) => (k.text || "").length > 30),
|
|
"und jeder erklaert sich in einem Satz");
|
|
|
|
const m = await ruf(modi, "/workspace/api/aufgaben");
|
|
ok((m.daten?.kanaele || []).length === 3, "ein Modi auch");
|
|
const h = await ruf(hand, "/workspace/api/aufgaben");
|
|
ok((h.daten?.kanaele || []).length === 3, "die rechte Hand auch");
|
|
|
|
/* DIE VERBORGENHEIT: Wer sie nicht benutzt, bekommt die Liste gar
|
|
nicht erst -- statt sie zu bekommen und ausgeblendet zu sehen. */
|
|
const c = await ruf(crea, "/workspace/api/aufgaben");
|
|
ok((c.daten?.kanaele || []).length === 0,
|
|
`ein Creator bekommt keine (${(c.daten?.kanaele || []).length})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. SETZEN UND WIEDERFINDEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 2. Setzen ===");
|
|
let idClip = 0;
|
|
{
|
|
const a = await ruf(dogi, "/workspace/api/aufgaben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ titel: "Drei Ausschnitte schneiden",
|
|
verantwortlich_id: idModi, kanal: "clips", kategorie: "clipping" }),
|
|
});
|
|
ok(a.status === 201, `eine Aufgabe fuer Clips anlegen (${a.status})`);
|
|
idClip = a.daten?.id || 0;
|
|
|
|
const liste = await ruf(dogi, "/workspace/api/aufgaben");
|
|
const gefunden = (liste.daten?.aufgaben || []).find((x) => x.id === idClip);
|
|
ok(gefunden?.kanal === "clips", `und sie kommt mit ihrem Kanal zurueck (${gefunden?.kanal})`);
|
|
|
|
/* DER MODI SIEHT IHN AUCH -- es ist seine Aufgabe. */
|
|
const beimModi = await ruf(modi, "/workspace/api/aufgaben");
|
|
const seine = (beimModi.daten?.aufgaben || []).find((x) => x.id === idClip);
|
|
ok(seine?.kanal === "clips", `der Modi sieht ihn ebenfalls (${seine?.kanal})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. EIN ERFUNDENER KANAL
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 3. Was nicht durchgeht ===");
|
|
{
|
|
const a = await ruf(dogi, "/workspace/api/aufgaben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ titel: "Irgendwas", kanal: "tiktok-zweitkanal" }),
|
|
});
|
|
ok(a.status === 400 && /Kanal/i.test(a.daten?.fehler || ""),
|
|
`ein erfundener Kanal wird abgelehnt (${a.status} ${a.daten?.fehler || ""})`);
|
|
|
|
/* UND DIE GEGENPROBE ZUR ABLEHNUNG: Ohne sie hiesse "400" nur, dass
|
|
das Anlegen ueberhaupt scheitert -- etwa am fehlenden Titel. */
|
|
const b = await ruf(dogi, "/workspace/api/aufgaben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ titel: "Irgendwas", kanal: "hasidog" }),
|
|
});
|
|
ok(b.status === 201, `derselbe Aufruf mit echtem Kanal geht durch (${b.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. DIE VERBORGENHEIT
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 4. Ein Creator schickt einen Kanal ===");
|
|
{
|
|
const a = await ruf(crea, "/workspace/api/aufgaben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ titel: "Meine eigene Sache", kanal: "clips" }),
|
|
});
|
|
/* KEINE ABSAGE. Eine Fehlermeldung waere die Auskunft, dass es das
|
|
Feld gibt -- und der Creator soll von den Kanaelen nichts wissen. */
|
|
ok(a.status === 201, `es wird angelegt, nicht abgelehnt (${a.status})`);
|
|
|
|
const liste = await ruf(crea, "/workspace/api/aufgaben");
|
|
const seine = (liste.daten?.aufgaben || []).find((x) => x.id === a.daten?.id);
|
|
ok(seine && !seine.kanal,
|
|
`aber der Kanal faellt stillschweigend (${seine?.kanal ?? "leer"})`);
|
|
/* Und ein erfundener Kanal von ihm ebenso -- gleiche Antwort, gleiche
|
|
Auskunft, kein Unterschied, an dem sich etwas ablesen liesse. */
|
|
const b = await ruf(crea, "/workspace/api/aufgaben", {
|
|
method: "POST", headers: jsonKopf,
|
|
body: JSON.stringify({ titel: "Noch eine", kanal: "gibt-es-nicht" }),
|
|
});
|
|
ok(b.status === a.status,
|
|
`ein erfundener klingt bei ihm genauso (${b.status} wie ${a.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. LEER HEISST "FUER ALLES" -- UND LAESST SICH SETZEN
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 5. Wieder entfernen ===");
|
|
{
|
|
const a = await ruf(dogi, `/workspace/api/aufgaben/${idClip}`, {
|
|
method: "PATCH", headers: jsonKopf, body: JSON.stringify({ kanal: "" }),
|
|
});
|
|
ok(a.status === 200, `leeren geht (${a.status})`);
|
|
const liste = await ruf(dogi, "/workspace/api/aufgaben");
|
|
const jetztOhne = (liste.daten?.aufgaben || []).find((x) => x.id === idClip);
|
|
ok(jetztOhne && !jetztOhne.kanal,
|
|
`und der Kanal ist wirklich weg (${jetztOhne?.kanal ?? "leer"})`);
|
|
|
|
/* HIN UND ZURUECK: Ein Feld, das sich nur einmal setzen laesst, faellt
|
|
erst beim zweiten Mal auf. */
|
|
const b = await ruf(dogi, `/workspace/api/aufgaben/${idClip}`, {
|
|
method: "PATCH", headers: jsonKopf, body: JSON.stringify({ kanal: "hasidog" }),
|
|
});
|
|
ok(b.status === 200, `und wieder setzen auch (${b.status})`);
|
|
const liste2 = await ruf(dogi, "/workspace/api/aufgaben");
|
|
const wieder = (liste2.daten?.aufgaben || []).find((x) => x.id === idClip);
|
|
ok(wieder?.kanal === "hasidog", `jetzt steht HasiDog darin (${wieder?.kanal})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. STEHT ES AUCH WIRKLICH IN DER DATENBANK?
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 6. Nicht nur nachgeplappert ===");
|
|
{
|
|
/* OHNE DIESEN ABSCHNITT koennte alles oben gruen sein, obwohl der Wert
|
|
nur durch die Auskunft zurueckgereicht wird -- zum Beispiel aus
|
|
einem Zwischenspeicher. Gefragt wird deshalb die Datei selbst. */
|
|
const e = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const zeile = e.prepare("SELECT kanal FROM aufgaben WHERE id = ?").get(idClip);
|
|
const verteilung = e.prepare(
|
|
"SELECT kanal, COUNT(*) AS n FROM aufgaben GROUP BY kanal ORDER BY n DESC").all();
|
|
e.close();
|
|
ok(zeile?.kanal === "hasidog", `in der Datenbank steht "${zeile?.kanal}"`);
|
|
ok(verteilung.length >= 2,
|
|
`und es gibt mehr als einen Zustand (${verteilung
|
|
.map((v) => `${v.kanal ?? "leer"}:${v.n}`).join(" ")})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. STIMMEN DIE ADRESSEN MIT DER OEFFENTLICHEN SEITE UEBEREIN?
|
|
======================================================================= */
|
|
melde("");
|
|
melde("=== 7. Dieselben Accounts wie draussen ===");
|
|
{
|
|
/* WARUM DIESER ABSCHNITT AM 21.09.2026 DAZUKAM.
|
|
|
|
In KANAELE stand "@hasidog00804" -- eine Null zu viel. Nachgemessen
|
|
bei TikTok selbst: "@hasidog0804" hat 65 Follower, "@hasidog00804"
|
|
liefert "Couldn't find this account". Der Tippfehler war nicht
|
|
kosmetisch: `kanalVonHandle` ordnet ein hochgeladenes Video ueber
|
|
genau diesen Text einem Kanal zu. Ein echtes HasiDog-Video waere
|
|
abgelehnt worden -- mit dem Satz "ist keiner deiner Kanaele.
|
|
Moeglich sind: ..." und einer Adresse darin, die es nicht gibt.
|
|
Eine Sackgasse, die auch noch falsch erklaert, wo sie hinfuehrt.
|
|
|
|
GEFUNDEN HAT ES KEINE PRUEFUNG, sondern ein Vergleich: Die
|
|
oeffentliche Website verlinkt dieselben Accounts, und dort stand
|
|
die richtige Schreibweise. Zwei Orte im selben Haus, die dasselbe
|
|
behaupten und sich widersprechen -- das ist messbar, ohne irgendwen
|
|
im Netz zu fragen.
|
|
|
|
WARUM NICHT BEI TIKTOK NACHFRAGEN: Das waere die direktere Frage,
|
|
haengt aber an einem fremden Dienst. Faellt er aus, wird aus einer
|
|
Stoerung ein roter Alarm (der Fall vom 04.09. in der Buchhaltung).
|
|
Der Vergleich im Haus braucht kein Netz und kann deshalb immer
|
|
antworten. */
|
|
const { readFileSync, readdirSync } = await import("node:fs");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const { KANAELE } = await import("./workspace.js");
|
|
const wurzel = fileURLToPath(new URL("../", import.meta.url));
|
|
|
|
const draussen = new Set();
|
|
let gelesen = 0;
|
|
for (const f of readdirSync(wurzel)) {
|
|
if (!f.endsWith(".html")) continue;
|
|
gelesen++;
|
|
const text = readFileSync(wurzel + f, "utf8");
|
|
for (const m of text.matchAll(/tiktok\.com\/@([A-Za-z0-9._-]+)/g)) {
|
|
draussen.add("@" + m[1].toLowerCase());
|
|
}
|
|
}
|
|
|
|
/* DIE ZAHL GEHOERT IN DIE BEDINGUNG. Faende die Suche gar nichts --
|
|
weil die Seiten umgezogen sind oder anders verlinken --, waere
|
|
"keiner fehlt" wahr und vollkommen wertlos. */
|
|
ok(gelesen > 10 && draussen.size >= 3,
|
|
`auf ${gelesen} oeffentlichen Seiten stehen ${draussen.size} TikTok-Adressen`);
|
|
|
|
const fehlend = KANAELE.filter((k) => !draussen.has(k.handle.toLowerCase()));
|
|
ok(fehlend.length === 0,
|
|
fehlend.length
|
|
? `DIESE ADRESSEN KENNT DIE OEFFENTLICHE SEITE NICHT: `
|
|
+ fehlend.map((k) => `${k.name} ${k.handle}`).join(", ")
|
|
+ ` -- eine davon ist falsch geschrieben, und Videos von diesem `
|
|
+ `Account wuerden abgelehnt`
|
|
: `alle ${KANAELE.length} Accounts stehen genauso auch draussen `
|
|
+ `(${KANAELE.map((k) => k.handle).join(", ")})`);
|
|
|
|
/* GEGENPROBE: Kann dieser Vergleich ueberhaupt "nicht in Ordnung"
|
|
sagen? Eine erfundene Adresse muss durchfallen -- sonst wuerde
|
|
oben auch der echte Tippfehler durchrutschen. */
|
|
ok(!draussen.has("@hasidog00804"),
|
|
"Gegenprobe: die falsch geschriebene Adresse findet sich draussen NICHT");
|
|
ok(!draussen.has("@diesen-account-gibt-es-nicht"),
|
|
"Gegenprobe: eine erfundene Adresse wuerde auffallen");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------- */
|
|
melde("");
|
|
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|