mirror of
https://github.com/PretendoNetwork/juxtaposition-ui.git
synced 2026-03-21 17:34:24 -05:00
Some checks are pending
Build and Publish Docker Image / build-publish (push) Waiting to run
527 lines
14 KiB
JavaScript
527 lines
14 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
|
const crypto = require('crypto');
|
|
const database = require('./database');
|
|
const logger = require('./logger');
|
|
const grpc = require('nice-grpc');
|
|
const config = require('../config.json');
|
|
const { SETTINGS } = require('./models/settings');
|
|
const { CONTENT } = require('./models/content');
|
|
const { NOTIFICATION } = require('./models/notifications');
|
|
const { COMMUNITY } = require('./models/communities');
|
|
const { AccountDefinition } = require('@pretendonetwork/grpc/account/account_service');
|
|
const { FriendsDefinition } = require('@pretendonetwork/grpc/friends/friends_service');
|
|
const { APIDefinition } = require('@pretendonetwork/grpc/api/api_service');
|
|
const translations = require('./translations');
|
|
const HashMap = require('hashmap');
|
|
const TGA = require('tga');
|
|
const imagePixels = require('image-pixels');
|
|
const pako = require('pako');
|
|
const PNG = require('pngjs').PNG;
|
|
const bmp = require('bmp-js');
|
|
const sharp = require('sharp');
|
|
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
|
const crc32 = require('crc/crc32');
|
|
const communityMap = new HashMap();
|
|
const userMap = new HashMap();
|
|
|
|
const { ip: friendsIP, port: friendsPort, api_key: friendsKey } = config.grpc.friends;
|
|
const friendsChannel = grpc.createChannel(`${friendsIP}:${friendsPort}`);
|
|
const friendsClient = grpc.createClient(FriendsDefinition, friendsChannel);
|
|
|
|
const { ip: apiIP, port: apiPort, api_key: apiKey } = config.grpc.account;
|
|
const apiChannel = grpc.createChannel(`${apiIP}:${apiPort}`);
|
|
const apiClient = grpc.createClient(APIDefinition, apiChannel);
|
|
|
|
const accountChannel = grpc.createChannel(`${apiIP}:${apiPort}`);
|
|
const accountClient = grpc.createClient(AccountDefinition, accountChannel);
|
|
|
|
const s3 = new S3Client({
|
|
endpoint: config.aws.endpoint,
|
|
forcePathStyle: true,
|
|
region: config.aws.region,
|
|
credentials: {
|
|
accessKeyId: config.aws.spaces.key,
|
|
secretAccessKey: config.aws.spaces.secret,
|
|
},
|
|
});
|
|
|
|
nameCache();
|
|
|
|
function nameCache() {
|
|
database.connect().then(async e => {
|
|
const communities = await COMMUNITY.find();
|
|
if (communities !== null) {
|
|
for (let i = 0; i < communities.length; i++) {
|
|
if (communities[i].title_id !== null) {
|
|
for (let j = 0; j < communities[i].title_id.length; j++) {
|
|
communityMap.set(communities[i].title_id[j], communities[i].name);
|
|
communityMap.set(communities[i].title_id[j] + '-id', communities[i].olive_community_id);
|
|
}
|
|
communityMap.set(communities[i].olive_community_id, communities[i].name);
|
|
}
|
|
}
|
|
logger.success('Created community index of ' + communities.length + ' communities');
|
|
}
|
|
const users = await database.getUsersSettings(-1);
|
|
if (users !== null) {
|
|
for (let i = 0; i < users.length; i++) {
|
|
if (users[i].pid !== null) {
|
|
userMap.set(users[i].pid, users[i].screen_name.replace(/[\u{0080}-\u{FFFF}]/gu, '').replace(/\u202e/g, ''));
|
|
}
|
|
}
|
|
logger.success('Created user index of ' + users.length + ' users');
|
|
}
|
|
|
|
}).catch(error => {
|
|
logger.error(error);
|
|
});
|
|
}
|
|
|
|
// TODO - This doesn't belong here, just hacking it in. Gonna redo this whole server anyway so fuck it
|
|
const INVALID_POST_BODY_REGEX = /[^\p{L}\p{P}\d\n\r$^¨←→↑↓√¦⇒⇔¤¢€£¥™©®+×÷=±∞˘˙¸˛˜°¹²³♭♪¬¯¼½¾♡♥●◆■▲▼☆★♀♂<> ]/gu;
|
|
async function create_user(pid, experience, notifications) {
|
|
const pnid = await this.getUserDataFromPid(pid);
|
|
if (!pnid) {
|
|
return;
|
|
}
|
|
const newSettings = {
|
|
pid: pid,
|
|
screen_name: pnid.mii.name,
|
|
game_skill: experience,
|
|
receive_notifications: notifications,
|
|
};
|
|
const newContent = {
|
|
pid: pid
|
|
};
|
|
const newSettingsObj = new SETTINGS(newSettings);
|
|
await newSettingsObj.save();
|
|
|
|
const newContentObj = new CONTENT(newContent);
|
|
await newContentObj.save();
|
|
|
|
this.setName(pid, pnid.mii.name);
|
|
}
|
|
function decodeParamPack(paramPack) {
|
|
/* Decode base64 */
|
|
let dec = Buffer.from(paramPack, 'base64').toString('ascii');
|
|
/* Remove starting and ending '/', split into array */
|
|
dec = dec.slice(1, -1).split('\\');
|
|
/* Parameters are in the format [name, val, name, val]. Copy into out{}. */
|
|
const out = {};
|
|
for (let i = 0; i < dec.length; i += 2) {
|
|
out[dec[i].trim()] = dec[i + 1].trim();
|
|
}
|
|
return out;
|
|
}
|
|
function processServiceToken(encryptedToken) {
|
|
try {
|
|
const B64token = Buffer.from(encryptedToken, 'base64');
|
|
const decryptedToken = this.decryptToken(B64token);
|
|
const token = this.unpackToken(decryptedToken);
|
|
|
|
// * Only allow token types 1 (Wii U) and 2 (3DS)
|
|
if (token.system_type !== 1 && token.system_type !== 2) {
|
|
return null;
|
|
}
|
|
|
|
return token.pid;
|
|
} catch (e) {
|
|
console.error(e);
|
|
return null;
|
|
}
|
|
|
|
}
|
|
function decryptToken(token) {
|
|
if (!config.aes_key) {
|
|
throw new Error('Service token AES key not found. Set config.aes_key');
|
|
}
|
|
|
|
const iv = Buffer.alloc(16);
|
|
const key = Buffer.from(config.aes_key, 'hex');
|
|
|
|
const expectedChecksum = token.readUint32BE();
|
|
const encryptedBody = token.subarray(4);
|
|
|
|
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
|
|
const decrypted = Buffer.concat([
|
|
decipher.update(encryptedBody),
|
|
decipher.final()
|
|
]);
|
|
|
|
if (expectedChecksum !== crc32(decrypted)) {
|
|
throw new Error('Checksum did not match. Failed decrypt. Are you using the right key?');
|
|
}
|
|
|
|
return decrypted;
|
|
}
|
|
function unpackToken(token) {
|
|
return {
|
|
system_type: token.readUInt8(0x0),
|
|
token_type: token.readUInt8(0x1),
|
|
pid: token.readUInt32LE(0x2),
|
|
expire_time: token.readBigUInt64LE(0x6),
|
|
title_id: token.readBigUInt64LE(0xE),
|
|
access_level: token.readInt8(0x16)
|
|
};
|
|
}
|
|
async function processPainting(painting, isTGA) {
|
|
if (isTGA) {
|
|
const paintingBuffer = Buffer.from(painting, 'base64');
|
|
let output = '';
|
|
try {
|
|
output = pako.inflate(paintingBuffer);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
let tga;
|
|
try {
|
|
tga = new TGA(Buffer.from(output));
|
|
} catch (e) {
|
|
console.error(e);
|
|
return null;
|
|
}
|
|
const png = new PNG({
|
|
width: tga.width,
|
|
height: tga.height
|
|
});
|
|
png.data = tga.pixels;
|
|
return PNG.sync.write(png);
|
|
//return `data:image/png;base64,${pngBuffer.toString('base64')}`;
|
|
} else {
|
|
const paintingBuffer = Buffer.from(painting, 'base64');
|
|
const bitmap = bmp.decode(paintingBuffer);
|
|
const tga = this.createBMPTgaBuffer(bitmap.width, bitmap.height, bitmap.data, false);
|
|
|
|
let output;
|
|
try {
|
|
output = pako.deflate(tga, { level: 6 });
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
return new Buffer(output).toString('base64');
|
|
}
|
|
}
|
|
function nintendoPasswordHash(password, pid) {
|
|
const pidBuffer = Buffer.alloc(4);
|
|
pidBuffer.writeUInt32LE(pid);
|
|
|
|
const unpacked = Buffer.concat([
|
|
pidBuffer,
|
|
Buffer.from('\x02\x65\x43\x46'),
|
|
Buffer.from(password)
|
|
]);
|
|
return crypto.createHash('sha256').update(unpacked).digest().toString('hex');
|
|
}
|
|
function getCommunityHash() {
|
|
return communityMap;
|
|
}
|
|
function getUserHash() {
|
|
return userMap;
|
|
}
|
|
function refreshCache() {
|
|
nameCache();
|
|
}
|
|
function setName(pid, name) {
|
|
if (!pid || !name) {
|
|
return;
|
|
}
|
|
this.userMap.delete(pid);
|
|
this.userMap.set(pid, name.replace(/[\u{0080}-\u{FFFF}]/gu, '').replace(/\u202e/g, ''));
|
|
}
|
|
|
|
function updateCommunityHash(community) {
|
|
if (!community) {
|
|
return;
|
|
}
|
|
for (let i = 0; i < community.title_id.length; i++) {
|
|
communityMap.set(community.title_id[i], community.name);
|
|
communityMap.set(community.title_id[i] + '-id', community.olive_community_id);
|
|
}
|
|
communityMap.set(community.olive_community_id, community.name);
|
|
}
|
|
|
|
async function resizeImage(file, width, height) {
|
|
return new Promise(function (resolve) {
|
|
const image = Buffer.from(file, 'base64');
|
|
sharp(image)
|
|
.resize({ height: height, width: width })
|
|
.toBuffer()
|
|
.then(data => {
|
|
resolve(data);
|
|
}).catch(err => console.error(err));
|
|
});
|
|
}
|
|
|
|
async function getTGAFromPNG(image) {
|
|
const pngData = await imagePixels(Buffer.from(image));
|
|
const tga = TGA.createTgaBuffer(pngData.width, pngData.height, pngData.data);
|
|
let output;
|
|
try {
|
|
output = pako.deflate(tga, { level: 6 });
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
return new Buffer(output).toString('base64').trim();
|
|
}
|
|
|
|
function createBMPTgaBuffer(width, height, pixels, dontFlipY) {
|
|
const buffer = Buffer.alloc(18 + pixels.length);
|
|
// write header
|
|
buffer.writeInt8(0, 0);
|
|
buffer.writeInt8(0, 1);
|
|
buffer.writeInt8(2, 2);
|
|
buffer.writeInt16LE(0, 3);
|
|
buffer.writeInt16LE(0, 5);
|
|
buffer.writeInt8(0, 7);
|
|
buffer.writeInt16LE(0, 8);
|
|
buffer.writeInt16LE(0, 10);
|
|
buffer.writeInt16LE(width, 12);
|
|
buffer.writeInt16LE(height, 14);
|
|
buffer.writeInt8(32, 16);
|
|
buffer.writeInt8(8, 17);
|
|
|
|
let offset = 18;
|
|
for (let i = 0; i < height; i++) {
|
|
for (let j = 0; j < width; j++) {
|
|
const idx = ((dontFlipY ? i : height - i - 1) * width + j) * 4;
|
|
buffer.writeUInt8(pixels[idx + 1], offset++); // b
|
|
buffer.writeUInt8(pixels[idx + 2], offset++); // g
|
|
buffer.writeUInt8(pixels[idx + 3], offset++); // r
|
|
buffer.writeUInt8(255, offset++); // a
|
|
}
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
function processLanguage(paramPackData) {
|
|
if (!paramPackData) {
|
|
return translations.EN;
|
|
}
|
|
switch (paramPackData.language_id) {
|
|
case '0':
|
|
return translations.JA;
|
|
case '1':
|
|
return translations.EN;
|
|
case '2':
|
|
return translations.FR;
|
|
case '3':
|
|
return translations.DE;
|
|
case '4':
|
|
return translations.IT;
|
|
case '5':
|
|
return translations.ES;
|
|
case '6':
|
|
return translations.ZH;
|
|
case '7':
|
|
return translations.KO;
|
|
case '8':
|
|
return translations.NL;
|
|
case '9':
|
|
return translations.PT;
|
|
case '10':
|
|
return translations.RU;
|
|
case '11':
|
|
return translations.ZH;
|
|
default:
|
|
return translations.EN;
|
|
}
|
|
}
|
|
async function uploadCDNAsset(key, data, acl) {
|
|
const awsPutParams = new PutObjectCommand({
|
|
Body: data,
|
|
Key: key,
|
|
Bucket: config.aws.bucket,
|
|
ACL: acl
|
|
});
|
|
try {
|
|
await s3.send(awsPutParams);
|
|
return true;
|
|
} catch (e) {
|
|
console.error(e);
|
|
return false;
|
|
}
|
|
}
|
|
async function newNotification(notification) {
|
|
const now = new Date();
|
|
if (notification.type === 'follow') {
|
|
// { pid: userToFollowContent.pid, type: "follow", objectID: req.pid, link: `/users/${req.pid}` }
|
|
let existingNotification = await NOTIFICATION.findOne({ pid: notification.pid, objectID: notification.objectID });
|
|
if (existingNotification) {
|
|
existingNotification.lastUpdated = now;
|
|
existingNotification.read = false;
|
|
return await existingNotification.save();
|
|
}
|
|
const last60min = new Date(now.getTime() - 60 * 60 * 1000);
|
|
existingNotification = await NOTIFICATION.findOne({ pid: notification.pid, type: 'follow', lastUpdated: { $gte: last60min } });
|
|
if (existingNotification) {
|
|
existingNotification.users.push({
|
|
user: notification.objectID,
|
|
timeStamp: now
|
|
});
|
|
existingNotification.lastUpdated = now;
|
|
existingNotification.link = notification.link;
|
|
existingNotification.objectID = notification.objectID;
|
|
existingNotification.read = false;
|
|
return await existingNotification.save();
|
|
} else {
|
|
const newNotification = new NOTIFICATION({
|
|
pid: notification.pid,
|
|
type: notification.type,
|
|
users: [{
|
|
user: notification.objectID,
|
|
timestamp: now
|
|
}],
|
|
link: notification.link,
|
|
objectID: notification.objectID,
|
|
read: false,
|
|
lastUpdated: now
|
|
});
|
|
await newNotification.save();
|
|
}
|
|
} else if (notification.type == 'notice') {
|
|
const newNotification = new NOTIFICATION({
|
|
pid: notification.pid,
|
|
type: notification.type,
|
|
text: notification.text,
|
|
image: notification.image,
|
|
link: notification.link,
|
|
read: false,
|
|
lastUpdated: now
|
|
});
|
|
await newNotification.save();
|
|
}
|
|
/*else if(notification.type === 'yeah') {
|
|
// { pid: userToFollowContent.pid, type: "follow", objectID: req.pid, link: `/users/${req.pid}` }
|
|
let existingNotification = await NOTIFICATION.findOne({ pid: notification.pid, objectID: notification.objectID })
|
|
if(existingNotification) {
|
|
existingNotification.lastUpdated = new Date();
|
|
return await existingNotification.save();
|
|
}
|
|
existingNotification = await NOTIFICATION.findOne({ pid: notification.pid, type: 'yeah' });
|
|
if(existingNotification) {
|
|
existingNotification.users.push({
|
|
user: notification.objectID,
|
|
timeStamp: new Date()
|
|
});
|
|
existingNotification.lastUpdated = new Date();
|
|
existingNotification.link = notification.link;
|
|
existingNotification.objectID = notification.objectID;
|
|
return await existingNotification.save();
|
|
}
|
|
else {
|
|
let newNotification = new NOTIFICATION({
|
|
pid: notification.pid,
|
|
type: notification.type,
|
|
users: [{
|
|
user: notification.objectID,
|
|
timestamp: new Date()
|
|
}],
|
|
link: notification.link,
|
|
objectID: notification.objectID,
|
|
read: false,
|
|
lastUpdated: new Date()
|
|
});
|
|
await newNotification.save();
|
|
}
|
|
}*/
|
|
}
|
|
async function getFriends(pid) {
|
|
try {
|
|
const pids = await friendsClient.getUserFriendPIDs({
|
|
pid: pid
|
|
}, {
|
|
metadata: grpc.Metadata({
|
|
'X-API-Key': friendsKey
|
|
})
|
|
});
|
|
return pids.pids;
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
async function getFriendRequests(pid) {
|
|
try {
|
|
const requests = await friendsClient.getUserFriendRequestsIncoming({
|
|
pid: pid
|
|
}, {
|
|
metadata: grpc.Metadata({
|
|
'X-API-Key': friendsKey
|
|
})
|
|
});
|
|
return requests.friendRequests;
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
async function login(username, password) {
|
|
return await apiClient.login({
|
|
username: username,
|
|
password: password,
|
|
grantType: 'password'
|
|
}, {
|
|
metadata: grpc.Metadata({
|
|
'X-API-Key': apiKey
|
|
})
|
|
});
|
|
}
|
|
async function refreshLogin(refreshToken) {
|
|
return await apiClient.login({
|
|
refreshToken: refreshToken
|
|
}, {
|
|
metadata: grpc.Metadata({
|
|
'X-API-Key': apiKey
|
|
})
|
|
});
|
|
}
|
|
async function getUserDataFromToken(token) {
|
|
return apiClient.getUserData({}, {
|
|
metadata: grpc.Metadata({
|
|
'X-API-Key': apiKey,
|
|
'X-Token': token
|
|
})
|
|
});
|
|
}
|
|
async function getUserDataFromPid(pid) {
|
|
return accountClient.getUserData({
|
|
pid: pid
|
|
}, {
|
|
metadata: grpc.Metadata({
|
|
'X-API-Key': apiKey
|
|
})
|
|
});
|
|
}
|
|
async function getPid(token) {
|
|
const user = await this.getUserDataFromToken(token);
|
|
return user.pid;
|
|
}
|
|
module.exports = {
|
|
decodeParamPack,
|
|
processServiceToken,
|
|
decryptToken,
|
|
unpackToken,
|
|
processPainting,
|
|
nintendoPasswordHash,
|
|
getCommunityHash,
|
|
getUserHash,
|
|
refreshCache,
|
|
setName,
|
|
updateCommunityHash,
|
|
resizeImage,
|
|
getTGAFromPNG,
|
|
createBMPTgaBuffer,
|
|
processLanguage,
|
|
uploadCDNAsset,
|
|
newNotification,
|
|
getFriends,
|
|
getFriendRequests,
|
|
login,
|
|
refreshLogin,
|
|
getUserDataFromToken,
|
|
getUserDataFromPid,
|
|
getPid,
|
|
create_user,
|
|
INVALID_POST_BODY_REGEX
|
|
};
|