mirror of
https://github.com/ibratabian17/OpenParty.git
synced 2026-01-15 14:22:54 -03:00
550 lines
22 KiB
JavaScript
550 lines
22 KiB
JavaScript
/**
|
|
* Account Route Handler for OpenParty
|
|
* Handles user account-related routes
|
|
*/
|
|
const axios = require('axios');
|
|
const RouteHandler = require('./RouteHandler'); // Assuming RouteHandler is in the same directory
|
|
const { updateMostPlayed } = require('../../carousel/carousel'); // Adjust path as needed
|
|
const AccountService = require('../../services/AccountService'); // Import the AccountService
|
|
const { getDb } = require('../../database/sqlite');
|
|
const Logger = require('../../utils/logger');
|
|
|
|
class AccountRouteHandler extends RouteHandler {
|
|
/**
|
|
* Create a new account route handler
|
|
*/
|
|
constructor() {
|
|
super('AccountRouteHandler');
|
|
this.logger = new Logger('AccountRouteHandler');
|
|
|
|
// Bind handler methods to maintain 'this' context
|
|
this.handlePostProfiles = this.handlePostProfiles.bind(this);
|
|
this.handleGetProfiles = this.handleGetProfiles.bind(this);
|
|
this.handleMapEnded = this.handleMapEnded.bind(this);
|
|
this.handleDeleteFavoriteMap = this.handleDeleteFavoriteMap.bind(this);
|
|
this.handleGetProfileSessions = this.handleGetProfileSessions.bind(this);
|
|
this.handleFilterPlayers = this.handleFilterPlayers.bind(this);
|
|
|
|
// Initialize properties
|
|
this.ubiwsurl = "https://public-ubiservices.ubi.com";
|
|
this.prodwsurl = "https://prod.just-dance.com";
|
|
}
|
|
|
|
/**
|
|
* Initialize the routes
|
|
* @param {Express} app - The Express application instance
|
|
*/
|
|
initroute(app) {
|
|
this.logger.info(`Initializing routes...`);
|
|
|
|
// Register routes based on the 'old code'
|
|
this.registerPost(app, "/profile/v2/profiles", this.handlePostProfiles);
|
|
this.registerGet(app, "/profile/v2/profiles", this.handleGetProfiles);
|
|
this.registerPost(app, "/profile/v2/map-ended", this.handleMapEnded);
|
|
this.registerDelete(app, "/profile/v2/favorites/maps/:MapName", this.handleDeleteFavoriteMap);
|
|
this.registerGet(app, "/v3/profiles/sessions", this.handleGetProfileSessions);
|
|
this.registerPost(app, "/profile/v2/filter-players", this.handleFilterPlayers);
|
|
|
|
this.logger.info(`Routes initialized`);
|
|
}
|
|
|
|
/**
|
|
* Helper: Get the current week number
|
|
* @returns {number} The current week number
|
|
* @private
|
|
*/
|
|
getWeekNumber() {
|
|
const now = new Date();
|
|
const startOfWeek = new Date(now.getFullYear(), 0, 1);
|
|
const daysSinceStartOfWeek = Math.floor((now - startOfWeek) / (24 * 60 * 60 * 1000));
|
|
return Math.ceil((daysSinceStartOfWeek + 1) / 7);
|
|
}
|
|
|
|
/**
|
|
* Helper: Get game version from SkuId
|
|
* @param {Request} req - The request object
|
|
* @returns {string} The game version
|
|
* @private
|
|
*/
|
|
getGameVersion(req) {
|
|
const sku = req.header('X-SkuId') || "jd2019-pc-ww";
|
|
return sku.substring(0, 6) || "jd2019";
|
|
}
|
|
|
|
/**
|
|
* Helper: Find user by ticket
|
|
* @param {string} ticket - The user's ticket
|
|
* @returns {string|null} Profile ID if found, null otherwise
|
|
* @private
|
|
*/
|
|
findUserFromTicket(ticket) {
|
|
return AccountService.findUserFromTicket(ticket);
|
|
}
|
|
|
|
/**
|
|
* Helper: Find user by nickname
|
|
* @param {string} nickname - The user's nickname
|
|
* @returns {Object|undefined} User profile if found, undefined otherwise
|
|
* @private
|
|
*/
|
|
findUserFromNickname(nickname) {
|
|
return AccountService.findUserFromNickname(nickname);
|
|
}
|
|
|
|
/**
|
|
* Helper: Add a new user profile.
|
|
* @param {string} profileId - The unique ID for the user profile.
|
|
* @param {Object} userProfile - The user profile data to add.
|
|
* @private
|
|
*/
|
|
addUser(profileId, userProfile) {
|
|
this.logger.info(`Added User With UUID: `, profileId);
|
|
return AccountService.updateUser(profileId, userProfile);
|
|
}
|
|
|
|
/**
|
|
* Helper: Update or override existing user data.
|
|
* @param {string} profileId - The ID of the profile to update.
|
|
* @param {Object} userProfile - The data to merge into the existing profile.
|
|
* @private
|
|
*/
|
|
updateUser(profileId, userProfile) {
|
|
return AccountService.updateUser(profileId, userProfile);
|
|
}
|
|
|
|
/**
|
|
* Helper: Retrieve user data by profile ID.
|
|
* @param {string} profileId - The ID of the profile to retrieve.
|
|
* @returns {Object|null} The user profile data, or null if not found.
|
|
* @private
|
|
*/
|
|
getUserData(profileId) {
|
|
return AccountService.getUserData(profileId);
|
|
}
|
|
|
|
/**
|
|
* Helper: Read leaderboard data from SQLite.
|
|
* @param {boolean} isDotw - True if reading Dancer of the Week leaderboard.
|
|
* @returns {Promise<Object>} A promise that resolves to the leaderboard data.
|
|
* @private
|
|
*/
|
|
async readLeaderboard(isDotw = false) {
|
|
const db = getDb();
|
|
return new Promise((resolve, reject) => {
|
|
const tableName = isDotw ? 'dotw' : 'leaderboard';
|
|
db.all(`SELECT * FROM ${tableName}`, [], (err, rows) => {
|
|
if (err) {
|
|
this.logger.error(`Error reading ${tableName} from DB:`, err.message);
|
|
reject(err);
|
|
} else {
|
|
const data = {};
|
|
// For leaderboard, group by mapName
|
|
if (!isDotw) {
|
|
rows.forEach(row => {
|
|
if (!data[row.mapName]) {
|
|
data[row.mapName] = [];
|
|
}
|
|
data[row.mapName].push(row);
|
|
});
|
|
} else {
|
|
// For DOTW, assume a single entry per week or handle as needed
|
|
// For now, just return the rows as an array, or the first row if only one is expected
|
|
if (rows.length > 0) {
|
|
data.week = this.getWeekNumber(); // Assuming 'week' property is used to check current week
|
|
data.entries = rows;
|
|
}
|
|
}
|
|
resolve(data);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper: Generate a leaderboard object from user data.
|
|
* This method is now primarily for transforming user data into a format suitable for leaderboard entries,
|
|
* not for directly interacting with the database.
|
|
* @param {Object} userDataList - All decrypted user profiles.
|
|
* @param {Request} req - The request object (for getGameVersion).
|
|
* @returns {Array} An array of leaderboard entries.
|
|
* @private
|
|
*/
|
|
generateLeaderboard(userDataList, req) {
|
|
const leaderboardEntries = [];
|
|
Object.entries(userDataList).forEach(([profileId, userProfile]) => {
|
|
if (userProfile.scores) {
|
|
Object.entries(userProfile.scores).forEach(([mapName, scoreData]) => {
|
|
leaderboardEntries.push({
|
|
mapName: mapName,
|
|
profileId: profileId,
|
|
username: userProfile.name, // Assuming 'name' is the username
|
|
score: scoreData.highest,
|
|
timestamp: scoreData.lastPlayed, // Using lastPlayed as timestamp
|
|
gameVersion: this.getGameVersion(req),
|
|
rank: userProfile.rank,
|
|
name: userProfile.name,
|
|
avatar: userProfile.avatar,
|
|
country: userProfile.country,
|
|
platformId: userProfile.platformId,
|
|
alias: userProfile.alias,
|
|
aliasGender: userProfile.aliasGender,
|
|
jdPoints: userProfile.jdPoints,
|
|
portraitBorder: userProfile.portraitBorder
|
|
});
|
|
});
|
|
}
|
|
});
|
|
this.logger.info('Leaderboard List Generated for processing.');
|
|
return leaderboardEntries;
|
|
}
|
|
|
|
/**
|
|
* Helper: Save leaderboard data to SQLite.
|
|
* This method will now handle inserting/updating multiple leaderboard entries.
|
|
* @param {Array} leaderboardEntries - An array of leaderboard entries to save.
|
|
* @param {boolean} isDotw - True if saving Dancer of the Week leaderboard.
|
|
* @returns {Promise<void>} A promise that resolves when data is saved.
|
|
* @private
|
|
*/
|
|
async saveLeaderboard(leaderboardEntries, isDotw = false) {
|
|
const db = getDb();
|
|
const tableName = isDotw ? 'dotw' : 'leaderboard';
|
|
|
|
return new Promise((resolve, reject) => {
|
|
db.serialize(() => {
|
|
db.run('BEGIN TRANSACTION;');
|
|
|
|
let stmt;
|
|
if (isDotw) {
|
|
stmt = db.prepare(`INSERT OR REPLACE INTO ${tableName} (mapName, profileId, username, score, timestamp, gameVersion, rank, name, avatar, country, platformId, alias, aliasGender, jdPoints, portraitBorder, weekNumber) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
} else {
|
|
stmt = db.prepare(`INSERT OR REPLACE INTO ${tableName} (mapName, profileId, username, score, timestamp) VALUES (?, ?, ?, ?, ?)`);
|
|
}
|
|
|
|
const promises = leaderboardEntries.map(entry => {
|
|
return new Promise((resolveRun, rejectRun) => {
|
|
if (isDotw) {
|
|
stmt.run(
|
|
entry.mapName,
|
|
entry.profileId,
|
|
entry.username,
|
|
entry.score,
|
|
entry.timestamp,
|
|
entry.gameVersion,
|
|
entry.rank,
|
|
entry.name,
|
|
entry.avatar,
|
|
entry.country,
|
|
entry.platformId,
|
|
entry.alias,
|
|
entry.aliasGender,
|
|
entry.jdPoints,
|
|
entry.portraitBorder,
|
|
this.getWeekNumber(),
|
|
(err) => {
|
|
if (err) rejectRun(err);
|
|
else resolveRun();
|
|
}
|
|
);
|
|
} else {
|
|
stmt.run(
|
|
entry.mapName,
|
|
entry.profileId,
|
|
entry.username,
|
|
entry.score,
|
|
entry.timestamp,
|
|
(err) => {
|
|
if (err) rejectRun(err);
|
|
else resolveRun();
|
|
}
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
Promise.all(promises)
|
|
.then(() => {
|
|
stmt.finalize((err) => {
|
|
if (err) {
|
|
this.logger.error(`Error finalizing statement for ${tableName}:`, err.message);
|
|
db.run('ROLLBACK;');
|
|
reject(err);
|
|
} else {
|
|
db.run('COMMIT;', (commitErr) => {
|
|
if (commitErr) {
|
|
this.logger.error(`Error committing transaction for ${tableName}:`, commitErr.message);
|
|
reject(commitErr);
|
|
} else {
|
|
this.logger.info(`Saved ${tableName} data to DB.`);
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
})
|
|
.catch(error => {
|
|
this.logger.error(`Error during batch insert for ${tableName}:`, error.message);
|
|
db.run('ROLLBACK;');
|
|
reject(error);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle POST to /profile/v2/profiles
|
|
* @param {Request} req - The request object
|
|
* @param {Response} res - The response object
|
|
*/
|
|
async handlePostProfiles(req, res) {
|
|
const profileId = req.body?.profileId || req.body?.userId;
|
|
|
|
if (!profileId) {
|
|
return res.status(400).send({ message: "Missing profileId or userId" });
|
|
}
|
|
|
|
const userData = await AccountService.getUserData(profileId); // Await getUserData
|
|
|
|
if (userData) {
|
|
this.logger.info(`Updating existing profile ${profileId}`);
|
|
|
|
// Update only the fields present in the request body, preserving other fields
|
|
const updatedProfile = await AccountService.updateUser(profileId, req.body); // Await updateUser
|
|
|
|
return res.send({
|
|
__class: "UserProfile",
|
|
...updatedProfile.toJSON()
|
|
});
|
|
} else {
|
|
this.logger.info(`Creating new profile ${profileId}`);
|
|
|
|
// Create a new profile with default values and request body values
|
|
const newProfile = await AccountService.updateUser(profileId, { // Await updateUser
|
|
...req.body,
|
|
name: req.body.name || "Player",
|
|
alias: req.body.alias || "default",
|
|
aliasGender: req.body.aliasGender || 2,
|
|
scores: req.body.scores || {},
|
|
songsPlayed: req.body.songsPlayed || [],
|
|
avatar: req.body.avatar || "UI/menu_avatar/base/light.png",
|
|
country: req.body.country || "US",
|
|
createdAt: new Date().toISOString()
|
|
});
|
|
|
|
return res.send({
|
|
__class: "UserProfile",
|
|
...newProfile.toJSON()
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle GET to /profile/v2/profiles
|
|
* @param {Request} req - The request object
|
|
* @param {Response} res - The response object
|
|
*/
|
|
async handleGetProfiles(req, res) {
|
|
// Get the profileId from query parameters or authorization header
|
|
const profileId = req.query.profileId || await this.findUserFromTicket(req.header('Authorization')); // Await findUserFromTicket
|
|
|
|
if (!profileId) {
|
|
return res.status(400).send({ message: "Missing profileId" });
|
|
}
|
|
|
|
const userProfile = await AccountService.getUserData(profileId); // Await getUserData
|
|
|
|
if (!userProfile) {
|
|
this.logger.info(`Profile ${profileId} not found`);
|
|
return res.status(404).send({ message: "Profile not found" });
|
|
}
|
|
|
|
// If query contains specific profile requests by IDs
|
|
if (req.query.requestedProfiles) {
|
|
try {
|
|
const requestedProfiles = JSON.parse(req.query.requestedProfiles);
|
|
const profiles = {};
|
|
|
|
// Get each requested profile
|
|
for (const reqProfileId of requestedProfiles) {
|
|
const profile = await AccountService.getUserData(reqProfileId); // Await getUserData
|
|
if (profile) {
|
|
profiles[reqProfileId] = {
|
|
__class: "UserProfile",
|
|
...profile.toJSON()
|
|
};
|
|
}
|
|
}
|
|
|
|
return res.send({
|
|
__class: "ProfilesContainer",
|
|
profiles: profiles
|
|
});
|
|
} catch (error) {
|
|
this.logger.error('Error parsing requestedProfiles:', error);
|
|
return res.status(400).send({ message: "Invalid requestedProfiles format" });
|
|
}
|
|
}
|
|
|
|
// Return single profile
|
|
return res.send({
|
|
__class: "UserProfile",
|
|
...userProfile.toJSON()
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle POST to /profile/v2/map-ended
|
|
* @param {Request} req - The request object
|
|
* @param {Response} res - The response object
|
|
*/
|
|
async handleMapEnded(req, res) {
|
|
const { mapName, score } = req.body;
|
|
const profileId = req.query.profileId || await this.findUserFromTicket(req.header('Authorization')); // Await findUserFromTicket
|
|
|
|
if (!profileId) {
|
|
return res.status(400).send({ message: "Missing profileId" });
|
|
}
|
|
|
|
if (!mapName || !score) {
|
|
return res.status(400).send({ message: "Missing mapName or score" });
|
|
}
|
|
|
|
const userProfile = await AccountService.getUserData(profileId); // Await getUserData
|
|
|
|
if (!userProfile) {
|
|
this.logger.info(`Profile ${profileId} not found`);
|
|
return res.status(404).send({ message: "Profile not found" });
|
|
}
|
|
|
|
// Update most played maps
|
|
updateMostPlayed(mapName);
|
|
|
|
// Update user's score for this map
|
|
const currentScore = userProfile.scores?.[mapName]?.highest || 0;
|
|
const newHighest = Math.max(currentScore, score);
|
|
|
|
await AccountService.updateUserScore(profileId, mapName, { // Await updateUserScore
|
|
highest: newHighest,
|
|
lastPlayed: new Date().toISOString(),
|
|
history: [
|
|
...(userProfile.scores?.[mapName]?.history || []),
|
|
{
|
|
score,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
].slice(-10) // Keep only last 10 scores
|
|
});
|
|
|
|
// Add to songsPlayed array if not already present
|
|
if (!userProfile.songsPlayed?.includes(mapName)) {
|
|
await AccountService.updateUser(profileId, { // Await updateUser
|
|
songsPlayed: [...(userProfile.songsPlayed || []), mapName]
|
|
});
|
|
}
|
|
|
|
// Update leaderboards
|
|
const allAccounts = await AccountService.getAllAccounts();
|
|
const leaderboardEntries = this.generateLeaderboard(allAccounts, req);
|
|
await this.saveLeaderboard(leaderboardEntries);
|
|
|
|
// Update DOTW (Dancer of the Week) leaderboard if it's a new week
|
|
const currentWeek = this.getWeekNumber();
|
|
const dotwData = await this.readLeaderboard(true);
|
|
|
|
if (!dotwData.week || dotwData.week !== currentWeek) {
|
|
this.logger.info(`New week detected: ${currentWeek}, resetting DOTW`);
|
|
await this.saveLeaderboard([], true);
|
|
}
|
|
|
|
return res.send({
|
|
__class: "MapEndResult",
|
|
isNewPersonalBest: score > currentScore,
|
|
personalBestScore: newHighest
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle DELETE to /profile/v2/favorites/maps/:MapName
|
|
* @param {Request} req - The request object
|
|
* @param {Response} res - The response object
|
|
*/
|
|
async handleDeleteFavoriteMap(req, res) {
|
|
const mapName = req.params.MapName;
|
|
const profileId = req.query.profileId || this.findUserFromTicket(req.header('Authorization'));
|
|
|
|
if (!profileId) {
|
|
return res.status(400).send({ message: "Missing profileId" });
|
|
}
|
|
|
|
if (!mapName) {
|
|
return res.status(400).send({ message: "Missing mapName" });
|
|
}
|
|
|
|
AccountService.removeMapFromFavorites(profileId, mapName);
|
|
|
|
return res.status(204).send();
|
|
}
|
|
|
|
/**
|
|
* Handle GET to /v3/profiles/sessions
|
|
* @param {Request} req - The request object
|
|
* @param {Response} res - The response object
|
|
*/
|
|
async handleGetProfileSessions(req, res) {
|
|
const profileId = req.query.profileId || this.findUserFromTicket(req.header('Authorization'));
|
|
|
|
if (!profileId) {
|
|
return res.status(400).send({ message: "Missing profileId" });
|
|
}
|
|
|
|
const userProfile = AccountService.getUserData(profileId);
|
|
|
|
if (!userProfile) {
|
|
this.logger.info(`Profile ${profileId} not found`);
|
|
return res.status(404).send({ message: "Profile not found" });
|
|
}
|
|
|
|
return res.send({
|
|
sessionId: profileId,
|
|
trackingEnabled: false
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle POST to /profile/v2/filter-players
|
|
* @param {Request} req - The request object
|
|
* @param {Response} res - The response object
|
|
*/
|
|
handleFilterPlayers(req, res) {
|
|
const { count, countryFilter, platform } = req.body;
|
|
const allAccounts = AccountService.getAllAccounts();
|
|
|
|
// Filter accounts based on criteria
|
|
const filteredAccounts = Object.values(allAccounts)
|
|
.filter(account => {
|
|
if (countryFilter && account.country !== countryFilter) {
|
|
return false;
|
|
}
|
|
if (platform && account.platformId !== platform) {
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.slice(0, count || 10) // Limit to requested count or default 10
|
|
.map(account => ({
|
|
profileId: account.profileId,
|
|
name: account.name,
|
|
country: account.country,
|
|
platformId: account.platformId,
|
|
avatar: account.avatar
|
|
}));
|
|
|
|
return res.send({
|
|
__class: "FilterPlayersResponse",
|
|
players: filteredAccounts
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = new AccountRouteHandler();
|