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;

View File

@@ -197,207 +197,6 @@ function OrdersInPeriod({ fromDate, toDate }) {
);
}
// MakerWorld Creator-Center Statistik
function MakerworldTab({ toast }) {
const [state, setState] = useState(null); // { configured, data, fetchedAt }
const [loading, setLoading] = useState(true);
const [tokenInfo, setTokenInfo] = useState(null); // { token, origin }
const [regenerating, setRegenerating] = useState(false);
const [showModels, setShowModels] = useState(false);
const load = () => {
setLoading(true);
api('/tools/makerworld/stats').then(setState).catch(()=>{}).finally(()=>setLoading(false));
};
const loadToken = () => {
api('/tools/makerworld/ingest-token').then(setTokenInfo).catch(()=>{});
};
useEffect(() => { load(); loadToken(); }, []);
const regenerateToken = async () => {
if (!window.confirm('Neues Bookmarklet erzeugen? Das alte funktioniert danach nicht mehr — du musst es neu ins Lesezeichen legen.')) return;
setRegenerating(true);
try {
const r = await api('/tools/makerworld/ingest-token/regenerate', { method:'POST' });
setTokenInfo(r);
toast?.('Neues Bookmarklet erzeugt — bitte neu einrichten');
} catch(e) { toast?.(e.message,'error'); }
finally { setRegenerating(false); }
};
// Skript-Inhalt getrennt von der Verpackung, damit er sauber Base64-kodiert werden kann —
// das eliminiert praktisch alle Leerzeichen/Anführungszeichen im übertragenen Code, die von
// mobilen Tastaturen (intelligente Anführungszeichen) oder URL-Feldern verändert werden könnten.
const bookmarkletInner = tokenInfo ? `(function(){try{var el=document.getElementById('__NEXT_DATA__');if(!el){alert('MakerWorld: __NEXT_DATA__ nicht gefunden. Bist du auf der Statistikseite (/my/data-overview/model)?');return;}var nd=JSON.parse(el.textContent);var sd=nd.props&&nd.props.pageProps&&nd.props.pageProps.statisticalData;if(!sd||!sd.summary){alert('MakerWorld: Keine Statistikdaten auf dieser Seite gefunden.');return;}fetch('${tokenInfo.origin}/api/tools/makerworld/ingest',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:'${tokenInfo.token}',statisticalData:sd})}).then(function(r){return r.json();}).then(function(j){alert(j.ok?'DickenDock aktualisiert ✓':('Fehler: '+(j.error||'unbekannt')));}).catch(function(e){alert('Fehler: '+e.message);});}catch(e){alert('Fehler: '+e.message);}})();` : '';
const bookmarkletCode = tokenInfo
? (() => {
const b64 = btoa(unescape(encodeURIComponent(bookmarkletInner)));
const urlSafeB64 = b64.replace(/\+/g, '-').replace(/\//g, '_');
return `javascript:eval(atob('${urlSafeB64}'.replace(/-/g,'+').replace(/_/g,'/')))`;
})()
: '';
const codeRef = useRef(null);
const copyBookmarklet = async () => {
// Moderne Clipboard-API zuerst versuchen
try {
await navigator.clipboard.writeText(bookmarkletCode);
toast?.('Bookmarklet-Code kopiert');
return;
} catch {}
// Fallback: klassisches execCommand über eine unsichtbare Textarea
// (Clipboard-API meldet in manchen PWA/Mobile-Kontexten fälschlich Erfolg
// oder schlägt ohne Fehler fehl — dieser Weg ist zuverlässiger)
try {
const ta = document.createElement('textarea');
ta.value = bookmarkletCode;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.focus();
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
if (ok) { toast?.('Bookmarklet-Code kopiert'); return; }
} catch {}
toast?.('Kopieren nicht möglich — bitte Code unten manuell markieren und kopieren','error');
selectAllCode();
};
const selectAllCode = () => {
if (codeRef.current) {
codeRef.current.focus();
codeRef.current.select();
}
};
if (loading) return <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>Lädt</div>;
const d = state?.data;
const fmtInt = v => (v ?? 0).toLocaleString('de-DE');
const fmtAgo = ts => {
if (!ts) return '—';
const min = Math.round((Date.now()-ts)/60000);
if (min < 1) return 'gerade eben';
if (min < 60) return `vor ${min} Min.`;
if (min < 1440) return `vor ${Math.round(min/60)} Std.`;
return `vor ${Math.round(min/1440)} Tag(en)`;
};
return (
<div>
<div style={S.card}>
<div style={S.head}>EINRICHTUNG (EINMALIG)</div>
<div style={{...S.sub,marginBottom:10}}>
Kein Login, kein Passwort nötig nur ein Lesezeichen. Öffne die Seite{' '}
<code style={{color:'#4ecdc4'}}>makerworld.com/de/my/data-overview/model</code>{' '}
in deinem eigenen Browser (wo du eh eingeloggt bist), klicke dann auf das Lesezeichen
die aktuell auf der Seite geladenen Zahlen werden direkt an DickenDock geschickt.
</div>
<div style={{...S.sub,marginBottom:6,fontWeight:700,color:'rgba(255,255,255,0.6)'}}>Am PC:</div>
<div style={{...S.sub,marginBottom:10}}>
Lesezeichenleiste anzeigen, dann diesen Link dorthin ziehen:
</div>
<div style={{marginBottom:14}}>
<a href={bookmarkletCode} onClick={e=>e.preventDefault()} draggable="true"
style={{...S.btn('#4ecdc4'),display:'inline-block',cursor:'grab',textDecoration:'none'}}>
📌 DickenDock Sync
</a>
</div>
<div style={{...S.sub,marginBottom:6,fontWeight:700,color:'rgba(255,255,255,0.6)'}}>Am Handy (Chrome):</div>
<div style={{...S.sub,marginBottom:10}}>
Normale Lesezeichen mit <code style={{color:'#4ecdc4'}}>javascript:</code>-Code werden von mobilen
Browsern beim Antippen mittlerweile blockiert. Zuverlässig funktioniert stattdessen der
"Alternative Suchmaschine"-Trick:
<br/>1. Code unten kopieren
<br/>2. Chrome Einstellungen Suchmaschine Suchmaschinen verwalten "Hinzufügen"
<br/>3. Suchmaschine: z.B. <code style={{color:'#4ecdc4'}}>DickenDock Sync</code>, Suchwort: z.B. <code style={{color:'#4ecdc4'}}>dds</code>, URL: den kopierten Code einfügen
<br/>4. Speichern, dann auf der MakerWorld-Statistikseite in die Adresszeile tippen, <code style={{color:'#4ecdc4'}}>dds</code> eingeben, Leertaste, Enter
</div>
<button onClick={copyBookmarklet} style={{...S.btn('#4ecdc4')}}>
📋 Bookmarklet-Code kopieren
</button>
<div style={{...S.sub,marginTop:10,marginBottom:6}}>
Falls der Button nichts in die Zwischenablage legt (kommt auf manchen Handys/PWAs vor):
hier antippen, Alles auswählen" wählen, dann normal kopieren.
</div>
<textarea ref={codeRef} readOnly value={bookmarkletCode} onClick={selectAllCode}
rows={3} style={{...S.inp,resize:'vertical',fontSize:9,fontFamily:'monospace',wordBreak:'break-all'}}/>
<div style={{...S.sub,marginTop:10}}>
Firefox für Android unterstützt diesen Trick leider nicht zuverlässig — dafür entweder kurz zu
Chrome wechseln, oder den <b>Kiwi Browser</b> installieren (Chromium-basiert, unterstützt
klassische Lesezeichen-Bookmarklets wieder ganz normal).
</div>
<div style={{display:'flex',gap:8,marginTop:14,paddingTop:14,borderTop:'1px solid rgba(255,255,255,0.07)'}}>
<button onClick={regenerateToken} disabled={regenerating} style={{...S.btn('#f87171'),opacity:regenerating?0.5:1}}>
{regenerating?'…':'🔄 Bookmarklet zurücksetzen'}
</button>
</div>
</div>
{!state?.configured && (
<div style={S.card}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>
Noch keine Daten empfangen — Lesezeichen einrichten und einmal anklicken.
</div>
</div>
)}
{d && (
<>
<div style={S.card}>
<div style={S.head}>CREATOR CENTER {state.fetchedAt ? `· ${fmtAgo(state.fetchedAt)}` : ''}</div>
<div style={{display:'flex',flexWrap:'wrap',gap:20,marginBottom:16}}>
<KPI label="Downloads" value={fmtInt(d.downloads)} color="#4ecdc4"/>
<KPI label="Prints" value={fmtInt(d.prints)} color="#4ecdc4"/>
<KPI label="Likes" value={fmtInt(d.likes)} color="#ff6b9d"/>
<KPI label="Collections" value={fmtInt(d.collections)} color="#ff6b9d"/>
<KPI label="Boosts" value={fmtInt(d.boosts)} color="#ffe66d"/>
<KPI label="Follower" value={fmtInt(d.followers)} color="#ffe66d"/>
</div>
<div style={{borderTop:'1px solid rgba(255,255,255,0.06)',paddingTop:16,display:'flex',flexWrap:'wrap',gap:20}}>
<KPI label="Views" value={fmtInt(d.views)} color="#60a5fa"/>
<KPI label="Impressions" value={fmtInt(d.impressions)} color="#60a5fa"/>
<KPI label="Creator Points" value={fmtInt(d.creatorPoints)} color="#c084fc"/>
<KPI label="Model Points" value={fmtInt(d.modelPoints)} color="#c084fc" sub="aus Modellen"/>
<KPI label="MakerLab Points" value={fmtInt(d.instructionPoints)} color="#c084fc" sub="aus Anleitungen"/>
</div>
</div>
{d.models?.length > 0 && (
<div style={S.card}>
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:showModels?10:0}}>
<div style={{...S.head,marginBottom:0}}>MODELLE ({d.models.length})</div>
<button onClick={()=>setShowModels(p=>!p)} style={{...S.btn('#888888'),fontSize:10}}>
{showModels ? '▲ einklappen' : '▼ anzeigen'}
</button>
</div>
{showModels && [...d.models].sort((a,b)=>b.downloads-a.downloads).map(m => (
<div key={m.id} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 0',
borderBottom:'1px solid rgba(255,255,255,0.05)',flexWrap:'wrap'}}>
<span style={{color:'rgba(255,255,255,0.8)',fontFamily:'monospace',fontSize:12,flex:1,minWidth:160}}>
{m.title}
</span>
<span style={{color:'#4ecdc4',fontFamily:"'Space Mono',monospace",fontSize:11}}>↓{fmtInt(m.downloads)}</span>
<span style={{color:'#ff6b9d',fontFamily:"'Space Mono',monospace",fontSize:11}}>♥{fmtInt(m.likes)}</span>
<span style={{color:'#60a5fa',fontFamily:"'Space Mono',monospace",fontSize:11}}>👁{fmtInt(m.views)}</span>
<span style={{color:'#c084fc',fontFamily:"'Space Mono',monospace",fontSize:11}}>{m.points?.toFixed?.(1) ?? m.points}pt</span>
</div>
))}
</div>
)}
</>
)}
</div>
);
}
export default function Statistik({ mobile, user, toast }) {
const [fromDate, setFromDate] = useState('');
const [toDate, setToDate] = useState(todayLocal());
@@ -471,7 +270,7 @@ export default function Statistik({ mobile, user, toast }) {
{/* ── Tabs ── */}
<div style={{display:'flex',gap:4,marginBottom:16,overflowX:'auto',overflowY:'hidden',
scrollbarWidth:'none',WebkitOverflowScrolling:'touch',paddingBottom:2}}>
{[['overview','📈 Übersicht'],['revenue','💰 Einnahmen'],['expenses','💸 Ausgaben'],['orders','📦 Bestellungen'],['makerworld','🖨️ MakerWorld']].map(([id,label])=>(
{[['overview','📈 Übersicht'],['revenue','💰 Einnahmen'],['expenses','💸 Ausgaben'],['orders','📦 Bestellungen']].map(([id,label])=>(
<button key={id} onClick={()=>setTab(id)} style={{
background: tab===id ? 'rgba(255,255,255,0.08)' : 'transparent',
border: `1px solid ${tab===id ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.08)'}`,
@@ -706,11 +505,6 @@ export default function Statistik({ mobile, user, toast }) {
{tab==='orders' && (
<OrdersInPeriod fromDate={fromDate} toDate={toDate}/>
)}
{/* ══ MAKERWORLD ══ */}
{tab==='makerworld' && (
<MakerworldTab toast={toast}/>
)}
</div>
);
}