diff --git a/backend/src/tools/makerworld/routes.js b/backend/src/tools/makerworld/routes.js deleted file mode 100644 index 8d8e5dc..0000000 --- a/backend/src/tools/makerworld/routes.js +++ /dev/null @@ -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; diff --git a/frontend/src/tools/statistik.jsx b/frontend/src/tools/statistik.jsx index 959710c..240df9a 100644 --- a/frontend/src/tools/statistik.jsx +++ b/frontend/src/tools/statistik.jsx @@ -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
Lädt…
; - - 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 ( -
-
-
EINRICHTUNG (EINMALIG)
-
- Kein Login, kein Passwort nötig — nur ein Lesezeichen. Öffne die Seite{' '} - makerworld.com/de/my/data-overview/model{' '} - 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. -
- -
Am PC:
-
- Lesezeichenleiste anzeigen, dann diesen Link dorthin ziehen: -
-
- e.preventDefault()} draggable="true" - style={{...S.btn('#4ecdc4'),display:'inline-block',cursor:'grab',textDecoration:'none'}}> - 📌 DickenDock Sync - -
- -
Am Handy (Chrome):
-
- Normale Lesezeichen mit javascript:-Code werden von mobilen - Browsern beim Antippen mittlerweile blockiert. Zuverlässig funktioniert stattdessen der - "Alternative Suchmaschine"-Trick: -
1. Code unten kopieren -
2. Chrome → ⋮ → Einstellungen → Suchmaschine → Suchmaschinen verwalten → "Hinzufügen" -
3. Suchmaschine: z.B. DickenDock Sync, Suchwort: z.B. dds, URL: den kopierten Code einfügen -
4. Speichern, dann auf der MakerWorld-Statistikseite in die Adresszeile tippen, dds eingeben, Leertaste, Enter -
- -
- Falls der Button nichts in die Zwischenablage legt (kommt auf manchen Handys/PWAs vor): - hier antippen, „Alles auswählen" wählen, dann normal kopieren. -
-