Added Mii renderer

This commit is contained in:
Jonathan Barrow 2021-08-22 11:22:17 -04:00
parent b9b68594eb
commit bf2a4eba12
12 changed files with 4044 additions and 73 deletions

3
.gitignore vendored
View File

@ -62,4 +62,5 @@ sign.js
t.js
config.json
servers.json
certs
certs
/cdn

3699
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,9 @@
"express": "^4.17.1",
"express-subdomain": "^1.0.5",
"fs-extra": "^8.1.0",
"got": "^11.8.2",
"image-pixels": "^2.2.2",
"kaitai-struct": "^0.9.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",
"mongoose": "^5.8.3",
@ -33,6 +36,7 @@
"node-rsa": "^1.0.7",
"nodemailer": "^6.4.2",
"prompt": "^1.0.0",
"tga": "^1.0.4",
"xmlbuilder": "^13.0.2",
"xmlbuilder2": "0.0.4"
}

View File

@ -132,7 +132,7 @@ async function getUserProfileJSONByPID(pid) {
primary: user.get('email.primary') ? 'Y' : 'N',
reachable: user.get('email.reachable') ? 'Y' : 'N',
type: 'DEFAULT',
updated_by: 'USER', // Can also be INTERNAL WS, don't know the difference
updated_by: 'INTERNAL WS', // Can also be INTERNAL WS, don't know the difference
validated: user.get('email.validated') ? 'Y' : 'N',
//validated_date: user.get('email.validated_date') // not used atm
},
@ -143,9 +143,10 @@ async function getUserProfileJSONByPID(pid) {
mii_hash: user.get('mii.hash'),
mii_images: {
mii_image: {
cached_url: user.get('mii.image_url'),
// Images MUST be loaded over HTTPS or console ignores them
cached_url: `https://mii-images.cdn.pretendo.cc/${user.pid}/standard.tga`,
id: user.get('mii.image_id'),
url: user.get('mii.image_url'),
url: `https://mii-images.cdn.pretendo.cc/${user.pid}/standard.tga`,
type: 'standard'
}
},

214
src/mii.js Normal file
View File

@ -0,0 +1,214 @@
const KaitaiStream = require('kaitai-struct/KaitaiStream');
class Mii extends KaitaiStream {
constructor(arrayBuffer, byteOffset) {
super(arrayBuffer, byteOffset);
this.decode();
}
decode() {
// Decode raw data
// A lot of this goes unused
this.unknown1 = this.readU1();
this.characterSet = this.readBitsIntBe(2);
this.regionLock = this.readBitsIntBe(2);
this.profanityFlag = this.readBitsIntBe(1) !== 0;
this.copying = this.readBitsIntBe(1) !== 0;
this.unknown2 = this.readBitsIntBe(2);
this.slotIndex = this.readBitsIntBe(4);
this.pageIndex = this.readBitsIntBe(4);
this.version = this.readBitsIntBe(4);
this.unknown3 = this.readBitsIntBe(4);
this.systemId = Array(8).fill().map(() => this.readU1());
this.avatarId = Array(4).fill().map(() => this.readU1());
this.clientId = Array(6).fill().map(() => this.readU1());
this.padding = this.readU2le();
this.miiMetaData = this.readU2le();
this.miiName = Buffer.from(this.readBytes(20)).toString('utf16le');
this.height = this.readU1();
this.build = this.readU1();
this.faceColor = this.readBitsIntBe(3);
this.faceType = this.readBitsIntBe(4);
this.mingle = this.readBitsIntBe(1) !== 0;
this.faceMakeup = this.readBitsIntBe(4);
this.faceWrinkles = this.readBitsIntBe(4);
this.alignToByte();
this.hairType = this.readU1();
this.unknown5 = this.readBitsIntBe(4);
this.hairFlip = this.readBitsIntBe(1) !== 0;
this.hairColor = this.readBitsIntBe(3);
this.alignToByte();
this.eyeData = this.readU4le();
this.eyebrowData = this.readU4le();
this.noseData = this.readU2le();
this.mouthData = this.readU2le();
this.mouthData2 = this.readU2le();
this.facialHairData = this.readU2le();
this.glassesData = this.readU2le();
this.moleData = this.readU2le();
this.creatorName = Buffer.from(this.readBytes(20)).toString('utf16le');
this.padding2 = this.readU2le();
this.checksum = this.readU2le();
// Carve out more specific data from the above values
// TODO: read these bits directly instead of getting them later
this.gender = (this.miiMetaData & 1);
this.birthMonth = ((this.miiMetaData >> 1) & 15);
this.birthDay = ((this.miiMetaData >> 5) & 31);
this.favoriteColor = ((this.miiMetaData >> 10) & 15);
this.favorite = ((this.miiMetaData >> 14) & 1);
this.eyeType = (this.eyeData & 63);
this.eyeColor = ((this.eyeData >> 6) & 7);
this.eyeSize = ((this.eyeData >> 9) & 7);
this.eyeStretch = ((this.eyeData >> 13) & 7);
this.eyeRotation = ((this.eyeData >> 16) & 31);
this.eyeHorizontal = ((this.eyeData >> 21) & 15);
this.eyeVertical = ((this.eyeData >> 25) & 31);
this.eyebrowType = (this.eyebrowData & 31);
this.eyebrowColor = ((this.eyebrowData >> 5) & 7);
this.eyebrowSize = ((this.eyebrowData >> 8) & 15);
this.eyebrowStretch = ((this.eyebrowData >> 12) & 7);
this.eyebrowRotation = ((this.eyebrowData >> 16) & 15);
this.eyebrowHorizontal = ((this.eyebrowData >> 21) & 15);
this.eyebrowVertical = ((this.eyebrowData >> 25) & 31);
this.noseType = (this.noseData & 31);
this.noseSize = ((this.noseData >> 5) & 15);
this.noseVertical = ((this.noseData >> 9) & 31);
this.mouthType = (this.mouthData & 63);
this.mouthColor = ((this.mouthData >> 6) & 7);
this.mouthSize = ((this.mouthData >> 9) & 15);
this.mouthStretch = ((this.mouthData >> 13) & 7);
this.mouthVertical = (this.mouthData2 & 31);
this.facialHairMustache = ((this.mouthData2 >> 5) & 7);
this.facialHairType = (this.facialHairData & 7);
this.facialHairColor = ((this.facialHairData >> 3) & 7);
this.facialHairSize = ((this.facialHairData >> 6) & 15);
this.facialHairVertical = ((this.facialHairData >> 10) & 31);
this.glassesType = (this.glassesData & 15);
this.glassesColor = (this.glassesData >> 4) & 7;
this.glassesSize = (this.glassesData >> 7) & 15;
this.glassesVertical = (this.glassesData >> 11) & 15;
this.moleEnable = (this.moleData >> 15);
this.moleSize = ((this.moleData >> 1) & 15);
this.moleHorizontal = ((this.moleData >> 5) & 31);
this.moleVertical = ((this.moleData >> 10) & 31);
}
toStudioMii() {
/*
Can also disable randomization with:
let miiStudioData = Buffer.alloc(0x2F);
let next = 256;
and removing "randomizer" and the "miiStudioData.writeUInt8(randomizer);" call
*/
const miiStudioData = Buffer.alloc(0x2F);
const randomizer = Math.floor(256 * Math.random());
let next = randomizer;
let pos = 1;
function encodeMiiPart(partValue) {
const encoded = (7 + (partValue ^ next)) % 256;
next = encoded;
miiStudioData.writeUInt8(encoded, pos);
pos++;
}
miiStudioData.writeUInt8(randomizer);
if (this.facialHairColor === 0) {
encodeMiiPart(8);
} else {
encodeMiiPart(this.facialHairColor);
}
encodeMiiPart(this.facialHairType);
encodeMiiPart(this.build);
encodeMiiPart(this.eyeStretch);
encodeMiiPart(this.eyeColor + 8);
encodeMiiPart(this.eyeRotation);
encodeMiiPart(this.eyeSize);
encodeMiiPart(this.eyeType);
encodeMiiPart(this.eyeHorizontal);
encodeMiiPart(this.eyeVertical);
encodeMiiPart(this.eyebrowStretch);
if (this.eyebrowColor === 0) {
encodeMiiPart(8);
} else {
encodeMiiPart(this.eyebrowColor);
}
encodeMiiPart(this.eyebrowRotation);
encodeMiiPart(this.eyebrowSize);
encodeMiiPart(this.eyebrowType);
encodeMiiPart(this.eyebrowHorizontal);
encodeMiiPart(this.eyebrowVertical);
encodeMiiPart(this.faceColor);
encodeMiiPart(this.faceMakeup);
encodeMiiPart(this.faceType);
encodeMiiPart(this.faceWrinkles);
encodeMiiPart(this.favoriteColor);
encodeMiiPart(this.gender);
if (this.glassesColor == 0) {
encodeMiiPart(8);
} else if (this.glassesColor < 6) {
encodeMiiPart(this.glassesColor + 13);
} else {
encodeMiiPart(0);
}
encodeMiiPart(this.glassesSize);
encodeMiiPart(this.glassesType);
encodeMiiPart(this.glassesVertical);
if (this.hairColor == 0) {
encodeMiiPart(8);
} else {
encodeMiiPart(this.hairColor);
}
encodeMiiPart(this.hairFlip ? 1 : 0);
encodeMiiPart(this.hairType);
encodeMiiPart(this.height);
encodeMiiPart(this.moleSize);
encodeMiiPart(this.moleEnable);
encodeMiiPart(this.moleHorizontal);
encodeMiiPart(this.moleVertical);
encodeMiiPart(this.mouthStretch);
if (this.mouthColor < 4) {
encodeMiiPart(this.mouthColor + 19);
} else {
encodeMiiPart(0);
}
encodeMiiPart(this.mouthSize);
encodeMiiPart(this.mouthType);
encodeMiiPart(this.mouthVertical);
encodeMiiPart(this.facialHairSize);
encodeMiiPart(this.facialHairMustache);
encodeMiiPart(this.facialHairVertical);
encodeMiiPart(this.noseSize);
encodeMiiPart(this.noseType);
encodeMiiPart(this.noseVertical);
return miiStudioData;
}
}
module.exports = Mii;

View File

@ -2,8 +2,14 @@ const { Schema, model } = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const bcrypt = require('bcrypt');
const crypto = require('crypto');
const fs = require('fs-extra');
const path = require('path');
const imagePixels = require('image-pixels');
const TGA = require('tga');
const got = require('got');
const util = require('../util');
const { DeviceSchema } = require('./device');
const Mii = require('../mii');
const PNIDSchema = new Schema({
access_level: {
@ -190,6 +196,33 @@ PNIDSchema.methods.updateMii = async function({name, primary, data}) {
this.set('mii.name', name);
this.set('mii.primary', primary === 'Y');
this.set('mii.data', data);
this.set('mii.hash', crypto.randomBytes(7).toString('hex'));
this.set('mii.id', crypto.randomBytes(4).readUInt32LE());
this.set('mii.image_id', crypto.randomBytes(4).readUInt32LE());
const studioMii = new Mii(Buffer.from(data, 'base64'));
const converted = studioMii.toStudioMii();
const encodedStudioMiiData = converted.toString('hex');
const miiStudioUrl = `https://studio.mii.nintendo.com/miis/image.png?data=${encodedStudioMiiData}&type=face&width=128&instanceCount=1`;
const miiStudioNormalFaceImageData = await got(miiStudioUrl).buffer();
const pngData = await imagePixels(miiStudioNormalFaceImageData);
const tga = TGA.createTgaBuffer(pngData.width, pngData.height, pngData.data);
const userMiiPath = path.normalize(`${__dirname}/../../cdn/${this.get('pid')}/miis`);
fs.ensureDirSync(userMiiPath);
fs.writeFileSync(`${userMiiPath}/standard.tga`, tga);
fs.writeFileSync(`${userMiiPath}/normal_face.png`, miiStudioNormalFaceImageData);
const expressions = ['frustrated', 'smile_open_mouth', 'wink_left', 'sorrow', 'surprise_open_mouth'];
for (const expression of expressions) {
const miiStudioExpressionUrl = `https://studio.mii.nintendo.com/miis/image.png?data=${encodedStudioMiiData}&type=face&expression=${expression}&width=128&instanceCount=1`;
const miiStudioExpressionImageData = await got(miiStudioExpressionUrl).buffer();
fs.writeFileSync(`${userMiiPath}/${expression}.png`, miiStudioExpressionImageData);
}
const miiStudioBodyUrl = `https://studio.mii.nintendo.com/miis/image.png?data=${encodedStudioMiiData}&type=all_body&width=270&instanceCount=1`;
const miiStudioBodyImageData = await got(miiStudioBodyUrl).buffer();
fs.writeFileSync(`${userMiiPath}/body.png`, miiStudioBodyImageData);
await this.save();
};

View File

@ -13,6 +13,7 @@ const app = express();
const accountWiiU = require('./services/wiiu');
//const account3DS = require('./services/3ds');
const cdnService = require('./services/cdn');
// START APPLICATION
app.set('etag', false);
@ -30,6 +31,7 @@ app.use(xmlparser);
// import the servers into one
app.use(accountWiiU);
//app.use(account3DS);
app.use(cdnService);
// 404 handler
logger.info('Creating 404 status handler');

20
src/services/cdn/index.js Normal file
View File

@ -0,0 +1,20 @@
const express = require('express');
const subdomain = require('express-subdomain');
const logger = require('../../../logger');
const routes = require('./routes');
// Router to handle the subdomain
const miiImagesCDN = express.Router();
// Setup routes
logger.info('[CDN] Applying imported routes');
miiImagesCDN.use(routes.MII_IMAGES);
// Main router for endpoints
const router = express.Router();
// Create subdomains
logger.info('[CDN] Creating \'mii-images.cdn\' subdomain');
router.use(subdomain('mii-images.cdn', miiImagesCDN));
module.exports = router;

View File

@ -0,0 +1,3 @@
module.exports = {
MII_IMAGES: require('./mii-images.js'),
};

View File

@ -0,0 +1,17 @@
const router = require('express').Router();
const fs = require('fs-extra');
const path = require('path');
router.get('/:pid/:image', async (request, response) => {
const { pid, image } = request.params;
const userMiiImagePath = path.normalize(`${__dirname}/../../../../cdn/${pid}/miis/${image}`);
console.log(userMiiImagePath);
if (fs.existsSync(userMiiImagePath)) {
response.sendFile(userMiiImagePath);
} else {
response.sendStatus(404);
}
});
module.exports = router;

View File

@ -5,7 +5,7 @@ const clientHeaderCheck = require('../../../middleware/client-header');
/**
* [GET]
* Replacement for: https://account.nintendo.net/v1/api/miis
* Replacement for: https://account.pretendo.cc/v1/api/miis
* Description: Returns a list of NNID miis
*/
router.get('/', clientHeaderCheck, async (request, response) => {
@ -15,66 +15,65 @@ router.get('/', clientHeaderCheck, async (request, response) => {
const results = await PNID.where('pid', pids);
const miis = [];
// We don't have a Mii renderer yet so hard code the images
const hardCodedImages = [
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_standard.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_standard.png',
type: 'standard'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_frustrated_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_frustrated_face.png',
type: 'frustrated_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_happy_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_happy_face.png',
type: 'happy_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_like_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_like_face.png',
type: 'like_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_normal_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_normal_face.png',
type: 'normal_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_puzzled_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_puzzled_face.png',
type: 'puzzled_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_surprised_face.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_surprised_face.png',
type: 'surprised_face'
},
{
cached_url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_whole_body.png',
id: 1292086302,
url: 'http://mii-images.cdn.nintendo.net/zcz5c1xf62bb_whole_body.png',
type: 'whole_body'
}
];
for (const user of results) {
const { mii } = user;
const miiImages = [
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
type: 'standard'
},
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/frustrated.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/frustrated.png`,
type: 'frustrated_face'
},
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/smile_open_mouth.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/smile_open_mouth.png`,
type: 'happy_face'
},
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/wink_left.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/wink_left.png`,
type: 'like_face'
},
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/normal_face.png`,
type: 'normal_face'
},
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/sorrow.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/sorrow.png`,
type: 'puzzled_face'
},
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/surprised_open_mouth.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/surprised_open_mouth.png`,
type: 'surprised_face'
},
{
cached_url: `http://mii-images.cdn.pretendo.cc/${user.pid}/body.png`,
id: mii.id,
url: `http://mii-images.cdn.pretendo.cc/${user.pid}/body.png`,
type: 'whole_body'
}
];
miis.push({
data: mii.data.replace(/(\r\n|\n|\r)/gm, ''),
id: mii.id,
images: {
image: hardCodedImages
image: miiImages
},
name: mii.name,
pid: user.pid,

View File

@ -180,11 +180,7 @@ router.get('/@me/profile', clientHeaderCheck, async (request, response) => {
response.send(xmlbuilder.create({
person
}).end());
//response.send(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><person><accounts><account><attributes><attribute><id>84955611</id><name>ctr_initial_device_account_id</name><updated_by>USER</updated_by><value>195833643</value></attribute><attribute><id>84955489</id><name>environment</name><updated_by>USER</updated_by><value>PROD</value></attribute></attributes><domain>ESHOP.NINTENDO.NET</domain><type>INTERNAL</type><username>327329101</username></account></accounts><active_flag>Y</active_flag><birth_date>1998-09-22</birth_date><country>US</country><create_date>2017-12-29T04:11:24</create_date><device_attributes><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>persistent_id</name><value>80000043</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base</name><value>1200000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>transferable_id_base_common</name><value>1180000444b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_account</name><value>42ef6c46-0ea3-11ea-97fe-010144b6221d</value></device_attribute><device_attribute><created_date>2019-11-24T15:26:06</created_date><name>uuid_common</name><value>3d9b06d8-0ea3-11ea-97fe-010144b6221d</value></device_attribute></device_attributes><gender>M</gender><language>en</language><updated>2019-06-02T04:17:56</updated><marketing_flag>Y</marketing_flag><off_device_flag>Y</off_device_flag><pid>1750087940</pid><email><address>halolink44@gmail.com</address><id>50463196</id><parent>N</parent><primary>Y</primary><reachable>Y</reachable><type>DEFAULT</type><updated_by>INTERNAL WS</updated_by><validated>Y</validated><validated_date>2017-12-29T04:12:32</validated_date></email><mii><status>COMPLETED</status><data>AwBzMOlVognnx0GCk6r2p0D0B2n+cgAA0lJSAGUAZABEAHUAYwBrAHMAAAAAAGQrAAAWAQJoRBgmNEYUgRIXaI0AiiWBSUhQUgBlAGQARAB1AGMAawBzAHMAAAAAAJZY</data><id>1151699634</id><mii_hash>u2jg043u028x</mii_hash><mii_images><mii_image><cached_url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</cached_url><id>1319591505</id><url>https://mii-secure.account.nintendo.net/u2jg043u028x_standard.tga</url><type>standard</type></mii_image></mii_images><name>RedDucks</name><primary>Y</primary></mii><region>822870016</region><tz_name>America/New_York</tz_name><user_id>RedDuckss</user_id><utc_offset>-18000</utc_offset></person>`);
}, { separateArrayItems: true }).end());
});
/**
@ -273,7 +269,7 @@ router.put('/@me/miis/@primary', clientHeaderCheck, async (request, response) =>
const mii = request.body.get('mii');
const [name, primary, data] = [mii.get('name'), mii.get('primary'), mii.get('data')]
const [name, primary, data] = [mii.get('name'), mii.get('primary'), mii.get('data')];
await pnid.updateMii({name, primary, data});