Improve User Assigning Profile and Fix Leaderboard Causing recap freezes

This commit is contained in:
ibratabian17
2024-07-28 11:52:07 +08:00
parent ee294f9755
commit 2de99b4e9c
5 changed files with 326 additions and 151 deletions

View File

@@ -2,12 +2,14 @@ var { main } = require('./var')
var { resolvePath } = require('./helper')
var { modules } = require('../settings.json')
var fs = require("fs"); // require https module
var requestIp = require('./lib/ipResolver.js')
function init(app, express) {
const bodyParser = require("body-parser");
app.use(express.json());
app.use(bodyParser.raw());
app.use(requestIp.mw())
app.use((err, req, res, next) => {
// shareLog('ERROR', `${err}`)
res.status(500).send('Internal Server Error');

206
core/lib/ipResolver.js Normal file
View File

@@ -0,0 +1,206 @@
const is = require('./is');
//taken from https://github.com/pbojinov/request-ip
/**
* Parse x-forwarded-for headers.
*
* @param {string} value - The value to be parsed.
* @return {string|null} First known IP address, if any.
*/
function getClientIpFromXForwardedFor(value) {
if (!is.existy(value)) {
return null;
}
if (is.not.string(value)) {
throw new TypeError(`Expected a string, got "${typeof value}"`);
}
// x-forwarded-for may return multiple IP addresses in the format:
// "client IP, proxy 1 IP, proxy 2 IP"
// Therefore, the right-most IP address is the IP address of the most recent proxy
// and the left-most IP address is the IP address of the originating client.
// source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
// Azure Web App's also adds a port for some reason, so we'll only use the first part (the IP)
const forwardedIps = value.split(',').map((e) => {
const ip = e.trim();
if (ip.includes(':')) {
const splitted = ip.split(':');
// make sure we only use this if it's ipv4 (ip:port)
if (splitted.length === 2) {
return splitted[0];
}
}
return ip;
});
// Sometimes IP addresses in this header can be 'unknown' (http://stackoverflow.com/a/11285650).
// Therefore taking the right-most IP address that is not unknown
// A Squid configuration directive can also set the value to "unknown" (http://www.squid-cache.org/Doc/config/forwarded_for/)
for (let i = 0; i < forwardedIps.length; i++) {
if (is.ip(forwardedIps[i])) {
return forwardedIps[i];
}
}
// If no value in the split list is an ip, return null
return null;
}
/**
* Determine client IP address.
*
* @param req
* @returns {string} ip - The IP address if known, defaulting to empty string if unknown.
*/
function getClientIp(req) {
// Server is probably behind a proxy.
if (req.headers) {
// Standard headers used by Amazon EC2, Heroku, and others.
if (is.ip(req.headers['x-client-ip'])) {
return req.headers['x-client-ip'];
}
// Load-balancers (AWS ELB) or proxies.
const xForwardedFor = getClientIpFromXForwardedFor(
req.headers['x-forwarded-for'],
);
if (is.ip(xForwardedFor)) {
return xForwardedFor;
}
// Cloudflare.
// @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
// CF-Connecting-IP - applied to every request to the origin.
if (is.ip(req.headers['cf-connecting-ip'])) {
return req.headers['cf-connecting-ip'];
}
// DigitalOcean.
// @see https://www.digitalocean.com/community/questions/app-platform-client-ip
// DO-Connecting-IP - applied to app platform servers behind a proxy.
if (is.ip(req.headers['do-connecting-ip'])) {
return req.headers['do-connecting-ip'];
}
// Fastly and Firebase hosting header (When forwared to cloud function)
if (is.ip(req.headers['fastly-client-ip'])) {
return req.headers['fastly-client-ip'];
}
// Akamai and Cloudflare: True-Client-IP.
if (is.ip(req.headers['true-client-ip'])) {
return req.headers['true-client-ip'];
}
// Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies.
if (is.ip(req.headers['x-real-ip'])) {
return req.headers['x-real-ip'];
}
// (Rackspace LB and Riverbed's Stingray)
// http://www.rackspace.com/knowledge_center/article/controlling-access-to-linux-cloud-sites-based-on-the-client-ip-address
// https://splash.riverbed.com/docs/DOC-1926
if (is.ip(req.headers['x-cluster-client-ip'])) {
return req.headers['x-cluster-client-ip'];
}
if (is.ip(req.headers['x-forwarded'])) {
return req.headers['x-forwarded'];
}
if (is.ip(req.headers['forwarded-for'])) {
return req.headers['forwarded-for'];
}
if (is.ip(req.headers.forwarded)) {
return req.headers.forwarded;
}
// Google Cloud App Engine
// https://cloud.google.com/appengine/docs/standard/go/reference/request-response-headers
if (is.ip(req.headers['x-appengine-user-ip'])) {
return req.headers['x-appengine-user-ip'];
}
}
// Remote address checks.
// Deprecated
if (is.existy(req.connection)) {
if (is.ip(req.connection.remoteAddress)) {
return req.connection.remoteAddress;
}
if (
is.existy(req.connection.socket) &&
is.ip(req.connection.socket.remoteAddress)
) {
return req.connection.socket.remoteAddress;
}
}
if (is.existy(req.socket) && is.ip(req.socket.remoteAddress)) {
return req.socket.remoteAddress;
}
if (is.existy(req.info) && is.ip(req.info.remoteAddress)) {
return req.info.remoteAddress;
}
// AWS Api Gateway + Lambda
if (
is.existy(req.requestContext) &&
is.existy(req.requestContext.identity) &&
is.ip(req.requestContext.identity.sourceIp)
) {
return req.requestContext.identity.sourceIp;
}
// Cloudflare fallback
// https://blog.cloudflare.com/eliminating-the-last-reasons-to-not-enable-ipv6/#introducingpseudoipv4
if (req.headers) {
if (is.ip(req.headers['Cf-Pseudo-IPv4'])) {
return req.headers['Cf-Pseudo-IPv4'];
}
}
// Fastify https://www.fastify.io/docs/latest/Reference/Request/
if (is.existy(req.raw)) {
return getClientIp(req.raw);
}
return null;
}
/**
* Expose request IP as a middleware.
*
* @param {object} [options] - Configuration.
* @param {string} [options.attributeName] - Name of attribute to augment request object with.
* @return {*}
*/
function mw(options) {
// Defaults.
const configuration = is.not.existy(options) ? {} : options;
// Validation.
if (is.not.object(configuration)) {
throw new TypeError('Options must be an object!');
}
const attributeName = configuration.attributeName || 'clientIp';
return (req, res, next) => {
const ip = getClientIp(req);
Object.defineProperty(req, attributeName, {
get: () => ip,
configurable: true,
});
next();
};
}
module.exports = {
getClientIpFromXForwardedFor,
getClientIp,
mw,
};

72
core/lib/is.js Normal file
View File

@@ -0,0 +1,72 @@
/**
* Inspired by and credit to is_js [https://github.com/arasatasaygin/is.js]
*/
const regexes = {
ipv4: /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/,
ipv6: /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i,
};
/**
* Helper function which reverses the sense of predicate result
* @param {*} func
* @returns
*/
function not(func) {
return function () {
return !func.apply(null, Array.prototype.slice.call(arguments));
};
}
/**
* Replaces is.existy from is_js.
* @param {*} value - The value to test
* @returns {boolean} True if the value is defined, otherwise false
*/
function existy(value) {
return value != null;
}
/**
* Replaces is.ip from is_js.
* @param {*} value - The value to test
* @returns {boolean} True if the value is an IP address, otherwise false
*/
function ip(value) {
return (
(existy(value) && regexes.ipv4.test(value)) || regexes.ipv6.test(value)
);
}
/**
* Replaces is.object from is_js.
* @param {*} value - The value to test
* @returns {boolean} True if the value is an object, otherwise false
*/
function object(value) {
return Object(value) === value;
}
/**
* Replaces is,.string from is_js.
* @param {*} value - The value to test
* @returns True if the value is a string, otherwise false
*/
function string(value) {
return Object.prototype.toString.call(value) === '[object String]';
}
const is = {
existy: existy,
ip: ip,
object: object,
string: string,
not: {
existy: not(existy),
ip: not(ip),
object: not(object),
string: not(string),
},
};
module.exports = is;

View File

@@ -29,15 +29,16 @@ exports.initroute = (app) => {
console.log('[ACC] Is the key correct? are the files corrupted?')
console.log('[ACC] Ignore this message if this first run')
console.log('[ACC] Resetting All User Data...')
console.log(err)
}
// Map over profileIds to retrieve corresponding user profiles or create default profiles
const responseProfiles = profilesid.map(profileId => {
const userProfile = decryptedData[profileId]; // Get user profile based on profileId
if (userProfile) {
return { ...userProfile, ip: req.ip, ticket: ticket }; // Add IP to userProfile but not in the response
return { ...userProfile, ip: req.clientIp, ticket: ticket }; // Add IP to userProfile but not in the response
} else {
const defaultProfile = { ip: req.ip, ticket: ticket }; // Create a default profile with IP address
const defaultProfile = { ip: req.clientIp, ticket: ticket }; // Create a default profile with IP address
decryptedData[profileId] = defaultProfile; // Add default profile to decrypted data
return {}; // Return an empty object (don't include defaultProfile in response)
}
@@ -64,6 +65,7 @@ exports.initroute = (app) => {
console.log('[ACC] Is the key correct? are the files corrupted?')
console.log('[ACC] Ignore this message if this first run')
console.log('[ACC] Resetting All User Data...')
console.log(err)
}
// Find a matching profile based on name or IP address (only one profile)
@@ -75,7 +77,7 @@ exports.initroute = (app) => {
}
const matchedProfileId = Object.keys(decryptedData).find(profileId => {
const userProfile = decryptedData[profileId]; // Get user profile based on profileId
return userProfile.name === content.name || userProfile.ticket === ticket || userProfile.ip === req.ip; // Check for name or IP match
return userProfile.name === content.name || userProfile.ticket === ticket || userProfile.ip === req.clientIp; // Check for name or IP match
});
if (matchedProfileId) {

View File

@@ -9,6 +9,11 @@ const core = {
generateCarousel: require('../carousel/carousel').generateCarousel, generateSweatCarousel: require('../carousel/carousel').generateSweatCarousel, generateCoopCarousel: require('../carousel/carousel').generateCoopCarousel, updateMostPlayed: require('../carousel/carousel').updateMostPlayed
}
const DOTW_PATH = path.join(core.getSavefilePath(), 'leaderboard/dotw/');
const { getSavefilePath } = require('../helper');
const { encrypt, decrypt } = require('../lib/encryptor')
const secretKey = require('../../database/encryption.json').encrpytion.userEncrypt;
decryptedData = {};
function generateToolNickname() {
const prefixes = ["Wordkeeper", "Special", "Krakenbite", "DinosaurFan", "Definehub", "Termtracker", "Lexiconet", "Vocabvault", "Lingolink", "Glossarygenius", "Thesaurustech", "Synonymster", "Definitionary", "Jargonjot", "Idiomizer", "Phraseforge", "Meaningmaker", "Languageledger", "Etymologyengine", "Grammarguard", "Syntaxsense", "Semanticsearch", "Orthographix", "Phraseology", "Vernacularvault", "Dictionet", "Slangscroll", "Lingualist", "Grammargrid", "Lingoledge", "Termtoolbox", "Wordware", "Lexigizmo", "Synosearch", "Thesaurustech", "Phrasefinder", "Vocabvortex", "Meaningmatrix", "Languageledger", "Etymologist", "Grammargate", "Syntaxsphere", "Semanticsearch", "Orthographix", "Phraseplay", "Vernacularvault", "Dictionator", "Slangstack", "Lingolink", "Grammarguide", "Lingopedia", "Termtracker", "Wordwizard", "Lexilist", "Synomate", "Thesaurustool", "Definitizer", "Jargonjunction", "Idiomgenius", "Phrasemaker", "Meaningmate", "Duolingo", "Languagelink", "Etymoengine", "Grammarguru", "Syntaxsage", "Semanticsuite", "Orthography", "Phrasefinder", "Vocabverse", "Lexipedia", "Synoscribe", "Thesaurusware", "Definitionary", "Jargonscribe", "Idiomster", "Phrasetech", "Meaningmax", "Flop", "Slayguy", "Languagelex", "Etymoedge", "Grammargenie", "Syntaxsync", "Semanticsearch", "Orthography", "Phraseforge", "Vernacularex", "Dictionmaster", "Slangster", "Lingoware", "Grammargraph", "Lingomate", "Termmate", "Wordwork", "Lexixpert", "Synostar", "Thesaurusmax", "OculusVision", "FlowerPower", "RustySilver", "Underfire", "Shakeawake", "Truthhand", "Kittywake", "Definize", "Jargonize", "Idiomify", "Phrasemaster", "Meaningmark", "Lingualine", "Etymogenius", "Grammarguard", "Syntaxsmart", "Semanticsearch", "Orthography", "Phrasedex", "Vocabmax", "Lexilock", "Synomind", "Thesaurusmart", "Definify", "Jargonmatrix", "Idiomnet", "Phraseplay", "Meaningmate", "Lingolink", "Etymoexpert", "Grammargetter", "Syntaxsage", "Semanticsearch", "Orthography", "Phrasepad", "Vernacularvibe", "Dictiondom", "Slangster", "Lingolytics", "Grammargenie", "Lingotutor", "Termtracker", "Wordwarp", "Lexisync", "Synomind", "Thesaurusmate", "Definizer", "Jargonify", "Idiomster", "Phraselab", "Meaningmark", "Languageleaf", "Etymoedge", "Grammargrid", "Syntaxsync", "Semanticsuite", "Orthographix", "Phraseforge", "Vernacularvibe", "Dictiondom", "Slangster", "Lingolytics", "Grammargenie", "Lingotutor", "Termtracker", "Wordwarp", "Lexisync", "Synomind", "Thesaurusmate", "Definizer", "Jargonify", "Idiomster", "Phraselab", "Meaningmark", "Languageleaf", "Etymoedge", "Grammargrid", "Syntaxsync", "Semanticsuite", "Orthographix"];
@@ -27,38 +32,37 @@ function generateToolNickname() {
}
const getProfileData = async (req) => {
function getProfileData(ticket, content, clientIp) {
const dataFilePath = path.join(getSavefilePath(), '/account/profiles/user.json');
try {
const ticket = req.header("Authorization");
const sku = req.header('X-SkuId');
const prodwsurl = "https://prod.just-dance.com/";
const response = await axios({
method: req.method,
url: prodwsurl + req.url,
headers: {
"X-SkuId": sku,
"Authorization": ticket,
"Content-Type": "application/json"
},
data: req.body
});
return response.data;
} catch (error) {
if (error.response) {
throw new Error(`HTTP status ${error.response.status}`);
} else if (error.request) {
throw new Error('Network error');
} else {
throw new Error(error.message);
if (Object.keys(decryptedData).length === 0) {
const encryptedData = fs.readFileSync(dataFilePath, 'utf8');
decryptedData = JSON.parse(decrypt(encryptedData, secretKey));
}
} catch (err) {
decryptedData = {};
console.log('[ACC] Unable to read user.json');
console.log('[ACC] Is the key correct? are the files corrupted?');
console.log('[ACC] Ignore this message if this first run');
console.log('[ACC] Resetting All User Data...');
console.log(err);
}
};
const matchedProfileId = Object.keys(decryptedData).find(profileId => {
const userProfile = decryptedData[profileId];
return userProfile.ticket === ticket || userProfile.ip === clientIp;
});
if (matchedProfileId) {
return decryptedData[matchedProfileId];
} else {
return false;
}
}
const getGameVersion = (req) => {
const sku = req.header('X-SkuId');
return sku.substring(0, 6);
const sku = req.header('X-SkuId') || "jd2019-pc-ww";
return sku.substring(0, 6) || "jd2019";
};
const initroute = (app) => {
@@ -102,62 +106,6 @@ const initroute = (app) => {
try {
leaderboardData.entries.push(
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
@@ -189,62 +137,6 @@ const initroute = (app) => {
try {
leaderboardData.entries.push(
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
"score": Math.floor(Math.random() * 1333) + 12000,
"name": generateToolNickname(),
"avatar": Math.floor(Math.random() * 100),
"country": Math.floor(Math.random() * 20),
"platformId": "e3",
"alias": 0,
"aliasGender": 0,
"jdPoints": 0,
"portraitBorder": 0,
"mapName": mapName
},
{
"__class": "LeaderboardEntry_Online",
"profileId": "00000000-0000-0000-0000-000000000000",
@@ -267,11 +159,12 @@ const initroute = (app) => {
});
app.post("/profile/v2/map-ended", async (req, res) => {
const codename = req.body;
const ticket = req.header("Authorization");
const clientIp = req.ip;
try {
for (let song of codename) {
core.updateMostPlayed(song);
const mapList = req.body;
for (let song of mapList) {
core.updateMostPlayed(song.mapName);
const dotwFilePath = path.join(DOTW_PATH, `${song.mapName}.json`);
if (fs.existsSync(dotwFilePath)) {
@@ -281,9 +174,9 @@ const initroute = (app) => {
return res.send('1');
}
} else {
const profiljson1 = await getProfileData(req);
const profiljson1 = await getProfileData(ticket, song, clientIp);
if (!profiljson1) {
return res.status(500).send('Error fetching profile data');
return res.send('1')
}
const jsontodancerweek = {
@@ -304,12 +197,12 @@ const initroute = (app) => {
fs.writeFileSync(dotwFilePath, JSON.stringify(jsontodancerweek, null, 2));
console.log(`DOTW file for ${song.mapName} created!`);
res.send(profiljson1);
res.send('');
}
}
} catch (error) {
console.error(error);
res.status(500).send('Internal Server Error');
console.log(error)
res.status(200).send(''); //keep send
}
});