122 lines
4.8 KiB
JavaScript
122 lines
4.8 KiB
JavaScript
'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 };
|