juxtaposition-ui/src/util.js
Jemma Poffinbarger 664bf04a01
Some checks are pending
Build and Publish Docker Image / build-publish (push) Waiting to run
fix: updating existing community not updating cache
2025-01-20 19:53:18 -06:00

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
};