revert: MakerWorld Statistik-Integration erstmal zurueckgebaut
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user