'use strict'; /* * skins.js – Skin und Umhang über die Minecraft-Dienste ändern * ------------------------------------------------------------- * Der Launcher hält bereits gültige Zugangsdaten des angemeldeten Kontos. * Damit lässt sich der Skin direkt setzen, ohne Umweg über den Browser. * * Verwendete Wege (api.minecraftservices.com): * POST /minecraft/profile/skins Skin hochladen (Datei oder Adresse) * DELETE /minecraft/profile/skins/active Skin zurücksetzen (Standard-Steve/Alex) * GET /minecraft/profile aktuelles Profil samt Skin-Adresse */ const fs = require('fs'); const path = require('path'); const API = 'https://api.minecraftservices.com'; const UA = 'AeroMC-Launcher'; // Minecraft nimmt nur PNG, und die Vorlagen sind 64x64 bzw. 64x32 const MAX_BYTES = 512 * 1024; function pruefeDatei(dateiPfad) { if (!dateiPfad || !fs.existsSync(dateiPfad)) return { ok: false, message: 'Datei nicht gefunden.' }; if (!/\.png$/i.test(dateiPfad)) return { ok: false, message: 'Minecraft nimmt nur PNG-Dateien.' }; const st = fs.statSync(dateiPfad); if (!st.isFile()) return { ok: false, message: 'Das ist keine Datei.' }; if (st.size > MAX_BYTES) return { ok: false, message: 'Die Datei ist zu groß (über 512 KB).' }; // PNG-Kennung und Maße aus dem Kopf lesen, bevor wir sie hochschicken const kopf = Buffer.alloc(33); const fd = fs.openSync(dateiPfad, 'r'); try { fs.readSync(fd, kopf, 0, 33, 0); } finally { fs.closeSync(fd); } const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); if (!kopf.subarray(0, 8).equals(PNG)) return { ok: false, message: 'Die Datei ist kein gültiges PNG.' }; const breite = kopf.readUInt32BE(16); const hoehe = kopf.readUInt32BE(20); // 64x64 ist heute üblich, 64x32 sind die alten Skins if (!(breite === 64 && (hoehe === 64 || hoehe === 32))) { return { ok: false, message: `Ein Skin muss 64×64 (oder 64×32) Pixel groß sein – diese Datei ist ${breite}×${hoehe}.` }; } return { ok: true, breite, hoehe }; } function fehlerText(status) { if (status === 401) return 'Die Anmeldung ist abgelaufen. Bitte das Konto neu verbinden.'; if (status === 403) return 'Minecraft lehnt die Änderung ab. Hat das Konto Minecraft Java Edition?'; if (status === 429) return 'Zu viele Änderungen in kurzer Zeit. Bitte einen Moment warten.'; return 'Minecraft-Dienste antworten mit HTTP ' + status + '.'; } /* * Skin setzen. * variante: 'classic' (breite Arme) oder 'slim' (schmale Arme, Alex) */ async function setzeSkin(accessToken, dateiPfad, variante) { const geprueft = pruefeDatei(dateiPfad); if (!geprueft.ok) return geprueft; const form = new FormData(); form.append('variant', variante === 'slim' ? 'slim' : 'classic'); const daten = fs.readFileSync(dateiPfad); form.append('file', new Blob([daten], { type: 'image/png' }), path.basename(dateiPfad)); const res = await fetch(API + '/minecraft/profile/skins', { method: 'POST', headers: { Authorization: 'Bearer ' + accessToken, 'User-Agent': UA }, body: form, }); if (!res.ok) return { ok: false, message: fehlerText(res.status) }; let profil = null; try { profil = await res.json(); } catch { /* Antwort ohne Rumpf ist auch in Ordnung */ } return { ok: true, variante: variante === 'slim' ? 'slim' : 'classic', profil }; } // Skin auf den Standard zurücksetzen async function setzeSkinZurueck(accessToken) { const res = await fetch(API + '/minecraft/profile/skins/active', { method: 'DELETE', headers: { Authorization: 'Bearer ' + accessToken, 'User-Agent': UA }, }); if (!res.ok) return { ok: false, message: fehlerText(res.status) }; return { ok: true }; } /* * Aktuelles Profil abrufen – liefert die Adresse des aktiven Skins, * damit die Oberfläche vorher/nachher zeigen kann. */ /* * Mojang liefert Skin-Adressen bis heute als "http://textures.minecraft.net/…". * Die Oberfläche lässt nach ihrer Sicherheitsrichtlinie nur https zu, das Bild * würde also blockiert. textures.minecraft.net beantwortet dieselbe Adresse * auch über https – daher heben wir sie an. */ function aufHttps(url) { const s = String(url || ''); return s.startsWith('http://') ? 'https://' + s.slice('http://'.length) : s; } async function holeProfil(accessToken) { const res = await fetch(API + '/minecraft/profile', { headers: { Authorization: 'Bearer ' + accessToken, 'User-Agent': UA, Accept: 'application/json' }, }); if (!res.ok) return { ok: false, message: fehlerText(res.status) }; const daten = await res.json(); const aktiv = (daten.skins || []).find((s) => s.state === 'ACTIVE') || null; return { ok: true, name: daten.name, id: daten.id, skinUrl: aktiv ? aufHttps(aktiv.url) : '', variante: aktiv ? String(aktiv.variant || '').toLowerCase() : '', }; } module.exports = { setzeSkin, setzeSkinZurueck, holeProfil, pruefeDatei, aufHttps };