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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user