revert: MakerWorld Statistik-Integration erstmal zurueckgebaut

This commit is contained in:
2026-07-12 11:41:23 +02:00
parent b0d090d85f
commit 5b4b0b6908
2 changed files with 1 additions and 345 deletions

View File

@@ -1,138 +0,0 @@
const express = require('express');
const crypto = require('crypto');
const db = require('../../db');
const { authenticate } = require('../../middleware/auth');
const router = express.Router();
// ── MakerWorld Creator Center Statistik — Bookmarklet-Ansatz ─────────────────
// Nach mehreren gescheiterten Versuchen (Cloudflare blockt sowohl reinen
// fetch() als auch stealth-gepatchtes Puppeteer-Chromium, vermutlich per
// TLS-Fingerprinting) gibt es hier bewusst KEINE Server-seitige Automatisierung
// mehr. Stattdessen: der Nutzer öffnet die Statistikseite ganz normal in seinem
// eigenen, echten, eingeloggten Browser und klickt auf ein Lesezeichen
// ("Bookmarklet"). Das liest den bereits im Browser geladenen __NEXT_DATA__-
// Block direkt aus der Seite (kein Netzwerk-Request an makerworld.com nötig,
// keine Cloudflare-Prüfung möglich, da es der echte Browser des Nutzers ist)
// und schickt nur die Statistikdaten an diesen Endpunkt.
//
// Authentifizierung: das Bookmarklet läuft im Kontext von makerworld.com
// (fremde Origin) und hat daher keinen Zugriff auf den DickenDock-Login-Token.
// Stattdessen bekommt jeder Nutzer ein eigenes, zufälliges Ingest-Token, das
// fest im Bookmarklet-Code steht — das ersetzt hier die reguläre Anmeldung.
const cacheByUser = new Map(); // userId -> { data, fetchedAt } (Lazy-Load aus DB)
function getCache(userId) {
if (!cacheByUser.has(userId)) {
const row = db.prepare('SELECT data_json, fetched_at FROM makerworld_stats WHERE user_id=?').get(userId);
cacheByUser.set(userId, row
? { data: JSON.parse(row.data_json), fetchedAt: row.fetched_at }
: { data: null, fetchedAt: 0 });
}
return cacheByUser.get(userId);
}
function persistCache(userId, data, fetchedAt) {
db.prepare(`
INSERT OR REPLACE INTO makerworld_stats (user_id, data_json, fetched_at)
VALUES (?, ?, ?)
`).run(userId, JSON.stringify(data), fetchedAt);
}
function getOrCreateToken(userId) {
let row = db.prepare('SELECT token FROM makerworld_ingest_tokens WHERE user_id=?').get(userId);
if (!row) {
const token = crypto.randomBytes(24).toString('hex');
db.prepare('INSERT INTO makerworld_ingest_tokens (user_id, token) VALUES (?, ?)').run(userId, token);
row = { token };
}
return row.token;
}
// Formt den vom Bookmarklet gesendeten statisticalData-Ausschnitt in unser Format
function mapStatisticalData(statisticalData) {
if (!statisticalData?.summary) throw new Error('Erwartete Datenstruktur nicht gefunden (summary fehlt)');
const s = statisticalData.summary;
return {
downloads: s.download ?? 0,
prints: s.print ?? 0,
likes: s.like ?? 0,
collections: s.collect ?? 0,
boosts: s.boost ?? 0,
followers: s.follower ?? 0,
views: s.view ?? 0,
impressions: s.impression ?? 0,
creatorPoints: s.point ?? 0,
modelPoints: s.pointFromModel ?? 0,
instructionPoints: s.pointFromInst ?? 0,
models: (statisticalData.statisticalList || []).map(m => ({
id: m.designId,
title: m.title || m.titleTranslated || '',
cover: m.cover || '',
publishedAt: m.publishTime || '',
downloads: m.download ?? 0,
prints: m.print ?? 0,
likes: m.like ?? 0,
collections: m.collect ?? 0,
boosts: m.boost ?? 0,
views: m.view ?? 0,
impressions: m.impression ?? 0,
points: m.point ?? 0,
})),
};
}
// ── Routes ────────────────────────────────────────────────────────────────────
// GET /stats eigene, zuletzt per Bookmarklet empfangene Daten
router.get('/stats', authenticate, (req, res) => {
const cache = getCache(req.user.id);
res.json({ configured: !!cache.fetchedAt, data: cache.data, fetchedAt: cache.fetchedAt || null });
});
// GET /ingest-token eigenes (ggf. neu erzeugtes) Token + Server-Origin, für den Bookmarklet-Code
router.get('/ingest-token', authenticate, (req, res) => {
const token = getOrCreateToken(req.user.id);
res.json({ token, origin: `${req.protocol}://${req.get('host')}` });
});
// POST /ingest-token/regenerate altes Token ungültig machen, neues erzeugen
// (z.B. falls der Bookmarklet-Code mal versehentlich geteilt wurde)
router.post('/ingest-token/regenerate', authenticate, (req, res) => {
const token = crypto.randomBytes(24).toString('hex');
db.prepare('INSERT OR REPLACE INTO makerworld_ingest_tokens (user_id, token) VALUES (?, ?)').run(req.user.id, token);
res.json({ token, origin: `${req.protocol}://${req.get('host')}` });
});
// OPTIONS/POST /ingest vom Bookmarklet aus makerworld.com aufgerufen (fremde
// Origin!) — daher bewusst OHNE "authenticate"-Middleware, das Ingest-Token im
// Body übernimmt die Rolle der Authentifizierung. CORS wird hier gezielt nur
// für diese eine Route geöffnet, nicht global für die App.
router.options('/ingest', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.sendStatus(204);
});
router.post('/ingest', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
const { token, statisticalData } = req.body || {};
if (!token || typeof token !== 'string') return res.status(400).json({ error: 'Token fehlt' });
const row = db.prepare('SELECT user_id FROM makerworld_ingest_tokens WHERE token=?').get(token);
if (!row) return res.status(403).json({ error: 'Ungültiges Token' });
try {
const data = mapStatisticalData(statisticalData);
const fetchedAt = Date.now();
const cache = getCache(row.user_id);
cache.data = data; cache.fetchedAt = fetchedAt;
persistCache(row.user_id, data, fetchedAt);
console.log(`[MakerWorld:${row.user_id}] Daten per Bookmarklet empfangen: ${data.downloads} Downloads, ${data.likes} Likes`);
res.json({ ok: true });
} catch (e) {
res.status(400).json({ error: e.message });
}
});
module.exports = router;