mirror of
https://github.com/ibratabian17/OpenParty.git
synced 2026-01-15 06:12:54 -03:00
refactor: Major refactoring of entire application for enhanced performance and clarity
This commit is contained in:
216
README.md
216
README.md
@@ -1,119 +1,199 @@
|
||||
|
||||
# OpenParty
|
||||
|
||||
OpenParty is a community-driven project developed by PartyTeam and LunarTeam as an alternative server solution for Just Dance Unlimited enthusiasts. This server allows you to enjoy the Just Dance Unlimited experience independently of official servers, which are no longer available.
|
||||
OpenParty is a community-driven project developed by PartyTeam and LunarTeam, which offers an alternative server solution for enthusiasts of Just Dance Unlimited. This server enables an independent Just Dance Unlimited experience, as official servers are no longer operational.
|
||||
|
||||
## About
|
||||
|
||||
This initiative aims to fill the void left by the discontinued official service, offering a reliable and enhanced alternative. OpenParty is crafted entirely from scratch, ensuring transparency and user trust. It strives to provide a seamless experience comparable to or even better than existing solutions like JDParty.
|
||||
This initiative addresses the void left by the discontinuation of the official service, providing a robust and enhanced alternative. OpenParty has been developed entirely from scratch, ensuring comprehensive transparency and fostering user confidence. The project aims to deliver a seamless user experience comparable to, or potentially surpassing, existing solutions such as JDParty.
|
||||
|
||||
## Features
|
||||
|
||||
- **Independence**: Enjoy Just Dance Unlimited without reliance on official servers (not 100% yet).
|
||||
- **Community-Driven**: Developed with contributions from the community, ensuring continuous improvement.
|
||||
- **Multi-platform**: This project supports various platforms such as PC, Switch, PS4, Xbox One (duh), Wii U
|
||||
* **Independence**: The system provides Just Dance Unlimited functionality without reliance on official servers; its current level of independence is approximately 90%.
|
||||
|
||||
* **Community-Driven Development**: The project's evolution is propelled by contributions from the community, thereby ensuring continuous enhancement and expansion of its capabilities.
|
||||
|
||||
* **Multi-platform Compatibility**: OpenParty supports a diverse range of platforms, including personal computers (PC), Nintendo Switch, PlayStation 4, Xbox One, and Wii U.
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js
|
||||
- Git
|
||||
- Server (duh, we can't pay it)
|
||||
- Just Dance Certificate (Optional) (For PS4, JD17-22 for NX, for JD16-18?? Wii U)
|
||||
* Node.js
|
||||
|
||||
### Setup
|
||||
* Git
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/ibratabian17/openparty.git
|
||||
* Server Hosting Environment
|
||||
|
||||
* Just Dance Certificate (Optional): This certificate is required for specific platforms, such as PlayStation 4, Just Dance 2017-2022 for Nintendo Switch (NX), and Just Dance 2016-2018 for Wii U.
|
||||
|
||||
### Setup Procedure
|
||||
|
||||
1. **Repository Cloning**:
|
||||
|
||||
```
|
||||
git clone [https://github.com/ibratabian17/openparty.git](https://github.com/ibratabian17/openparty.git)
|
||||
cd openparty
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
2. **Dependency Installation**:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the server:
|
||||
```bash
|
||||
3. **Server Initialization**:
|
||||
|
||||
```
|
||||
pm2 start server.js --name openparty-server --no-daemon
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
The directory structure of OpenParty is organized to facilitate ease of access and modification of the server's functionalities and data. Here's a detailed breakdown:
|
||||
The OpenParty directory structure is organized to optimize access and facilitate modifications to server functionalities and data. A detailed overview is provided below:
|
||||
|
||||
### `database/Platforms/openparty-all/songdbs.json`
|
||||
- **Purpose**: Contains the song database.
|
||||
- **Description**: This JSON file holds the list of songs available on the server. Users can edit this file to customize the song list without changing the server code. If a song list is found in the `SaveData`, it will override this file, allowing dynamic changes without needing to restart the server.
|
||||
* ### `database/Platforms/openparty-all/songdbs.json`
|
||||
|
||||
### `database/nohud/chunk.json`
|
||||
- **Purpose**: Contains no-HUD Videos.
|
||||
- **Description**: This JSON file manages the HUD-less video of the game. Similar to the song database, if a configuration file is in `SaveData`, it will take precedence over this file, making it easy to quickly adjust settings.
|
||||
* **Purpose**: This file contains the primary song database.
|
||||
|
||||
### `database/Platforms/jd2017-{Platform}/sku-packages.json`
|
||||
- **Purpose**: Contains SKU packages.
|
||||
- **Description**: This directory includes platform-specific SKU packages, which are bundles of songs and content specific to a version or platform of Just Dance. These can be tailored to suit different platforms such as PC, Xbox, or PlayStation. The platform-specific files in the `SaveData` directory will override these if available, providing an easy way to customize content per platform without altering the base server files.
|
||||
* **Description**: This JSON file enumerates all songs available on the server. Users can modify this file to customize the song list without altering to the core server code. Should a song list be present within the `SaveData` directory, it will supersede the contents of this file, thereby enabling dynamic updates without requiring a server restart.
|
||||
|
||||
### `database/encryption.json`
|
||||
- **Purpose**: Contains encryption settings.
|
||||
- **Description**: This JSON file holds the encryption settings for the server, including keys for generating HMAC and user data encryption.
|
||||
* ### `database/nohud/chunk.json`
|
||||
|
||||
### SaveData Directory
|
||||
- **Purpose**: Save server data, you can also change default settings and data.
|
||||
- **Description**: The `SaveData` directory is used to save user-specific or modified versions of data files and settings and also stores data owned by the server. If there is a Platforms folder and a nohud folder, this will replace the data from `database` without touching the core server code. This helps you update code without breaking Git.
|
||||
* **Purpose**: This file contains configurations for no-HUD video content.
|
||||
|
||||
## Configuration of settings.json
|
||||
* **Description**: This JSON file manages the configuration settings for game videos that do not display the Heads-Up Display (HUD). Similar to the song database, if a configuration file is present in `SaveData`, it will take precedence, enabling rapid adjustments.
|
||||
|
||||
You can configure the server with the `settings.json` file as follows:
|
||||
* ### `database/Platforms/jd2017-{Platform}/sku-packages.json`
|
||||
|
||||
### SaveData
|
||||
Defines the paths where the server will save data for different operating systems.
|
||||
- `"windows": "{Home}\\AppData\\Roaming\\openparty\\"`: Specifies the directory for Windows.
|
||||
- `"linux": "{Home}/.openparty/"`: Specifies the directory for Linux and other non-Windows systems.
|
||||
* **Purpose**: This directory contains Stock Keeping Unit (SKU) packages.
|
||||
|
||||
### Port
|
||||
- `"port": 80`: Sets the port for the server to listen on. Default is 80.
|
||||
* **Description**: This directory encompasses platform-specific SKU packages, which are collections of songs and content curated for a particular version or platform of Just Dance. These packages can be tailored to suit various platforms, including PC, Xbox, or PlayStation. Platform-specific files located within the `SaveData` directory will override these default packages if available, providing a flexible methodology for content customization per platform without modifying the foundational server files.
|
||||
|
||||
### ForcePort
|
||||
- `"forcePort": false`: Forces the server to use port 80 even if the OS assigns a different port. If set to true, it will always use port 80.
|
||||
* ### `core/scripts/run.js`
|
||||
|
||||
### Public Access
|
||||
- `"isPublic": true`: If set to true, the server is accessible publicly (0.0.0.0). If false, it's only accessible locally (127.0.0.1).
|
||||
* **Purpose**: This script manages the primary server process, including logging operations and automatic restarts.
|
||||
|
||||
### Enable SSL
|
||||
- `"enableSSL": true`: Enables SSL (HTTPS) if the server supports it. Set to true to enable.
|
||||
* **Description**: This file now integrates the `ProcessManager` class, which is responsible for initiating the `server.js` process, capturing its standard output and error streams, logging pertinent information to `database/tmp/logs.txt`, and automatically restarting the server if it terminates with a specific exit code (e.g., `42`). Furthermore, it incorporates mechanisms for graceful shutdown in response to `SIGINT` and `SIGTERM` signals. This architectural refinement significantly enhances the server's stability and facilitates improved observability.
|
||||
|
||||
### Domain
|
||||
- `"domain": "jdp.justdancenext.xyz"`: Specifies the domain name for the server.
|
||||
* ### `database/encryption.json`
|
||||
|
||||
### Server Status
|
||||
- `"serverstatus": {}`: Indicates whether the server is in maintenance mode and specifies the server channel.
|
||||
- `"isMaintenance": false`: Server is not in maintenance mode.
|
||||
- `"channel": "prod"`: Specifies the server channel, here set to "prod" (production).
|
||||
* **Purpose**: This file contains encryption settings.
|
||||
|
||||
### Modules
|
||||
- `"modules": []`: Defines server modules, their descriptions, file paths, and execution behavior.
|
||||
- `"name": ""`: The name of your modules
|
||||
- `"description": ""`: The description of your modules
|
||||
- `"path": ""`: The path where the server calls the module
|
||||
- `"execution": ""`: When will the server run. `pre-load' or `init`
|
||||
* **Description**: This JSON file stores the server's encryption parameters, including cryptographic keys used for HMAC generation and user data encryption.
|
||||
|
||||
* ### `SaveData` Directory
|
||||
|
||||
* **Purpose**: This directory serves to store server data and enables the overriding of default settings and data.
|
||||
|
||||
* **Description**: The `SaveData` directory is employed for preserving user-specific or modified versions of data files and configuration settings, in addition to housing server-owned data. If a `Platforms` folder or a `nohud` folder exists within `SaveData`, their respective contents will supersede the corresponding data residing in the `database` directory, thereby eliminating the need for modifications to the core server code. This approach streamlines code updates while preserving Git integrity.
|
||||
|
||||
## Configuration of `settings.json`
|
||||
|
||||
The server's operational parameters can be configured through the `settings.json` file, which includes the following key elements:
|
||||
|
||||
* ### `SaveData`
|
||||
|
||||
* **Description**: This parameter defines the file paths where the server will persist data across different operating systems.
|
||||
|
||||
* **Settings**:
|
||||
|
||||
* `"windows": "{Home}\\AppData\\Roaming\\openparty\\"`: Specifies the designated directory for Windows operating systems.
|
||||
|
||||
* `"linux": "{Home}/.openparty/"`: Specifies the designated directory for Linux and other non-Windows operating systems.
|
||||
|
||||
* ### `Port`
|
||||
|
||||
* **Description**: This parameter sets the network port on which the server will actively listen for incoming connections.
|
||||
|
||||
* **Settings**: `"port": 80` (Default value: 80).
|
||||
|
||||
* ### `ForcePort`
|
||||
|
||||
* **Description**: This parameter ensures the server uses the specified port, even if the operating system attempts to assign an alternative.
|
||||
|
||||
* **Settings**: `"forcePort": false` (If set to `true`, the server will consistently employ port 80).
|
||||
|
||||
* ### `Public Access`
|
||||
|
||||
* **Description**: This parameter determines the server's accessibility to external networks.
|
||||
|
||||
* **Settings**: `"isPublic": true` (If set to `true`, the server is publicly accessible via `0.0.0.0`; if `false`, access is restricted to the local machine via `127.0.0.1`).
|
||||
|
||||
* ### `Enable SSL`
|
||||
|
||||
* **Description**: This parameter activates Secure Sockets Layer (SSL) functionality, enabling HTTPS if supported by the server environment.
|
||||
|
||||
* **Settings**: `"enableSSL": true` (Set to `true` to enable SSL).
|
||||
|
||||
* ### `Domain`
|
||||
|
||||
* **Description**: This parameter specifies the fully qualified domain name for the server.
|
||||
|
||||
* **Settings**: `"domain": "jdp.justdancenext.xyz"`.
|
||||
|
||||
* ### `Server Status`
|
||||
|
||||
* **Description**: This parameter indicates whether the server is currently in maintenance mode and defines its operational channel.
|
||||
|
||||
* **Settings**:
|
||||
|
||||
* `"isMaintenance": false`: Indicates that the server is not currently undergoing maintenance.
|
||||
|
||||
* `"channel": "prod"`: Specifies the server's operational channel (e.g., "prod" for production).
|
||||
|
||||
* ### `Modules`
|
||||
|
||||
* **Description**: This parameter defines server modules, encompassing their designated names, descriptive summaries, file paths, and triggers for execution.
|
||||
|
||||
* **Settings**:
|
||||
|
||||
* `"name": ""`: The assigned name of the module.
|
||||
|
||||
* `"description": ""`: A concise description outlining the module's functionality.
|
||||
|
||||
* `"path": ""`: The file path from which the server invokes the module.
|
||||
|
||||
* `"execution": ""`: Specifies the phase during which the server will execute the module (`pre-load` or `init`).
|
||||
|
||||
* **Note**: Legacy OpenParty modules are not directly compatible with the current codebase. They require modification to conform to the new OpenParty Plugin format. Reference examples of the updated plugin format are available within the `plugins/` directory.
|
||||
|
||||
## Usage
|
||||
OpenParty provides a straightforward setup process to get you up and running quickly. Once installed, customize your experience using the settings.json file and manage song databases effortlessly.
|
||||
|
||||
OpenParty provides a streamlined setup procedure designed for rapid deployment. Upon successful installation, the server's behavior can be customized through the `settings.json` file, and song databases can be managed with efficiency.
|
||||
|
||||
## Contribution
|
||||
Contributions are welcome to enhance features, improve performance, or expand platform support.
|
||||
|
||||
Contributions are actively encouraged to enhance existing features, improve system performance, or expand platform compatibility. Prospective contributors are advised to consult the GitHub repository for detailed guidelines.
|
||||
|
||||
## Support
|
||||
For any issues or inquiries, feel free to reach out via GitHub Issues or our community channels.
|
||||
|
||||
For addressing technical issues or submitting inquiries, users are encouraged to utilize the GitHub Issues platform or engage with community channels.
|
||||
|
||||
## Credits
|
||||
We gratefully acknowledge the contributions of the following individuals, whose efforts have been instrumental in the development and success of OpenParty: Wodson for JDCosmos Code, Rama for his leaked JDU Code, Devvie for JDO Code, Connor for JDWorld Code, and Mfadamo for his assistance with JDU. Special thanks to alexandregreff, XFelixBlack for their JDU code contributions, JJRoyale for JD19-22 back-end assistance, JustRex for logging Ubiserver, klucva for general help and support, adrian_flopper as the first back-end developer, and nic for various fixes and improvements.
|
||||
|
||||
Each of these contributors has played a vital role in making OpenParty what it is today. Thank you for your hard work and dedication!
|
||||
We acknowledge the invaluable contributions of the following individuals instrumental in the development and success of OpenParty: Wodson (JDCosmos Code), Rama (leaked JDU Code), Devvie (JDO Code), Connor (JDWorld Code), and Mfadamo (JDU assistance). Special commendation is given to alexandregreff and XFelixBlack (JDU code contributions), JJRoyale (JD19-22 back-end assistance), JustRex (Ubiserver logging), klucva (general assistance and support), adrian_flopper (the inaugural back-end developer), and nic (various corrective measures and enhancements).
|
||||
|
||||
## Keep This Project Active
|
||||
Saweria : [Click Me to Open Saweria](https://saweria.co/ibraaltabian17)
|
||||
Patreon : [Click Me to Open Patreon](https://www.patreon.com/ibratabian17)
|
||||
Their collective endeavors have been pivotal in shaping the current state of OpenParty.
|
||||
|
||||
## To Do List
|
||||
|
||||
The following features are planned for future development:
|
||||
|
||||
1. **Built-in Administrative Panel**: Implementation of a web-based administrative interface to facilitate simplified server management.
|
||||
|
||||
2. **Dynamic Reloading of SongDB & SKUPackages**: Introduction of functionality enabling the dynamic reloading of song databases and SKU packages without requiring a server restart.
|
||||
|
||||
3. **User Ban Capability**: Addition of a feature allowing administrators to ban users from the server.
|
||||
|
||||
4. **Rectification of JD16, JD18-22 Ubiservices Route Path for NX**: Resolution of routing discrepancies pertaining to Ubiservices on Nintendo Switch for Just Dance 2016 and versions 2018-2022.
|
||||
|
||||
5. **Correction of JD2018_MAIN - JD2022_MAIN Branch Issues from Just Dance Engine**: Addressing and resolving identified issues within the main branches of the Just Dance engine for versions 2018-2022.
|
||||
|
||||
6. **Reinstatement of Missing OpenParty Features**: Reintroduction of previously available functionalities that are currently absent from the system.
|
||||
|
||||
7. **Additional Features**: Continuous exploration and implementation of further functionalities to augment the OpenParty experience.
|
||||
|
||||
## Support This Project
|
||||
|
||||
Saweria: <https://saweria.co/ibraaltabian17>
|
||||
Patreon: <https://www.patreon.com/ibratabian17>
|
||||
@@ -1,26 +1,18 @@
|
||||
console.log(`[CAROUSEL] Initializing....`);
|
||||
const Logger = require('../utils/logger');
|
||||
const logger = new Logger('CAROUSEL');
|
||||
|
||||
const { CloneObject, readDatabaseJson, loadJsonFile } = require('../helper');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
logger.info(`Initializing....`);
|
||||
|
||||
const { CloneObject, loadJsonFile } = require('../helper');
|
||||
const cClass = require("./classList.json");
|
||||
const songdb = loadJsonFile('Platforms/openparty-all/songdbs.json', '../database/Platforms/openparty-all/songdbs.json');
|
||||
const helper = require('../helper')
|
||||
const settings = require('../../settings.json')
|
||||
var mostPlayed = {}
|
||||
const settings = require('../../settings.json');
|
||||
const SongService = require('../services/SongService');
|
||||
const MostPlayedService = require('../services/MostPlayedService');
|
||||
|
||||
mostPlayed = loadJsonFile('carousel/mostplayed.json', '../database/carousel/mostplayed.json');
|
||||
var carousel = {}; //avoid list cached
|
||||
let carousel = {}; //avoid list cached
|
||||
|
||||
const WEEKLY_PLAYLIST_PREFIX = 'DFRecommendedFU';
|
||||
|
||||
function updateMostPlayed(maps) {
|
||||
const currentWeek = getWeekNumber();
|
||||
mostPlayed[currentWeek] = mostPlayed[currentWeek] || {};
|
||||
mostPlayed[currentWeek][maps] = (mostPlayed[currentWeek][maps] || 0) + 1;
|
||||
fs.writeFileSync(path.join(helper.getSavefilePath(), 'carousel/mostplayed.json'), JSON.stringify(mostPlayed, null, 2));
|
||||
}
|
||||
|
||||
function addCategories(categories) {
|
||||
carousel.categories.push(Object.assign({}, categories));
|
||||
}
|
||||
@@ -61,90 +53,6 @@ function generateInfomap(arrays, type = "infoMap") {
|
||||
}));
|
||||
}
|
||||
|
||||
function filterSongs(songdbs, filterFunction) {
|
||||
return songdbs.filter(filterFunction).sort((a, b) => {
|
||||
const titleA = (songdb[a].title + songdb[a].mapName).toLowerCase();
|
||||
const titleB = (songdb[b].title + songdb[b].mapName).toLowerCase();
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// The following function was taken from here: https://stackoverflow.com/questions/16096872/how-to-sort-2-dimensional-array-by-column-value (answer by jahroy)
|
||||
function compareSecondColumn(a, b) {
|
||||
if (a[1] === b[1]) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return (a[1] < b[1]) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sortByTitle(list, word) {
|
||||
var x = [];
|
||||
var doesntContainWord = [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var titleIndex = songdb[list[i]].title.toLowerCase().indexOf(word);
|
||||
if (titleIndex === -1) {
|
||||
doesntContainWord.push(list[i]);
|
||||
} else {
|
||||
x.push([list[i], titleIndex]);
|
||||
}
|
||||
}
|
||||
doesntContainWord.sort();
|
||||
x.sort(compareSecondColumn);
|
||||
var toReturn = [];
|
||||
for (var j = 0; j < x.length; j++) {
|
||||
toReturn[j] = x[j][0];
|
||||
}
|
||||
return toReturn.concat(doesntContainWord);
|
||||
}
|
||||
|
||||
|
||||
function filterSongsBySearch(songdbs, search) {
|
||||
// Filter songs based on search criteria
|
||||
return sortByTitle(filterSongs(songdbs, item => {
|
||||
const song = songdb[item];
|
||||
return (
|
||||
(song.title && song.title.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(song.artist && song.artist.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(song.mapName && song.mapName.toLowerCase().includes(search.toLowerCase())) ||
|
||||
(song.originalJDVersion && song.originalJDVersion == search) ||
|
||||
(song.tags && song.tags.includes(search))
|
||||
);
|
||||
}), search.toLowerCase());
|
||||
}
|
||||
|
||||
function filterSongsByFirstLetter(songdbs, filter) {
|
||||
return filterSongs(songdbs, song => {
|
||||
const title = songdb[song].title.toLowerCase();
|
||||
const regex = new RegExp(`^[${filter}].*`);
|
||||
return regex.test(title);
|
||||
});
|
||||
}
|
||||
|
||||
function filterSongsByJDVersion(songdbs, version) {
|
||||
return filterSongs(songdbs, song => songdb[song].originalJDVersion === version);
|
||||
}
|
||||
|
||||
function filterSongsByTags(songdbs, Key) {
|
||||
return filterSongs(songdbs, song => {
|
||||
const songData = songdb[song];
|
||||
return songData && songData.tags && songData.tags.indexOf(Key) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
function getGlobalPlayedSong() {
|
||||
try {
|
||||
return Object.entries(mostPlayed[getWeekNumber()])
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(item => item[0]);
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function shuffleArray(array) {
|
||||
const shuffledArray = array.slice();
|
||||
for (let i = shuffledArray.length - 1; i > 0; i--) {
|
||||
@@ -162,62 +70,56 @@ function processPlaylists(playlists, type = "partyMap") {
|
||||
});
|
||||
}
|
||||
|
||||
function generateWeeklyRecommendedSong(playlists, type = "partyMap") {
|
||||
async function generateWeeklyRecommendedSong(playlists, type = "partyMap") {
|
||||
const currentWeekNumber = MostPlayedService.getWeekNumber();
|
||||
playlists.forEach(playlist => {
|
||||
if (playlist.name === `${WEEKLY_PLAYLIST_PREFIX}${getWeekNumber()}`) {
|
||||
if (playlist.name === `${WEEKLY_PLAYLIST_PREFIX}${currentWeekNumber}`) {
|
||||
addCategories(generateCategories(`[icon:PLAYLIST]Weekly: ${playlist.RecommendedName || ""}`, playlist.songlist, type));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function 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);
|
||||
}
|
||||
|
||||
function addJDVersion(songdbs, type = "partyMap") {
|
||||
addCategories(generateCategories("ABBA: You Can Dance", filterSongsByJDVersion(songdbs, 4884), type));
|
||||
addCategories(generateCategories("Just Dance Asia", filterSongsByJDVersion(songdbs, 4514), type));
|
||||
addCategories(generateCategories("Just Dance Kids", filterSongsByJDVersion(songdbs, 123), type));
|
||||
function addJDVersion(songMapNames, type = "partyMap") {
|
||||
addCategories(generateCategories("ABBA: You Can Dance", SongService.filterSongsByJDVersion(songMapNames, 4884), type));
|
||||
addCategories(generateCategories("Just Dance Asia", SongService.filterSongsByJDVersion(songMapNames, 4514), type));
|
||||
addCategories(generateCategories("Just Dance Kids", SongService.filterSongsByJDVersion(songMapNames, 123), type));
|
||||
for (let year = 2069; year >= 2014; year--) {
|
||||
addCategories(generateCategories(`Just Dance ${year}`, filterSongsByJDVersion(songdbs, year), type));
|
||||
addCategories(generateCategories(`Just Dance ${year}`, SongService.filterSongsByJDVersion(songMapNames, year), type));
|
||||
}
|
||||
for (let year = 4; year >= 1; year--) {
|
||||
addCategories(generateCategories(`Just Dance ${year}`, filterSongsByJDVersion(songdbs, year), type));
|
||||
addCategories(generateCategories(`Just Dance ${year}`, SongService.filterSongsByJDVersion(songMapNames, year), type));
|
||||
}
|
||||
addCategories(generateCategories("Unplayable Songs", filterSongsByJDVersion(songdbs, 404)));
|
||||
addCategories(generateCategories("Unplayable Songs", SongService.filterSongsByJDVersion(songMapNames, 404)));
|
||||
}
|
||||
|
||||
exports.generateCarousel = (search, type = "partyMap") => {
|
||||
exports.generateCarousel = async (search, type = "partyMap") => {
|
||||
carousel = {};
|
||||
carousel = CloneObject(cClass.rootClass);
|
||||
carousel.actionLists = cClass.actionListsClass
|
||||
const songdbs = Object.keys(songdb)
|
||||
|
||||
carousel.actionLists = cClass.actionListsClass;
|
||||
const allSongMapNames = SongService.getAllMapNames();
|
||||
|
||||
// Dynamic Carousel System
|
||||
addCategories(generateCategories(settings.server.modName, filterSongs(songdbs, song => true), type));
|
||||
addCategories(generateCategories("Recommended For You", CloneObject(shuffleArray(songdbs), type)));
|
||||
addCategories(generateCategories("[icon:PLAYLIST]Recently Added!", CloneObject(filterSongsByTags(songdbs, 'NEW')), type));
|
||||
generateWeeklyRecommendedSong(loadJsonFile('carousel/playlist.json', '../database/carousel/playlist.json'), type);
|
||||
processPlaylists(loadJsonFile('carousel/playlist.json', '../database/carousel/playlist.json'), type);
|
||||
addJDVersion(songdbs, type);
|
||||
addCategories(generateCategories(`Most Played Weekly!`, CloneObject(getGlobalPlayedSong()), type));
|
||||
addCategories(generateCategories(settings.server.modName, allSongMapNames, type));
|
||||
addCategories(generateCategories("Recommended For You", CloneObject(shuffleArray(allSongMapNames), type)));
|
||||
addCategories(generateCategories("[icon:PLAYLIST]Recently Added!", CloneObject(SongService.filterSongsByTags(allSongMapNames, 'NEW')), type));
|
||||
const path = require('path');
|
||||
await generateWeeklyRecommendedSong(loadJsonFile('carousel/playlist.json', '../database/data/carousel/playlist.json'), type);
|
||||
processPlaylists(loadJsonFile('carousel/playlist.json', '../database/data/carousel/playlist.json'), type);
|
||||
addJDVersion(allSongMapNames, type);
|
||||
addCategories(generateCategories(`Most Played Weekly!`, CloneObject(await MostPlayedService.getGlobalPlayedSong()), type));
|
||||
addCategories(Object.assign({}, cClass.searchCategoryClass));
|
||||
if (search !== "") {
|
||||
addCategories(generateCategories(`[icon:SEARCH_RESULT] Result Of: ${search}`, CloneObject(filterSongsBySearch(songdbs, search)), type));
|
||||
addCategories(generateCategories(`[icon:SEARCH_RESULT] Result Of: ${search}`, CloneObject(SongService.filterSongsBySearch(allSongMapNames, search)), type));
|
||||
}
|
||||
return carousel
|
||||
}
|
||||
return carousel;
|
||||
};
|
||||
|
||||
exports.generateCoopCarousel = (search) => JSON.parse(JSON.stringify(generateCarousel(search, "partyMapCoop")));
|
||||
exports.generateCoopCarousel = async (search) => JSON.parse(JSON.stringify(await exports.generateCarousel(search, "partyMapCoop")));
|
||||
|
||||
exports.generateRivalCarousel = (search) => JSON.parse(JSON.stringify(generateCarousel(search, "partyMap")));
|
||||
exports.generateRivalCarousel = async (search) => JSON.parse(JSON.stringify(await exports.generateCarousel(search, "partyMap")));
|
||||
|
||||
exports.generateSweatCarousel = (search) => JSON.parse(JSON.stringify(generateCarousel(search, "sweatMap")));
|
||||
exports.generateSweatCarousel = async (search) => JSON.parse(JSON.stringify(await exports.generateCarousel(search, "sweatMap")));
|
||||
|
||||
exports.generateChallengeCarousel = (search) => JSON.parse(JSON.stringify(generateCarousel(search, "create-challenge")));
|
||||
exports.generateChallengeCarousel = async (search) => JSON.parse(JSON.stringify(await exports.generateCarousel(search, "create-challenge")));
|
||||
|
||||
exports.updateMostPlayed = (maps) => updateMostPlayed(maps);
|
||||
exports.updateMostPlayed = async (maps) => await MostPlayedService.updateMostPlayed(maps);
|
||||
|
||||
122
core/classes/Core.js
Normal file
122
core/classes/Core.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Core class for OpenParty
|
||||
* Handles routing and initialization
|
||||
*/
|
||||
const { main } = require('../var');
|
||||
const { resolvePath } = require('../helper');
|
||||
const PluginManager = require('./PluginManager');
|
||||
const Router = require('./Router');
|
||||
const ErrorHandler = require('./ErrorHandler');
|
||||
const bodyParser = require('body-parser');
|
||||
const requestIp = require('../lib/ipResolver.js');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class Core {
|
||||
/**
|
||||
* Create a new Core instance
|
||||
* @param {Object} settings - Server settings from settings.json
|
||||
*/
|
||||
constructor(settings) {
|
||||
this.settings = settings;
|
||||
this.pluginManager = new PluginManager();
|
||||
this.router = new Router();
|
||||
this.logger = new Logger('CORE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the core functionality
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {Express} express - The Express module
|
||||
* @param {http.Server} server - The HTTP server instance
|
||||
*/
|
||||
async init(app, express, server) {
|
||||
this.logger.info('Initializing core...');
|
||||
|
||||
// Initialize the database
|
||||
const { initializeDatabase } = require('../database/sqlite');
|
||||
try {
|
||||
await initializeDatabase();
|
||||
this.logger.info('Database initialized successfully.');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize database:', error);
|
||||
process.exit(1); // Exit if database cannot be initialized
|
||||
}
|
||||
|
||||
// Configure middleware
|
||||
this.configureMiddleware(app, express);
|
||||
|
||||
// Load plugins
|
||||
this.pluginManager.loadPlugins(this.settings.modules);
|
||||
|
||||
// Initialize pre-load plugins
|
||||
this.pluginManager.initializePlugins(app, 'pre-load');
|
||||
|
||||
// Initialize core routes
|
||||
this.initializeCoreRoutes(app);
|
||||
|
||||
// Initialize regular plugins
|
||||
this.pluginManager.initializePlugins(app, 'init');
|
||||
|
||||
// Add 404 handler
|
||||
this.configure404Handler(app);
|
||||
|
||||
this.logger.info('Core initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Express middleware
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {Express} express - The Express module
|
||||
*/
|
||||
configureMiddleware(app, express) {
|
||||
app.use(express.json());
|
||||
app.use(bodyParser.raw());
|
||||
app.use(requestIp.mw());
|
||||
|
||||
// Use centralized error handler
|
||||
app.use(ErrorHandler.createExpressErrorHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize core route handlers
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initializeCoreRoutes(app) {
|
||||
try {
|
||||
// Check if class-based route handlers exist, otherwise use legacy handlers
|
||||
try {
|
||||
// Use the Router class to load and initialize all route handlers
|
||||
this.router.loadAllHandlers().initializeRoutes(app);
|
||||
|
||||
this.logger.info('Using class-based route handlers');
|
||||
} catch (err) {
|
||||
this.logger.error(`Error loading class-based route handlers: ${err.message}`);
|
||||
// Fall back to legacy route handlers
|
||||
require('../route/rdefault').initroute(app);
|
||||
require('../route/account').initroute(app);
|
||||
require('../route/leaderboard').initroute(app);
|
||||
require('../route/ubiservices').initroute(app);
|
||||
this.logger.info('Using legacy route handlers');
|
||||
}
|
||||
|
||||
this.logger.info('Core routes initialized');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error initializing core routes: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure 404 handler for unmatched routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
configure404Handler(app) {
|
||||
app.get('*', function(req, res) {
|
||||
res.status(404).send({
|
||||
'error': 404,
|
||||
'message': 'Path Not Recognized'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Core;
|
||||
94
core/classes/ErrorHandler.js
Normal file
94
core/classes/ErrorHandler.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Error Handler class for OpenParty
|
||||
* Provides centralized error handling functionality
|
||||
*/
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class ErrorHandler {
|
||||
/**
|
||||
* Create a new ErrorHandler instance
|
||||
*/
|
||||
constructor() {
|
||||
this.errors = [];
|
||||
this.maxLoggedErrors = 100; // Maximum number of errors to keep in memory
|
||||
this.logger = new Logger('ErrorHandler');
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error
|
||||
* @param {string} source - The source of the error (e.g., component name)
|
||||
* @param {Error|string} error - The error object or message
|
||||
* @param {Object} [context] - Additional context information
|
||||
*/
|
||||
logError(source, error, context = {}) {
|
||||
const errorMessage = error instanceof Error ? error.message : error;
|
||||
const errorStack = error instanceof Error ? error.stack : null;
|
||||
|
||||
const errorEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
source,
|
||||
message: errorMessage,
|
||||
stack: errorStack,
|
||||
context
|
||||
};
|
||||
|
||||
// Add to in-memory log
|
||||
this.errors.unshift(errorEntry);
|
||||
|
||||
// Trim log if it exceeds maximum size
|
||||
if (this.errors.length > this.maxLoggedErrors) {
|
||||
this.errors = this.errors.slice(0, this.maxLoggedErrors);
|
||||
}
|
||||
|
||||
// Log to console using the new logger
|
||||
this.logger.error(`[${source}] ${errorMessage}`);
|
||||
if (errorStack) {
|
||||
this.logger.error(errorStack);
|
||||
}
|
||||
|
||||
return errorEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Express middleware for handling errors
|
||||
* @returns {Function} Express middleware function
|
||||
*/
|
||||
createExpressErrorHandler() {
|
||||
return (err, req, res, next) => {
|
||||
// Log the error
|
||||
this.logError('Express', err, {
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body
|
||||
});
|
||||
|
||||
// Send appropriate response
|
||||
res.status(500).json({
|
||||
error: 'Internal Server Error',
|
||||
message: process.env.NODE_ENV === 'production' ?
|
||||
'An unexpected error occurred' :
|
||||
err.message
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent errors
|
||||
* @param {number} [limit=10] - Maximum number of errors to return
|
||||
* @returns {Array} Recent errors
|
||||
*/
|
||||
getRecentErrors(limit = 10) {
|
||||
return this.errors.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all logged errors
|
||||
*/
|
||||
clearErrors() {
|
||||
this.errors = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Export a singleton instance
|
||||
module.exports = new ErrorHandler();
|
||||
55
core/classes/Plugin.js
Normal file
55
core/classes/Plugin.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Base Plugin class for OpenParty
|
||||
* All plugins should extend this class
|
||||
*/
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class Plugin {
|
||||
/**
|
||||
* Create a new plugin
|
||||
* @param {string} name - The name of the plugin
|
||||
* @param {string} description - A description of what the plugin does
|
||||
*/
|
||||
constructor(name, description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.enabled = true;
|
||||
this.logger = new Logger(name); // Use plugin name as module name for logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin's routes
|
||||
* This method should be overridden by plugin implementations
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
// This method should be implemented by child classes
|
||||
this.logger.info(`initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the plugin
|
||||
*/
|
||||
enable() {
|
||||
this.enabled = true;
|
||||
this.logger.info(`enabled`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the plugin
|
||||
*/
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
this.logger.info(`disabled`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the plugin is enabled
|
||||
* @returns {boolean} Whether the plugin is enabled
|
||||
*/
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Plugin;
|
||||
121
core/classes/PluginManager.js
Normal file
121
core/classes/PluginManager.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Plugin Manager for OpenParty
|
||||
* Handles loading and managing plugins
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Plugin = require('./Plugin'); // This is the Plugin class PluginManager uses for comparison
|
||||
const { resolvePath } = require('../helper');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class PluginManager {
|
||||
/**
|
||||
* Create a new plugin manager
|
||||
*/
|
||||
constructor() {
|
||||
this.plugins = new Map();
|
||||
this.logger = new Logger('PluginManager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugins from settings
|
||||
* @param {Object} modules - The modules configuration from settings.json
|
||||
* @returns {Map} The loaded plugins
|
||||
*/
|
||||
loadPlugins(modules) {
|
||||
this.logger.info('Loading plugins...');
|
||||
|
||||
// Log the Plugin class that PluginManager is using for comparison
|
||||
this.logger.info('Plugin class used for comparison:', Plugin.name);
|
||||
|
||||
modules.forEach((item) => {
|
||||
try {
|
||||
const plugin = require(resolvePath(item.path));
|
||||
|
||||
// Log the Plugin class that the loaded plugin is extending
|
||||
this.logger.info(`Loaded plugin '${item.path}' extends:`, Object.getPrototypeOf(plugin.constructor).name);
|
||||
|
||||
// Verify that the plugin extends the Plugin class
|
||||
if (plugin instanceof Plugin) {
|
||||
this.plugins.set(plugin.name, plugin);
|
||||
this.logger.info(`Loaded plugin: ${plugin.name}`);
|
||||
} else {
|
||||
this.logger.error(`Error: ${item.path} is not a valid plugin. It does not extend the expected 'Plugin' class.`);
|
||||
// Provide more detail if the instanceof check fails
|
||||
this.logger.error(`Expected Plugin constructor:`, Plugin);
|
||||
this.logger.error(`Actual plugin's prototype chain constructor:`, Object.getPrototypeOf(plugin.constructor));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error loading plugin ${item.path}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugins based on execution type
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {string} executionType - The execution type (pre-load, init, etc.)
|
||||
*/
|
||||
initializePlugins(app, executionType) {
|
||||
this.logger.info(`Initializing ${executionType} plugins...`);
|
||||
|
||||
this.plugins.forEach((plugin) => {
|
||||
// Assuming isEnabled() exists on the Plugin base class or is handled otherwise
|
||||
if (plugin.isEnabled && plugin.isEnabled()) {
|
||||
try {
|
||||
// Get the plugin's configuration from settings.json
|
||||
const pluginConfig = this.getPluginConfig(plugin.name);
|
||||
if (pluginConfig && pluginConfig.execution === executionType) {
|
||||
this.logger.info(`Calling initroute for plugin: ${plugin.name} (Execution Type: ${executionType})`);
|
||||
plugin.initroute(app);
|
||||
} else {
|
||||
this.logger.info(`Skipping plugin ${plugin.name}: Execution type mismatch or no config.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error initializing plugin ${plugin.name}: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.info(`Skipping disabled plugin: ${plugin.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a plugin by name
|
||||
* @param {string} name - The name of the plugin
|
||||
* @returns {Plugin|null} The plugin or null if not found
|
||||
*/
|
||||
getPlugin(name) {
|
||||
return this.plugins.get(name) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded plugins
|
||||
* @returns {Map} The loaded plugins
|
||||
*/
|
||||
getPlugins() {
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration for a plugin from settings.json
|
||||
* @param {string} name - The name of the plugin
|
||||
* @returns {Object|null} The plugin configuration or null if not found
|
||||
*/
|
||||
getPluginConfig(name) {
|
||||
// IMPORTANT: Adjust this path if your settings.json is not located relative to PluginManager.js
|
||||
// For example, if PluginManager is in 'core/classes' and settings.json is in the root,
|
||||
// '../../settings.json' is likely correct.
|
||||
try {
|
||||
const settings = require('../../settings.json');
|
||||
return settings.modules.find(module => module.name === name) || null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error loading settings.json: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PluginManager;
|
||||
82
core/classes/Router.js
Normal file
82
core/classes/Router.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Router class for OpenParty
|
||||
* Manages all route handlers in a centralized way
|
||||
*/
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class Router {
|
||||
/**
|
||||
* Create a new Router instance
|
||||
*/
|
||||
constructor() {
|
||||
this.routeHandlers = [];
|
||||
this.logger = new Logger('ROUTER');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a route handler
|
||||
* @param {RouteHandler} routeHandler - The route handler to register
|
||||
*/
|
||||
registerHandler(routeHandler) {
|
||||
this.routeHandlers.push(routeHandler);
|
||||
this.logger.info(`Registered route handler: ${routeHandler.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all registered route handlers
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initializeRoutes(app) {
|
||||
this.logger.info('Initializing all route handlers...');
|
||||
|
||||
if (this.routeHandlers.length === 0) {
|
||||
this.logger.info('No route handlers registered');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize each route handler
|
||||
this.routeHandlers.forEach(handler => {
|
||||
try {
|
||||
handler.initroute(app);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error initializing route handler ${handler.name}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.info(`Initialized ${this.routeHandlers.length} route handlers`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all route handlers from the routes directory
|
||||
* @returns {Router} This router instance for chaining
|
||||
*/
|
||||
loadAllHandlers() {
|
||||
this.logger.info('Loading all route handlers...');
|
||||
|
||||
try {
|
||||
// Load all route handlers
|
||||
const defaultHandler = require('./routes/DefaultRouteHandler');
|
||||
const accountHandler = require('./routes/AccountRouteHandler');
|
||||
const leaderboardHandler = require('./routes/LeaderboardRouteHandler');
|
||||
const ubiservicesHandler = require('./routes/UbiservicesRouteHandler');
|
||||
const songDBHandler = require('./routes/SongDBRouteHandler');
|
||||
const carouselHandler = require('./routes/CarouselRouteHandler');
|
||||
|
||||
// Register all handlers
|
||||
this.registerHandler(defaultHandler);
|
||||
this.registerHandler(accountHandler);
|
||||
this.registerHandler(leaderboardHandler);
|
||||
this.registerHandler(ubiservicesHandler);
|
||||
this.registerHandler(songDBHandler);
|
||||
this.registerHandler(carouselHandler);
|
||||
|
||||
this.logger.info('All route handlers loaded successfully');
|
||||
} catch (error) {
|
||||
this.logger.error(`Error loading route handlers: ${error.stack}`);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Router;
|
||||
73
core/classes/Server.js
Normal file
73
core/classes/Server.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Server class for OpenParty
|
||||
* Manages the HTTP server lifecycle
|
||||
*/
|
||||
const express = require('express');
|
||||
const Core = require('./Core');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class Server {
|
||||
/**
|
||||
* Create a new server instance
|
||||
* @param {Object} settings - Server settings from settings.json
|
||||
*/
|
||||
constructor(settings) {
|
||||
this.settings = settings;
|
||||
this.app = express();
|
||||
this.core = new Core(settings);
|
||||
this.port = settings.server.forcePort ? settings.server.port : process.env.PORT || settings.server.port;
|
||||
this.host = settings.server.isPublic ? '0.0.0.0' : '127.0.0.1';
|
||||
this.logger = new Logger('SERVER');
|
||||
|
||||
// Set process title
|
||||
process.title = 'OpenParty | Custom Just Dance Unlimited Server';
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the server
|
||||
* @returns {http.Server} The HTTP server instance
|
||||
*/
|
||||
start() {
|
||||
this.logger.info(`Starting OpenParty server...`);
|
||||
|
||||
// Create and start the HTTP server
|
||||
this.server = this.app.listen(this.port, this.host, async () => { // Made callback async
|
||||
// Initialize the core and await its completion
|
||||
await this.core.init(this.app, express, this.server);
|
||||
|
||||
this.logger.info(`Listening on ${this.host}:${this.port}`);
|
||||
this.logger.info(`Open panel to see more logs`);
|
||||
this.logger.info(`Running in ${process.env.NODE_ENV || 'development'} mode`);
|
||||
});
|
||||
|
||||
// Handle server errors
|
||||
this.server.on('error', (error) => {
|
||||
this.logger.error(`Error starting server: ${error.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', () => this.stop());
|
||||
process.on('SIGTERM', () => this.stop());
|
||||
|
||||
return this.server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server gracefully
|
||||
*/
|
||||
stop() {
|
||||
this.logger.info(`Stopping server...`);
|
||||
|
||||
if (this.server) {
|
||||
this.server.close(() => {
|
||||
this.logger.info(`Server stopped`);
|
||||
process.exit(0);
|
||||
});
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
549
core/classes/routes/AccountRouteHandler.js
Normal file
549
core/classes/routes/AccountRouteHandler.js
Normal file
@@ -0,0 +1,549 @@
|
||||
/**
|
||||
* 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();
|
||||
1
core/classes/routes/BaseRouteHandler.js
Normal file
1
core/classes/routes/BaseRouteHandler.js
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
123
core/classes/routes/CarouselRouteHandler.js
Normal file
123
core/classes/routes/CarouselRouteHandler.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Carousel Route Handler for OpenParty
|
||||
* Handles carousel and related content routes
|
||||
*/
|
||||
const RouteHandler = require('./RouteHandler');
|
||||
const CarouselService = require('../../services/CarouselService');
|
||||
const coreMain = require('../../var').main; // Assuming core.main is needed for various carousel data
|
||||
const Logger = require('../../utils/logger');
|
||||
|
||||
class CarouselRouteHandler extends RouteHandler {
|
||||
constructor() {
|
||||
super('CarouselRouteHandler');
|
||||
this.logger = new Logger('CarouselRouteHandler');
|
||||
|
||||
// Bind handler methods to maintain 'this' context
|
||||
this.handleCarousel = this.handleCarousel.bind(this);
|
||||
this.handleCarouselPages = this.handleCarouselPages.bind(this);
|
||||
this.handleUpsellVideos = this.handleUpsellVideos.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
this.logger.info(`Initializing routes...`);
|
||||
|
||||
// Carousel routes
|
||||
this.registerPost(app, "/carousel/v2/pages/avatars", this.handleCarousel);
|
||||
this.registerPost(app, "/carousel/v2/pages/dancerprofile", this.handleCarousel);
|
||||
this.registerPost(app, "/carousel/v2/pages/jdtv", this.handleCarousel);
|
||||
this.registerPost(app, "/carousel/v2/pages/jdtv-nx", this.handleCarousel);
|
||||
this.registerPost(app, "/carousel/v2/pages/quests", this.handleCarousel);
|
||||
this.registerPost(app, "/carousel/v2/pages/:mode", this.handleCarouselPages);
|
||||
this.registerPost(app, "/carousel/v2/pages/upsell-videos", this.handleUpsellVideos);
|
||||
|
||||
this.logger.info(`Routes initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle carousel requests for static data (avatars, dancerprofile, jdtv, quests)
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleCarousel(req, res) {
|
||||
const path = req.path.split('/').pop();
|
||||
switch (path) {
|
||||
case 'avatars':
|
||||
res.send(coreMain.avatars);
|
||||
break;
|
||||
case 'dancerprofile':
|
||||
res.send(coreMain.dancerprofile);
|
||||
break;
|
||||
case 'jdtv':
|
||||
case 'jdtv-nx':
|
||||
res.send(coreMain.jdtv);
|
||||
break;
|
||||
case 'quests':
|
||||
res.send(coreMain.quests);
|
||||
break;
|
||||
default:
|
||||
res.status(404).send('Not found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle carousel pages requests for dynamic content (party, sweat, challenges)
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleCarouselPages(req, res) {
|
||||
let search = "";
|
||||
if (req.body.searchString && req.body.searchString != "") {
|
||||
search = req.body.searchString;
|
||||
} else if (req.body.searchTags && req.body.searchTags != undefined) {
|
||||
search = req.body.searchTags[0];
|
||||
}
|
||||
|
||||
let action = null;
|
||||
let isPlaylist = false;
|
||||
|
||||
switch (req.params.mode) {
|
||||
case "party":
|
||||
case "partycoop":
|
||||
action = "partyMap";
|
||||
break;
|
||||
case "sweat":
|
||||
action = "sweatMap";
|
||||
break;
|
||||
case "create-challenge":
|
||||
action = "create-challenge";
|
||||
break;
|
||||
case "jd2019-playlists":
|
||||
case "jd2020-playlists":
|
||||
case "jd2021-playlists":
|
||||
case "jd2022-playlists":
|
||||
isPlaylist = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isPlaylist) {
|
||||
// Assuming core.generatePlaylist is still needed for playlist categories
|
||||
return res.json(require('../../lib/playlist').generatePlaylist().playlistcategory);
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
return res.send(await CarouselService.generateCarousel(search, action));
|
||||
}
|
||||
|
||||
return res.json({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle upsell videos requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleUpsellVideos(req, res) {
|
||||
res.send(coreMain.upsellvideos);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CarouselRouteHandler();
|
||||
335
core/classes/routes/DefaultRouteHandler.js
Normal file
335
core/classes/routes/DefaultRouteHandler.js
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Default Route Handler for OpenParty
|
||||
* Handles default routes and game functionality
|
||||
*/
|
||||
const RouteHandler = require('./RouteHandler');
|
||||
const requestCountry = require("request-country");
|
||||
const settings = require('../../../settings.json');
|
||||
const core = {
|
||||
main: require('../../var').main,
|
||||
generatePlaylist: require('../../lib/playlist').generatePlaylist,
|
||||
CloneObject: require('../../helper').CloneObject,
|
||||
loadJsonFile: require('../../helper').loadJsonFile,
|
||||
signer: require('../../lib/signUrl')
|
||||
};
|
||||
const ipResolver = require('../../lib/ipResolver');
|
||||
const deployTime = Date.now();
|
||||
|
||||
class DefaultRouteHandler extends RouteHandler {
|
||||
constructor() {
|
||||
super('DefaultRouteHandler');
|
||||
|
||||
// Load nohud list
|
||||
const path = require('path');
|
||||
this.chunk = core.loadJsonFile('nohud/chunk.json', path.join(__dirname, '../../../database/data/nohud/chunk.json'));
|
||||
|
||||
// Active users tracking
|
||||
this.activeUsers = {};
|
||||
|
||||
// Bind handler methods to maintain 'this' context
|
||||
this.handlePackages = this.handlePackages.bind(this);
|
||||
this.handleSession = this.handleSession.bind(this);
|
||||
this.handleHome = this.handleHome.bind(this);
|
||||
this.handleAliases = this.handleAliases.bind(this);
|
||||
this.handlePlaylists = this.handlePlaylists.bind(this);
|
||||
this.handleCountry = this.handleCountry.bind(this);
|
||||
this.handleSubscription = this.handleSubscription.bind(this);
|
||||
this.handleQuests = this.handleQuests.bind(this);
|
||||
this.handleSessionQuest = this.handleSessionQuest.bind(this);
|
||||
this.handleItems = this.handleItems.bind(this);
|
||||
this.handleSkuConstants = this.handleSkuConstants.bind(this);
|
||||
this.handleDanceMachine = this.handleDanceMachine.bind(this);
|
||||
this.handleContentAuthorization = this.handleContentAuthorization.bind(this);
|
||||
this.handlePackagesV2 = this.handlePackagesV2.bind(this);
|
||||
this.handleComVideos = this.handleComVideos.bind(this);
|
||||
this.handlePing = this.handlePing.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is authorized
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
* @returns {boolean} Whether the request is authorized
|
||||
* @private
|
||||
*/
|
||||
checkAuth(req, res) {
|
||||
if (req.header('X-SkuId')) {
|
||||
if (!(req.header('X-SkuId').startsWith("jd") || req.header('X-SkuId').startsWith("JD")) || !req.headers["authorization"].startsWith("Ubi")) {
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'Bad request! Oops you didn\'t specify what file should we give you, try again'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'Oopsie! We can\'t check that ur Request is valid',
|
||||
'headder': req.headers
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset timeout for active user
|
||||
* @param {string} ip - User's IP address
|
||||
* @param {string} platform - User's platform
|
||||
* @private
|
||||
*/
|
||||
resetTimeout(ip, platform) {
|
||||
if (this.activeUsers[ip]) {
|
||||
clearTimeout(this.activeUsers[ip].timeout);
|
||||
}
|
||||
this.activeUsers[ip] = {
|
||||
timestamp: Date.now(),
|
||||
platform: platform || null,
|
||||
timeout: setTimeout(() => {
|
||||
delete this.activeUsers[ip];
|
||||
}, 20 * 60 * 1000) // 20 minutes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
console.log(`[ROUTE] ${this.name} initializing routes...`);
|
||||
|
||||
// Package routes
|
||||
this.registerGet(app, "/packages/v1/sku-packages", this.handlePackages);
|
||||
this.registerPost(app, "/carousel/:version/packages", this.handlePackagesV2);
|
||||
|
||||
// Session routes
|
||||
this.registerPost(app, "/sessions/v1/session", this.handleSession);
|
||||
|
||||
// Home and profile routes
|
||||
this.registerPost(app, "/home/v1/tiles", this.handleHome);
|
||||
this.registerGet(app, "/aliasdb/v1/aliases", this.handleAliases);
|
||||
this.registerGet(app, "/playlistdb/v1/playlists", this.handlePlaylists);
|
||||
this.registerGet(app, "/profile/v2/country", this.handleCountry);
|
||||
|
||||
// Subscription and quest routes
|
||||
this.registerPost(app, "/subscription/v1/refresh", this.handleSubscription);
|
||||
this.registerGet(app, "/questdb/v1/quests", this.handleQuests);
|
||||
this.registerGet(app, "/session-quest/v1/", this.handleSessionQuest);
|
||||
|
||||
// Item and customization routes
|
||||
this.registerGet(app, "/customizable-itemdb/v1/items", this.handleItems);
|
||||
this.registerGet(app, "/constant-provider/v1/sku-constants", this.handleSkuConstants);
|
||||
this.registerGet(app, "/dance-machine/v1/blocks", this.handleDanceMachine);
|
||||
|
||||
// Content authorization route
|
||||
this.registerGet(app, "/content-authorization/v1/maps/*", this.handleContentAuthorization);
|
||||
|
||||
// Video routes
|
||||
this.registerGet(app, "/com-video/v1/com-videos-fullscreen", this.handleComVideos);
|
||||
|
||||
// Status route
|
||||
this.registerGet(app, "/status/v1/ping", this.handlePing);
|
||||
|
||||
console.log(`[ROUTE] ${this.name} routes initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle package requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePackages(req, res) {
|
||||
if (!this.checkAuth(req, res)) return;
|
||||
|
||||
const skuId = req.header('X-SkuId');
|
||||
const skuPackages = core.main.skupackages;
|
||||
const platforms = ['wiiu', 'nx', 'pc', 'durango', 'orbis'];
|
||||
|
||||
for (const platform of platforms) {
|
||||
if (skuId.includes(platform)) {
|
||||
res.send(skuPackages[platform]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleSession(req, res) {
|
||||
res.send({
|
||||
"pairingCode": "000000",
|
||||
"sessionId": "00000000-0000-0000-0000-000000000000",
|
||||
"docId": "0000000000000000000000000000000000000000"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle home requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleHome(req, res) {
|
||||
res.send(core.main.home);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle aliases requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleAliases(req, res) {
|
||||
res.send(core.main.aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle playlists requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePlaylists(req, res) {
|
||||
res.send(core.generatePlaylist().playlistdb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle country requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleCountry(req, res) {
|
||||
let country = requestCountry(req);
|
||||
if (country == false) {
|
||||
country = "US";
|
||||
}
|
||||
res.send({ "country": country });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle subscription requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleSubscription(req, res) {
|
||||
res.send(core.main.subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle quests requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleQuests(req, res) {
|
||||
const sku = req.header('X-SkuId');
|
||||
if (sku && sku.startsWith('jd2017-nx-all')) {
|
||||
res.send(core.main.questsnx);
|
||||
} else {
|
||||
res.send(core.main.questspc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session quest requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleSessionQuest(req, res) {
|
||||
res.send({
|
||||
"__class": "SessionQuestService::QuestData",
|
||||
"newReleases": []
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle items requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleItems(req, res) {
|
||||
res.send(core.main.items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle SKU constants requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleSkuConstants(req, res) {
|
||||
res.send(core.main.block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dance machine requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleDanceMachine(req, res) {
|
||||
if (req.header('X-SkuId').includes("pc")) {
|
||||
res.send(core.main.dancemachine_pc);
|
||||
} else if (req.header('X-SkuId').includes("nx")) {
|
||||
res.send(core.main.dancemachine_nx);
|
||||
} else {
|
||||
res.send('Invalid Game');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle content authorization requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleContentAuthorization(req, res) {
|
||||
if (!this.checkAuth(req, res)) return;
|
||||
|
||||
const maps = req.url.split("/").pop();
|
||||
try {
|
||||
if (this.chunk[maps]) {
|
||||
const placeholder = core.CloneObject(require('../../../database/data/nohud/placeholder.json'));
|
||||
placeholder.urls = this.chunk[maps];
|
||||
res.send(placeholder);
|
||||
} else {
|
||||
const placeholder = core.CloneObject(require('../../../database/data/nohud/placeholder.json'));
|
||||
placeholder.urls = {};
|
||||
res.send(placeholder);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle packages v2 requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePackagesV2(req, res) {
|
||||
res.send(core.main.packages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle com videos requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleComVideos(req, res) {
|
||||
res.send([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle ping requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePing(req, res) {
|
||||
const ip = ipResolver.getClientIp(req);
|
||||
const platform = req.header('X-SkuId') || "unknown";
|
||||
this.resetTimeout(ip, platform);
|
||||
res.send([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance of the route handler
|
||||
module.exports = new DefaultRouteHandler();
|
||||
370
core/classes/routes/LeaderboardRouteHandler.js
Normal file
370
core/classes/routes/LeaderboardRouteHandler.js
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* Leaderboard Route Handler for OpenParty
|
||||
* Handles leaderboard-related routes
|
||||
*/
|
||||
const RouteHandler = require('./RouteHandler');
|
||||
const { getDb } = require('../../database/sqlite');
|
||||
const core = require('../../var').main;
|
||||
|
||||
class LeaderboardRouteHandler extends RouteHandler {
|
||||
/**
|
||||
* Create a new leaderboard route handler
|
||||
*/
|
||||
constructor() {
|
||||
super('LeaderboardRouteHandler');
|
||||
|
||||
// Bind handler methods to maintain 'this' context
|
||||
this.handleGetLeaderboard = this.handleGetLeaderboard.bind(this);
|
||||
this.handlePostScore = this.handlePostScore.bind(this);
|
||||
this.handleGetDanceOfTheWeek = this.handleGetDanceOfTheWeek.bind(this);
|
||||
this.handleLeaderboard = this.handleLeaderboard.bind(this);
|
||||
this.getWeekNumber = this.getWeekNumber.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
console.log(`[ROUTE] ${this.name} initializing routes...`);
|
||||
|
||||
// Register routes
|
||||
this.registerGet(app, '/v1/leaderboard/:mapName', this.handleGetLeaderboard);
|
||||
this.registerPost(app, '/v1/leaderboard/:mapName', this.handlePostScore);
|
||||
this.registerGet(app, '/v1/dance-of-the-week', this.handleGetDanceOfTheWeek);
|
||||
this.registerGet(app, "/leaderboard/v1/maps/:mapName/:type", this.handleLeaderboard);
|
||||
this.registerGet(app, "/leaderboard/v1/coop_points/mine", this.handleCoopPoints);
|
||||
|
||||
console.log(`[ROUTE] ${this.name} routes initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle leaderboard retrieval
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleGetLeaderboard(req, res) {
|
||||
const { mapName } = req.params;
|
||||
try {
|
||||
const scores = await this.getLeaderboard(mapName);
|
||||
res.json({
|
||||
mapName,
|
||||
scores: scores.sort((a, b) => b.score - a.score).slice(0, 100)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[LEADERBOARD] Error getting leaderboard for ${mapName}:`, error.message);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle score submission
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handlePostScore(req, res) {
|
||||
const { mapName } = req.params;
|
||||
const { profileId, score, username } = req.body;
|
||||
|
||||
if (!profileId || !score || !username) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
|
||||
try {
|
||||
const db = getDb();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// Check if user already has a score for this map
|
||||
const existingScore = await new Promise((resolve, reject) => {
|
||||
db.get('SELECT score FROM leaderboard WHERE mapName = ? AND profileId = ?', [mapName, profileId], (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
|
||||
if (existingScore) {
|
||||
// Update score if new score is higher
|
||||
if (score > existingScore.score) {
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('UPDATE leaderboard SET score = ?, username = ?, timestamp = ? WHERE mapName = ? AND profileId = ?',
|
||||
[score, username, timestamp, mapName, profileId], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Add new score
|
||||
await new Promise((resolve, reject) => {
|
||||
db.run('INSERT INTO leaderboard (mapName, profileId, username, score, timestamp) VALUES (?, ?, ?, ?, ?)',
|
||||
[mapName, profileId, username, score, timestamp], (err) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
mapName,
|
||||
score
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[LEADERBOARD] Error posting score for ${mapName}:`, error.message);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dance of the week retrieval
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleGetDanceOfTheWeek(req, res) {
|
||||
try {
|
||||
const dotw = await this.getDanceOfTheWeek();
|
||||
res.json(dotw);
|
||||
} catch (error) {
|
||||
console.error('[LEADERBOARD] Error getting Dance of the Week:', error.message);
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leaderboard data from SQLite.
|
||||
* @param {string} mapName - The name of the map.
|
||||
* @returns {Promise<Array>} A promise that resolves to an array of leaderboard entries.
|
||||
* @private
|
||||
*/
|
||||
async getLeaderboard(mapName) {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT profileId, username, score, timestamp FROM leaderboard WHERE mapName = ? ORDER BY score DESC LIMIT 100', [mapName], (err, rows) => {
|
||||
if (err) {
|
||||
console.error(`[LEADERBOARD] Error loading leaderboard for ${mapName} from DB: ${err.message}`);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save leaderboard data to SQLite.
|
||||
* This method is now handled by handlePostScore for individual score updates.
|
||||
* This placeholder is kept for compatibility if other parts of the code still call it.
|
||||
* @param {Object} leaderboard - Leaderboard data to save (ignored for SQLite).
|
||||
* @private
|
||||
*/
|
||||
async saveLeaderboard(leaderboard) {
|
||||
console.log('[LEADERBOARD] saveLeaderboard (legacy) called. Data is saved via handlePostScore.');
|
||||
// This method is largely deprecated with the new SQLite approach for individual score updates.
|
||||
// If a full leaderboard object is passed, it implies a bulk update or migration, which is not
|
||||
// directly supported by the current single-score update logic.
|
||||
// For now, it will just log a message.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dance of the week data from SQLite.
|
||||
* @returns {Promise<Object>} A promise that resolves to the Dance of the Week data.
|
||||
* @private
|
||||
*/
|
||||
async getDanceOfTheWeek() {
|
||||
const db = getDb();
|
||||
const currentWeekNumber = this.getWeekNumber();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT mapName, weekNumber, startDate FROM dotw WHERE weekNumber = ?', [currentWeekNumber], async (err, row) => {
|
||||
if (err) {
|
||||
console.error(`[LEADERBOARD] Error loading DOTW from DB: ${err.message}`);
|
||||
reject(err);
|
||||
} else if (row) {
|
||||
resolve(row);
|
||||
} else {
|
||||
// Create default dance of the week if not found
|
||||
const defaultDotw = {
|
||||
mapName: 'Starships', // Default map
|
||||
weekNumber: currentWeekNumber,
|
||||
startDate: new Date().toISOString()
|
||||
};
|
||||
await this.saveDanceOfTheWeek(defaultDotw);
|
||||
resolve(defaultDotw);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save dance of the week data to SQLite.
|
||||
* @param {Object} dotw - Dance of the week data to save.
|
||||
* @returns {Promise<void>} A promise that resolves when data is saved.
|
||||
* @private
|
||||
*/
|
||||
async saveDanceOfTheWeek(dotw) {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(`INSERT OR REPLACE INTO dotw (mapName, weekNumber, startDate) VALUES (?, ?, ?)`,
|
||||
[dotw.mapName, dotw.weekNumber, dotw.startDate], (err) => {
|
||||
if (err) {
|
||||
console.error(`[LEADERBOARD] Error saving DOTW to DB: ${err.message}`);
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('[LEADERBOARD] Dance of the Week data saved to DB.');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current week number
|
||||
* @returns {number} Week number
|
||||
* @private
|
||||
*/
|
||||
getWeekNumber() {
|
||||
const now = new Date();
|
||||
const start = new Date(now.getFullYear(), 0, 1);
|
||||
const diff = now - start;
|
||||
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
||||
return Math.floor(diff / oneWeek);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle leaderboard requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleLeaderboard(req, res) {
|
||||
const { mapName, type } = req.params;
|
||||
const currentWeekNumber = this.getWeekNumber();
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case "dancer-of-the-week":
|
||||
await this.handleDancerOfTheWeek(req, res, mapName, currentWeekNumber);
|
||||
break;
|
||||
case "friends":
|
||||
res.send({ __class: "LeaderboardList", entries: [] });
|
||||
break;
|
||||
default:
|
||||
await this.handleRegularLeaderboard(req, res, mapName);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[LEADERBOARD] Error:", error.message);
|
||||
res.status(500).send("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dancer of the week requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
* @param {string} mapName - The map name
|
||||
* @param {number} currentWeekNumber - Current week number
|
||||
* @private
|
||||
*/
|
||||
async handleDancerOfTheWeek(req, res, mapName, currentWeekNumber) {
|
||||
const db = getDb();
|
||||
try {
|
||||
const dotwEntries = await new Promise((resolve, reject) => {
|
||||
db.all('SELECT profileId, score, gameVersion, rank, name, avatar, country, platformId, alias, aliasGender, jdPoints, portraitBorder FROM dotw WHERE mapName = ? AND weekNumber = ? ORDER BY score DESC LIMIT 1', [mapName, currentWeekNumber], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
|
||||
if (dotwEntries.length === 0) {
|
||||
return res.json({
|
||||
"__class": "DancerOfTheWeek",
|
||||
"gameVersion": "jd2019", // Default if no DOTW found
|
||||
});
|
||||
}
|
||||
|
||||
const highestEntry = dotwEntries[0]; // Since we limited to 1 and ordered by score DESC
|
||||
|
||||
const dancerOfTheWeek = {
|
||||
"__class": "DancerOfTheWeek",
|
||||
"profileId": highestEntry.profileId,
|
||||
"score": highestEntry.score,
|
||||
"gameVersion": highestEntry.gameVersion || "jd2020",
|
||||
"rank": highestEntry.rank || 1,
|
||||
"name": highestEntry.name,
|
||||
"avatar": highestEntry.avatar,
|
||||
"country": highestEntry.country,
|
||||
"platformId": highestEntry.platformId,
|
||||
"alias": highestEntry.alias,
|
||||
"aliasGender": highestEntry.aliasGender,
|
||||
"jdPoints": highestEntry.jdPoints,
|
||||
"portraitBorder": highestEntry.portraitBorder
|
||||
};
|
||||
|
||||
res.json(dancerOfTheWeek);
|
||||
} catch (error) {
|
||||
console.error(`[LEADERBOARD] Error in handleDancerOfTheWeek: ${error.message}`);
|
||||
res.status(500).send("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle regular leaderboard requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
* @param {string} mapName - The map name
|
||||
* @private
|
||||
*/
|
||||
async handleRegularLeaderboard(req, res, mapName) {
|
||||
const leaderboardData = {
|
||||
"__class": "LeaderboardList",
|
||||
"entries": []
|
||||
};
|
||||
const db = getDb();
|
||||
|
||||
try {
|
||||
const leaderboardEntries = await new Promise((resolve, reject) => {
|
||||
db.all('SELECT profileId, username, score, name, avatar, country, platformId, alias, aliasGender, jdPoints, portraitBorder FROM leaderboard WHERE mapName = ? ORDER BY score DESC LIMIT 6', [mapName], (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
|
||||
if (leaderboardEntries.length > 0) {
|
||||
let rank = 0;
|
||||
leaderboardData.entries = leaderboardEntries.map(entry => {
|
||||
rank++;
|
||||
return {
|
||||
"__class": "LeaderboardEntry_Online",
|
||||
"profileId": entry.profileId,
|
||||
"score": entry.score,
|
||||
"name": entry.name || entry.username,
|
||||
"avatar": entry.avatar,
|
||||
"rank": rank,
|
||||
"country": entry.country,
|
||||
"platformId": entry.platformId,
|
||||
"alias": entry.alias,
|
||||
"aliasGender": entry.aliasGender,
|
||||
"jdPoints": entry.jdPoints,
|
||||
"portraitBorder": entry.portraitBorder,
|
||||
"mapName": mapName
|
||||
};
|
||||
});
|
||||
}
|
||||
res.json(leaderboardData);
|
||||
} catch (error) {
|
||||
console.error(`[LEADERBOARD] Error in handleRegularLeaderboard: ${error.message}`);
|
||||
res.status(500).send("Internal Server Error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle coop points requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleCoopPoints(req, res) {
|
||||
res.send(core.leaderboard);
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance of the route handler
|
||||
module.exports = new LeaderboardRouteHandler();
|
||||
90
core/classes/routes/RouteHandler.js
Normal file
90
core/classes/routes/RouteHandler.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Base RouteHandler class for OpenParty
|
||||
* All route handlers should extend this class
|
||||
*/
|
||||
const ErrorHandler = require('../ErrorHandler');
|
||||
const Logger = require('../../utils/logger');
|
||||
|
||||
class RouteHandler {
|
||||
/**
|
||||
* Create a new route handler
|
||||
* @param {string} name - The name of the route handler
|
||||
*/
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.logger = new Logger(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the routes
|
||||
* This method should be overridden by route handler implementations
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
// This method should be implemented by child classes
|
||||
this.logger.info(`initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a GET route with error handling
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {string} path - The route path
|
||||
* @param {Function} handler - The route handler function
|
||||
*/
|
||||
registerGet(app, path, handler) {
|
||||
app.get(path, this.wrapHandler(handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a POST route with error handling
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {string} path - The route path
|
||||
* @param {Function} handler - The route handler function
|
||||
*/
|
||||
registerPost(app, path, handler) {
|
||||
app.post(path, this.wrapHandler(handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a PUT route with error handling
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {string} path - The route path
|
||||
* @param {Function} handler - The route handler function
|
||||
*/
|
||||
registerPut(app, path, handler) {
|
||||
app.put(path, this.wrapHandler(handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a DELETE route with error handling
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {string} path - The route path
|
||||
* @param {Function} handler - The route handler function
|
||||
*/
|
||||
registerDelete(app, path, handler) {
|
||||
app.delete(path, this.wrapHandler(handler));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a route handler with error handling
|
||||
* @param {Function} handler - The route handler function
|
||||
* @returns {Function} The wrapped handler function
|
||||
*/
|
||||
wrapHandler(handler) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
await handler(req, res, next);
|
||||
} catch (error) {
|
||||
// Use the centralized error handler
|
||||
ErrorHandler.logError(this.name, error, {
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
routeHandler: this.name
|
||||
});
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RouteHandler;
|
||||
197
core/classes/routes/SongDBRouteHandler.js
Normal file
197
core/classes/routes/SongDBRouteHandler.js
Normal file
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* SongDB Route Handler for OpenParty
|
||||
* Handles song database and localization related routes
|
||||
*/
|
||||
const RouteHandler = require('./RouteHandler');
|
||||
const settings = require('../../../settings.json');
|
||||
const md5 = require('md5');
|
||||
const signer = require('../../lib/signUrl');
|
||||
const coreMain = require('../../var').main; // Assuming core.main is needed for songdb data
|
||||
|
||||
class SongDBRouteHandler extends RouteHandler {
|
||||
constructor() {
|
||||
super('SongDBRouteHandler');
|
||||
|
||||
// Bind handler methods to maintain 'this' context
|
||||
this.handleSongdb = this.handleSongdb.bind(this);
|
||||
this.handleSongdbV2 = this.handleSongdbV2.bind(this);
|
||||
this.handlePrivateSongdb = this.handlePrivateSongdb.bind(this);
|
||||
this.handleLocalisation = this.handleLocalisation.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is authorized
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
* @returns {boolean} Whether the request is authorized
|
||||
* @private
|
||||
*/
|
||||
checkAuth(req, res) {
|
||||
if (req.header('X-SkuId')) {
|
||||
if (!(req.header('X-SkuId').startsWith("jd") || req.header('X-SkuId').startsWith("JD")) || !req.headers["authorization"].startsWith("Ubi")) {
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'Bad request! Oops you didn\'t specify what file should we give you, try again'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'Oopsie! We can\'t check that ur Request is valid',
|
||||
'headder': req.headers
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
console.log(`[ROUTE] ${this.name} initializing routes...`);
|
||||
|
||||
// Song database routes
|
||||
this.registerGet(app, "/songdb/v1/songs", this.handleSongdb);
|
||||
this.registerGet(app, "/songdb/v2/songs", this.handleSongdbV2);
|
||||
this.registerGet(app, "/private/songdb/prod/:filename", this.handlePrivateSongdb);
|
||||
this.registerGet(app, "/songdb/v1/localisation", this.handleLocalisation);
|
||||
|
||||
console.log(`[ROUTE] ${this.name} routes initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle song database requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleSongdb(req, res) {
|
||||
if (!this.checkAuth(req, res)) return;
|
||||
|
||||
const skuId = req.header('X-SkuId') || "jd2022-pc-ww";
|
||||
const year = skuId.match(/jd(\d{4})/i)?.[1];
|
||||
const platform = skuId.match(/-(pc|durango|orbis|nx|wiiu)/i)?.[1];
|
||||
const isParty = skuId.startsWith("openparty");
|
||||
const isDreynMOD = skuId.startsWith("jd2023pc-next") || skuId.startsWith("jd2024pc-next");
|
||||
const isPCParty = skuId.startsWith("JD2021PC") || skuId.startsWith("jd2022-pc");
|
||||
|
||||
if (isParty && platform === 'pc') {
|
||||
res.send(coreMain.songdb['2017'].pcparty);
|
||||
} else if (isParty && platform === 'nx') {
|
||||
res.send(coreMain.songdb['2018'].nx);
|
||||
} else if (isDreynMOD) {
|
||||
res.send(coreMain.songdb['2017'].pcparty);
|
||||
} else if (isPCParty) {
|
||||
res.send(coreMain.songdb['2017'].pcparty);
|
||||
} else if (year && platform) {
|
||||
switch (year) {
|
||||
case '2017':
|
||||
if (platform === 'pc' || platform === 'durango' || platform === 'orbis') {
|
||||
res.send(coreMain.songdb['2017'].pc);
|
||||
} else if (platform === 'nx') {
|
||||
res.send(coreMain.songdb['2017'].nx);
|
||||
}
|
||||
break;
|
||||
case '2018':
|
||||
if (platform === 'nx') {
|
||||
res.send(coreMain.songdb['2018'].nx);
|
||||
} else if (platform === 'pc') {
|
||||
res.send(coreMain.songdb['2017'].pc);
|
||||
}
|
||||
break;
|
||||
case '2019':
|
||||
if (platform === 'nx') {
|
||||
res.send(coreMain.songdb['2019'].nx);
|
||||
} else if (platform === 'wiiu') {
|
||||
res.send(coreMain.songdb['2019'].wiiu);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.send('Invalid Game');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
res.send('Invalid Game');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle song database v2 requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleSongdbV2(req, res) {
|
||||
const sku = req.header('X-SkuId');
|
||||
const isHttps = settings.server.enableSSL ? "https" : "http";
|
||||
|
||||
// Parse the SKU ID
|
||||
const skuParts = sku ? sku.split('-') : [];
|
||||
const version = parseInt(skuParts[0].replace('jd', ''));
|
||||
const platform = skuParts[1];
|
||||
|
||||
// Generate URLs
|
||||
const songDBUrl = signer.generateSignedURL(`${isHttps}://${settings.server.domain}/private/songdb/prod/${sku}.${md5(JSON.stringify(coreMain.songdb[version][platform]))}.json`);
|
||||
const localizationDB = signer.generateSignedURL(`${isHttps}://${settings.server.domain}/private/songdb/prod/localisation.${md5(JSON.stringify(coreMain.localisation))}.json`);
|
||||
|
||||
// Send response
|
||||
res.send({
|
||||
"requestSpecificMaps": require('../../../database/data/db/requestSpecificMaps.json'),
|
||||
"localMaps": [],
|
||||
"songdbUrl": songDBUrl,
|
||||
"localisationUrl": localizationDB
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle private song database requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePrivateSongdb(req, res) {
|
||||
if (!signer.verifySignedURL(req.originalUrl)) {
|
||||
return res.status(401).send('Unauthorized');
|
||||
}
|
||||
|
||||
if (req.path.split('/')[4].startsWith('localisation')) {
|
||||
return res.send(coreMain.localisation);
|
||||
}
|
||||
|
||||
const filename = req.path.split('/')[4].split('.')[0];
|
||||
const sku = filename.split('.')[0];
|
||||
|
||||
if (!sku) {
|
||||
return res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'You are not permitted to receive a response'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse the SKU ID
|
||||
const skuParts = sku.split('-');
|
||||
const version = parseInt(skuParts[0].replace('jd', ''));
|
||||
const platform = skuParts[1];
|
||||
|
||||
if (coreMain.songdb[version] && coreMain.songdb[version][platform]) {
|
||||
res.send(coreMain.songdb[version][platform]);
|
||||
} else {
|
||||
res.status(404).send({
|
||||
'error': 404,
|
||||
'message': 'Song database not found for the given version and platform'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle localization requests
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleLocalisation(req, res) {
|
||||
res.send(coreMain.localisation);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SongDBRouteHandler();
|
||||
414
core/classes/routes/UbiservicesRouteHandler.js
Normal file
414
core/classes/routes/UbiservicesRouteHandler.js
Normal file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* Ubiservices Route Handler for OpenParty
|
||||
* Handles Ubisoft services related routes
|
||||
*/
|
||||
const axios = require('axios');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const RouteHandler = require('./RouteHandler'); // Assuming RouteHandler is in the same directory
|
||||
const AccountService = require('../../services/AccountService'); // Import AccountService directly
|
||||
|
||||
class UbiservicesRouteHandler extends RouteHandler {
|
||||
/**
|
||||
* Create a new ubiservices route handler
|
||||
*/
|
||||
constructor() {
|
||||
super('UbiservicesRouteHandler');
|
||||
|
||||
// Bind handler methods to maintain 'this' context
|
||||
this.handleConfiguration = this.handleConfiguration.bind(this);
|
||||
this.handleAppConfig = this.handleAppConfig.bind(this);
|
||||
this.handleSessions = this.handleSessions.bind(this);
|
||||
this.handleDeleteSessions = this.handleDeleteSessions.bind(this);
|
||||
this.handleGetProfiles = this.handleGetProfiles.bind(this);
|
||||
this.handleGetPopulations = this.handleGetPopulations.bind(this);
|
||||
this.handleGetParametersJD22 = this.handleGetParametersJD22.bind(this);
|
||||
this.handleGetParametersJD18 = this.handleGetParametersJD18.bind(this);
|
||||
this.handlePostUsers = this.handlePostUsers.bind(this);
|
||||
this.handleGetSpaceEntities = this.handleGetSpaceEntities.bind(this); // Bind new handler
|
||||
|
||||
// Initialize properties
|
||||
this.core = {
|
||||
main: require('../../var').main,
|
||||
CloneObject: require('../../helper').CloneObject,
|
||||
generateCarousel: require('../../carousel/carousel').generateCarousel,
|
||||
generateSweatCarousel: require('../../carousel/carousel').generateSweatCarousel,
|
||||
generateCoopCarousel: require('../../carousel/carousel').generateCoopCarousel,
|
||||
updateMostPlayed: require('../../carousel/carousel').updateMostPlayed,
|
||||
signer: require('../../lib/signUrl'),
|
||||
ipResolver: require('../../lib/ipResolver'),
|
||||
};
|
||||
|
||||
this.settings = require('../../../settings.json');
|
||||
this.cachedTicket = {}; // Cache for fake tickets
|
||||
this.ipCache = {}; // Cache to store session data based on IP
|
||||
this.prodwsurl = "https://public-ubiservices.ubi.com/";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
console.log(`[ROUTE] ${this.name} initializing routes...`);
|
||||
|
||||
// Serve application configuration
|
||||
this.registerGet(app, '/:version/applications/:appId/configuration', this.handleConfiguration);
|
||||
|
||||
// Serve alternative application configuration
|
||||
this.registerGet(app, '/:version/applications/:appId', this.handleAppConfig);
|
||||
|
||||
// Handle session creation
|
||||
this.registerPost(app, '/v3/profiles/sessions', this.handleSessions);
|
||||
|
||||
// Handle session deletion
|
||||
this.registerDelete(app, '/v3/profiles/sessions', this.handleDeleteSessions);
|
||||
|
||||
// spaceID
|
||||
this.registerGet(app, '/v2/spaces/:spaceId/entities', this.handleGetSpaceEntities);
|
||||
|
||||
// Retrieve profiles based on query parameters
|
||||
this.registerGet(app, '/v3/profiles', this.handleGetProfiles);
|
||||
|
||||
// Serve population data
|
||||
this.registerGet(app, '/v1/profiles/me/populations', this.handleGetPopulations);
|
||||
|
||||
// Serve application parameters for JD22
|
||||
this.registerGet(app, '/v1/applications/34ad0f04-b141-4793-bdd4-985a9175e70d/parameters', this.handleGetParametersJD22);
|
||||
|
||||
// Serve application parameters for JD18
|
||||
this.registerGet(app, '/v1/spaces/041c03fa-1735-4ea7-b5fc-c16546d092ca/parameters', this.handleGetParametersJD18);
|
||||
|
||||
// Handle user-related requests (stubbed for now)
|
||||
this.registerPost(app, '/v3/users/:user', this.handlePostUsers);
|
||||
|
||||
|
||||
console.log(`[ROUTE] ${this.name} routes initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle application configuration request
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleConfiguration(req, res) {
|
||||
res.send(this.core.main.configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle alternative application configuration request
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleAppConfig(req, res) {
|
||||
res.send(this.core.main.configurationnx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session creation
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleSessions(req, res) {
|
||||
const clientIp = this.getClientIp(req);
|
||||
const clientIpCountry = this.getCountryFromIp(clientIp);
|
||||
|
||||
// Remove Host header before forwarding to external service
|
||||
const headers = { ...req.headers };
|
||||
delete headers.host;
|
||||
|
||||
const customAuthData = this.parseCustomAuthHeader(headers.authorization);
|
||||
|
||||
if (customAuthData) {
|
||||
console.log("[ACC] CustomAuth detected, verifying...");
|
||||
|
||||
const { profileId, username, email, password } = customAuthData;
|
||||
const userData = AccountService.getUserData(profileId);
|
||||
const ticket = `CustomAuth${headers.authorization.split(" t=")[1]}`;
|
||||
|
||||
if (userData && userData.password && userData.email && userData.password === password) {
|
||||
console.log("[ACC] CustomAuth login: ", this.atob(username));
|
||||
AccountService.updateUser(profileId, { username: this.atob(username), nickname: this.atob(username), email, password, userId: profileId, ticket: `Ubi_v1 ${ticket}` });
|
||||
const sessionData = this.generateSessionData(profileId, this.atob(username), clientIp, clientIpCountry, ticket);
|
||||
res.send(sessionData);
|
||||
return;
|
||||
} else if (!userData || !userData.password || !userData.email) {
|
||||
console.log("[ACC] CustomAuth register: ", this.atob(username));
|
||||
AccountService.updateUser(profileId, { username: this.atob(username), nickname: this.atob(username), email, password, userId: profileId, ticket: `Ubi_v1 ${ticket}` });
|
||||
res.send(this.generateSessionData(profileId, this.atob(username), clientIp, clientIpCountry, ticket));
|
||||
return;
|
||||
} else {
|
||||
console.log("[ACC] CustomAuth login, Invalid Credentials: ", this.atob(username));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[ACC] Fetching Ticket From Official Server");
|
||||
const response = await axios.post(`${this.prodwsurl}/v3/profiles/sessions`, req.body, { headers });
|
||||
|
||||
res.send(response.data);
|
||||
console.log("[ACC] Using Official Ticket");
|
||||
|
||||
// Update user mappings
|
||||
AccountService.addUserId(response.data.profileId, response.data.userId);
|
||||
AccountService.updateUserTicket(response.data.profileId, `Ubi_v1 ${response.data.ticket}`);
|
||||
} catch (error) {
|
||||
console.log("[ACC] Error fetching from Ubisoft services", error.message);
|
||||
|
||||
if (this.ipCache[clientIp]) {
|
||||
console.log(`[ACC] Returning cached session for IP ${clientIp}`);
|
||||
return res.send(this.ipCache[clientIp]);
|
||||
}
|
||||
|
||||
const profileId = uuidv4();
|
||||
const userTicket = this.generateFalseTicket();
|
||||
this.cachedTicket[userTicket] = profileId; // Store fake ticket to profileId mapping
|
||||
|
||||
console.log("[ACC] Generating Fake Session for", profileId);
|
||||
|
||||
const fakeSession = this.generateSessionData(profileId, "NintendoSwitch", clientIp, clientIpCountry, userTicket);
|
||||
this.ipCache[clientIp] = fakeSession; // Cache the fake session for this IP
|
||||
|
||||
res.send(fakeSession);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle session deletion
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleDeleteSessions(req, res) {
|
||||
res.send(); // No specific action needed for deletion in this stub
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve profiles based on query parameters
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetProfiles(req, res) {
|
||||
// Use the cachedTicket to resolve profileId from Authorization header, or default
|
||||
const authHeader = req.header('Authorization');
|
||||
let profileIdFromTicket = 'UnknownTicket';
|
||||
if (authHeader && authHeader.startsWith('Ubi_v1 ')) {
|
||||
const ticket = authHeader.substring(7); // Remove 'Ubi_v1 ' prefix
|
||||
profileIdFromTicket = this.cachedTicket[ticket] || 'UnknownTicket';
|
||||
}
|
||||
|
||||
const profileId = `${profileIdFromTicket}/userId/${req.query.idOnPlatform}`;
|
||||
|
||||
res.send({
|
||||
profiles: [{
|
||||
profileId,
|
||||
userId: profileId,
|
||||
platformType: "uplay",
|
||||
idOnPlatform: profileId,
|
||||
nameOnPlatform: "Ryujinx"
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve population data
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetPopulations(req, res) {
|
||||
const spaceId = req.query.spaceIds || uuidv4();
|
||||
res.send({
|
||||
spaceId,
|
||||
data: {
|
||||
US_SDK_APPLICATION_BUILD_ID: "202007232022",
|
||||
US_SDK_DURABLES: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve application parameters for JD22
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetParametersJD22(req, res) {
|
||||
res.send(this.replaceDomainPlaceholder(require("../../database/config/v1/parameters.json"), this.settings.server.domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve application parameters for JD18
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetParametersJD18(req, res) {
|
||||
res.send(this.replaceDomainPlaceholder(require("../../database/config/v1/parameters2.json"), this.settings.server.domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user-related requests (stubbed for now)
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostUsers(req, res) {
|
||||
res.send(); // No specific action needed for this stubbed endpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET requests for space entities.
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetSpaceEntities(req, res) {
|
||||
res.send(this.core.main.entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace domain placeholders in an object
|
||||
* @param {Object|Array|string} obj - The object to process
|
||||
* @param {string} domain - The domain to replace with
|
||||
* @returns {Object|Array|string} The processed object
|
||||
* @private
|
||||
*/
|
||||
replaceDomainPlaceholder(obj, domain) {
|
||||
if (typeof obj === 'string') {
|
||||
return obj.replace('{SettingServerDomainVarOJDP}', domain);
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(item => this.replaceDomainPlaceholder(item, domain));
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((acc, key) => {
|
||||
acc[key] = this.replaceDomainPlaceholder(obj[key], domain);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client IP from request
|
||||
* @param {Request} req - The request object
|
||||
* @returns {string} Client IP address
|
||||
* @private
|
||||
*/
|
||||
getClientIp(req) {
|
||||
return this.core.ipResolver.getClientIp(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country from IP address
|
||||
* @param {string} ip - IP address
|
||||
* @returns {string} Country code
|
||||
* @private
|
||||
*/
|
||||
getCountryFromIp(ip) {
|
||||
// Placeholder function - in a real app, you'd use a geolocation service
|
||||
return 'US';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a fake ticket for fallback
|
||||
* @returns {string} Fake ticket
|
||||
* @private
|
||||
*/
|
||||
generateFalseTicket() {
|
||||
const start = "ew0KIC";
|
||||
const end = "KfQ==";
|
||||
const middleLength = 60;
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let middle = '';
|
||||
|
||||
for (let i = 0; i < middleLength; i++) {
|
||||
middle += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return start + middle + end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse custom authorization header
|
||||
* @param {string} authorization - Authorization header
|
||||
* @returns {Object|null} Parsed authorization data or null if invalid
|
||||
* @private
|
||||
*/
|
||||
parseCustomAuthHeader(authorization) {
|
||||
if (!authorization) return null; // Handle undefined or null authorization header
|
||||
|
||||
const [method, encoded] = authorization.split(" ");
|
||||
if (method !== "uplaypc_v1" || !encoded || !encoded.includes("t=")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the part after the first "t="
|
||||
const encodedPart = encoded.substring(encoded.indexOf("t=") + 2);
|
||||
|
||||
// Decode Base64 after "t="
|
||||
let decoded;
|
||||
try {
|
||||
decoded = this.atob(encodedPart);
|
||||
} catch (e) {
|
||||
console.error("Error decoding custom auth header:", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = decoded.split(":");
|
||||
// Ensure we have enough parts for the expected structure
|
||||
if (parts.length < 6) {
|
||||
console.error("Custom auth header has too few parts:", decoded);
|
||||
return null;
|
||||
}
|
||||
|
||||
const [tag, encodedProfileId, profileId, username, email, encodedPassword] = parts;
|
||||
|
||||
if (tag !== "JDParty") return null;
|
||||
|
||||
return {
|
||||
profileId,
|
||||
username,
|
||||
email,
|
||||
password: this.atob(encodedPassword),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode Base64 string
|
||||
* @param {string} base64 - Base64 encoded string
|
||||
* @returns {string} Decoded string
|
||||
* @private
|
||||
*/
|
||||
atob(base64) {
|
||||
return Buffer.from(base64, 'base64').toString('utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate session data
|
||||
* @param {string} profileId - Profile ID
|
||||
* @param {string} username - Username
|
||||
* @param {string} clientIp - Client IP address
|
||||
* @param {string} clientIpCountry - Client country
|
||||
* @param {string} ticket - Session ticket
|
||||
* @returns {Object} Session data
|
||||
* @private
|
||||
*/
|
||||
generateSessionData(profileId, username, clientIp, clientIpCountry, ticket) {
|
||||
const now = new Date();
|
||||
const expiration = new Date(now.getTime() + 3 * 60 * 60 * 1000); // 3 hours as in original
|
||||
|
||||
return {
|
||||
platformType: "uplay",
|
||||
ticket,
|
||||
twoFactorAuthenticationTicket: null,
|
||||
profileId,
|
||||
userId: profileId,
|
||||
nameOnPlatform: username,
|
||||
environment: "Prod",
|
||||
expiration: expiration.toISOString(),
|
||||
spaceId: uuidv4(), // Original had uuidv4 here, refactored had it missing
|
||||
clientIp,
|
||||
clientIpCountry,
|
||||
serverTime: now.toISOString(),
|
||||
sessionId: uuidv4(),
|
||||
sessionKey: "TqCz5+J0w9e8qpLp/PLr9BCfAc30hKlEJbN0Xr+mbZa=", // Fixed string as in original
|
||||
rememberMeTicket: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance of the route handler
|
||||
module.exports = new UbiservicesRouteHandler();
|
||||
46
core/core.js
46
core/core.js
@@ -1,46 +0,0 @@
|
||||
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');
|
||||
//idk what happened
|
||||
});
|
||||
|
||||
//initialize route module
|
||||
modules.forEach((item) => {
|
||||
if(item.execution == "pre-load")
|
||||
require(resolvePath(item.path)).initroute(app);
|
||||
})
|
||||
|
||||
require('./route/rdefault').initroute(app);
|
||||
require('./route/account').initroute(app);
|
||||
require('./route/leaderboard').initroute(app);
|
||||
require('./route/ubiservices').initroute(app);
|
||||
|
||||
modules.forEach((item) => {
|
||||
if(item.execution == "init")
|
||||
require(resolvePath(item.path)).initroute(app);
|
||||
})
|
||||
|
||||
//hide error when prod
|
||||
app.get('*', function(req, res){
|
||||
res.status(404).send({
|
||||
'error': 404,
|
||||
'message': 'Path Not Recognized'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
main, init
|
||||
}
|
||||
138
core/database/DatabaseManager.js
Normal file
138
core/database/DatabaseManager.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const { getSavefilePath } = require('../helper');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
const DB_PATH = path.join(getSavefilePath(), 'openparty.db');
|
||||
|
||||
class DatabaseManager {
|
||||
constructor() {
|
||||
this.logger = new Logger('DatabaseManager');
|
||||
if (DatabaseManager._instance) {
|
||||
this.logger.info('Returning existing instance.');
|
||||
return DatabaseManager._instance;
|
||||
}
|
||||
this._db = null;
|
||||
DatabaseManager._instance = this;
|
||||
this.logger.info('New instance created.');
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!DatabaseManager._instance) {
|
||||
DatabaseManager._instance = new DatabaseManager();
|
||||
}
|
||||
return DatabaseManager._instance;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (this._db) {
|
||||
this.logger.info('Database already initialized (this._db is set).');
|
||||
return Promise.resolve(this._db);
|
||||
}
|
||||
|
||||
this.logger.info('Starting database initialization...');
|
||||
return new Promise((resolve, reject) => {
|
||||
this._db = new sqlite3.Database(DB_PATH, (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error connecting to database:', err.message);
|
||||
this._db = null;
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info('Connected to the SQLite database. this._db is now set.');
|
||||
this._db.serialize(() => {
|
||||
// Create most_played table
|
||||
this._db.run(`CREATE TABLE IF NOT EXISTS most_played (
|
||||
mapName TEXT PRIMARY KEY,
|
||||
playCount INTEGER DEFAULT 0
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error creating most_played table:', err.message);
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Create leaderboard table
|
||||
this._db.run(`CREATE TABLE IF NOT EXISTS leaderboard (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mapName TEXT NOT NULL,
|
||||
profileId TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
score INTEGER NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
UNIQUE(mapName, profileId)
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error creating leaderboard table:', err.message);
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Create dotw (Dance of the Week) table
|
||||
this._db.run(`CREATE TABLE IF NOT EXISTS dotw (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mapName TEXT NOT NULL,
|
||||
profileId TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
score INTEGER NOT NULL,
|
||||
timestamp TEXT NOT NULL,
|
||||
weekNumber INTEGER NOT NULL,
|
||||
gameVersion TEXT,
|
||||
rank INTEGER,
|
||||
name TEXT,
|
||||
avatar TEXT,
|
||||
country TEXT,
|
||||
platformId TEXT,
|
||||
alias TEXT,
|
||||
aliasGender INTEGER,
|
||||
jdPoints INTEGER,
|
||||
portraitBorder TEXT,
|
||||
UNIQUE(mapName, profileId, weekNumber)
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error creating dotw table:', err.message);
|
||||
return reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Create user_profiles table
|
||||
this._db.run(`CREATE TABLE IF NOT EXISTS user_profiles (
|
||||
profileId TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
alias TEXT,
|
||||
aliasGender INTEGER,
|
||||
avatar TEXT,
|
||||
country TEXT,
|
||||
createdAt TEXT,
|
||||
ticket TEXT,
|
||||
platformId TEXT,
|
||||
jdPoints INTEGER,
|
||||
portraitBorder TEXT,
|
||||
-- Store scores and songsPlayed as JSON strings
|
||||
scores TEXT,
|
||||
songsPlayed TEXT,
|
||||
favorites TEXT
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error creating user_profiles table:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info('All tables created. Resolving initialize promise.');
|
||||
resolve(this._db);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getDb() {
|
||||
this.logger.info(`getDb() called. this._db is: ${this._db ? 'set' : 'null'}`);
|
||||
if (!this._db) {
|
||||
throw new Error('DatabaseManager: Database not initialized. Call initialize() first.');
|
||||
}
|
||||
return this._db;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DatabaseManager;
|
||||
14
core/database/sqlite.js
Normal file
14
core/database/sqlite.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const DatabaseManager = require('./DatabaseManager'); // Import the DatabaseManager class
|
||||
|
||||
async function initializeDatabase() {
|
||||
return DatabaseManager.getInstance().initialize();
|
||||
}
|
||||
|
||||
function getDb() {
|
||||
return DatabaseManager.getInstance().getDb();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDatabase,
|
||||
getDb
|
||||
};
|
||||
@@ -4,14 +4,14 @@ const axios = require('axios');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const settings = require('../settings.json');
|
||||
const Logger = require('./utils/logger');
|
||||
const logger = new Logger('HELPER');
|
||||
|
||||
const downloader = {
|
||||
};
|
||||
function CloneObject(ObjectC) {
|
||||
return JSON.parse(JSON.stringify(ObjectC))
|
||||
}
|
||||
function readDatabaseJson(path) {
|
||||
return JSON.parse(fs.readFileSync(`${__dirname}/../database/${path}`, 'utf8'));
|
||||
}
|
||||
var donotlog = {}
|
||||
|
||||
downloader.getJson = async (url, options) => {
|
||||
@@ -50,12 +50,12 @@ function getSavefilePath() {
|
||||
const ensureDirectoryExists = (dirPath) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
console.log(`[HELPER] Created directory: ${dirPath}`);
|
||||
logger.info(`Created directory: ${dirPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
const baseDir = getSavefilePath();
|
||||
console.log(`[HELPER] Checking SaveData dir`);
|
||||
logger.info(`Checking SaveData dir`);
|
||||
|
||||
// Pastikan direktori utama ada
|
||||
ensureDirectoryExists(baseDir);
|
||||
@@ -82,14 +82,14 @@ function loadJsonFile(layeredPath, originalPath) {
|
||||
return require(savedDataPath);
|
||||
} else {
|
||||
if (!donotlog[path.basename(savedDataPath)]) {
|
||||
console.log(`[HELPER] Serving ${path.basename(savedDataPath)} from Static Database`)
|
||||
logger.info(`Serving ${path.basename(savedDataPath)} from Static Database`)
|
||||
donotlog[path.basename(savedDataPath)] = true
|
||||
}
|
||||
return require(originalPath);
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePath(input=""){
|
||||
function resolvePath(input = "") {
|
||||
var new_input = input.replace('{dirname}', process.cwd())
|
||||
new_input = new_input.replace('{Home}', os.homedir())
|
||||
return new_input
|
||||
@@ -97,5 +97,5 @@ function resolvePath(input=""){
|
||||
|
||||
|
||||
module.exports = {
|
||||
CloneObject, readDatabaseJson, downloader, extractSkuIdInfo, getSavefilePath, loadJsonFile, resolvePath
|
||||
}
|
||||
CloneObject, downloader, extractSkuIdInfo, getSavefilePath, loadJsonFile, resolvePath
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { loadJsonFile } = require('../helper')
|
||||
const inputJson = loadJsonFile('carousel/playlist.json', '../database/carousel/playlist.json');
|
||||
const path = require('path');
|
||||
const inputJson = loadJsonFile('carousel/playlist.json', '../database/data/carousel/playlist.json');
|
||||
const ClassList = require('./playlist-Class.json')
|
||||
const WEEKLY_PLAYLIST_PREFIX = 'DFRecommendedFU';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const crypto = require('crypto');
|
||||
const settings = require('../../settings.json')
|
||||
const secretKey = require('../../database/encryption.json').encrpytion.secretKey;
|
||||
const secretKey = require('../../database/config/encryption.json').encrpytion.secretKey;
|
||||
|
||||
exports.generateSignedURL = (originalURL) => {
|
||||
// Set expiration time (in seconds)
|
||||
|
||||
@@ -2,6 +2,8 @@ const settings = require('../../settings.json');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { loadJsonFile } = require('../helper');
|
||||
const Logger = require('../utils/logger');
|
||||
const logger = new Logger('SONGDB');
|
||||
|
||||
const songdbF = {};
|
||||
const main = {
|
||||
@@ -143,9 +145,9 @@ songdbF.generate = function () {
|
||||
};
|
||||
|
||||
songdbF.generateSonglist = function () {
|
||||
console.log(`[SONGDB] Processing Songdbs`)
|
||||
logger.info(`Processing Songdbs`)
|
||||
songdbF.generate()
|
||||
console.log(`[SONGDB] ${Object.keys(main.songdb[2017].pc).length} Maps Loaded`)
|
||||
logger.info(`${Object.keys(main.songdb[2017].pc).length} Maps Loaded`)
|
||||
return main.songdb
|
||||
}
|
||||
|
||||
|
||||
119
core/models/Account.js
Normal file
119
core/models/Account.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Account model representing a user account
|
||||
*/
|
||||
class Account {
|
||||
/**
|
||||
* Create a new Account instance
|
||||
* @param {Object} data Initial account data
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
this.profileId = data.profileId || null;
|
||||
this.userId = data.userId || null;
|
||||
this.username = data.username || null;
|
||||
this.nickname = data.nickname || null;
|
||||
this.name = data.name || null;
|
||||
this.email = data.email || null;
|
||||
this.password = data.password || null;
|
||||
this.ticket = data.ticket || null;
|
||||
this.avatar = data.avatar || null;
|
||||
this.country = data.country || null;
|
||||
this.platformId = data.platformId || null;
|
||||
this.alias = data.alias || null;
|
||||
this.aliasGender = data.aliasGender || null;
|
||||
this.jdPoints = data.jdPoints || 0;
|
||||
this.portraitBorder = data.portraitBorder || null;
|
||||
this.rank = data.rank || 0;
|
||||
this.scores = data.scores || {}; // Map of mapName to score data
|
||||
this.favorites = data.favorites || {}; // User's favorite maps
|
||||
this.createdAt = data.createdAt || new Date().toISOString();
|
||||
this.updatedAt = data.updatedAt || new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update account properties
|
||||
* @param {Object} data Data to update
|
||||
* @returns {Account} Updated account instance
|
||||
*/
|
||||
update(data) {
|
||||
Object.assign(this, data);
|
||||
this.updatedAt = new Date().toISOString();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a score for a specific map
|
||||
* @param {string} mapName The map name
|
||||
* @param {Object} scoreData Score data to save
|
||||
*/
|
||||
updateScore(mapName, scoreData) {
|
||||
if (!this.scores) {
|
||||
this.scores = {};
|
||||
}
|
||||
|
||||
this.scores[mapName] = {
|
||||
...this.scores[mapName],
|
||||
...scoreData,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a map to favorites
|
||||
* @param {string} mapName The map name to favorite
|
||||
*/
|
||||
addFavorite(mapName) {
|
||||
if (!this.favorites) {
|
||||
this.favorites = {};
|
||||
}
|
||||
|
||||
this.favorites[mapName] = {
|
||||
addedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a map from favorites
|
||||
* @param {string} mapName The map name to remove from favorites
|
||||
*/
|
||||
removeFavorite(mapName) {
|
||||
if (this.favorites && this.favorites[mapName]) {
|
||||
delete this.favorites[mapName];
|
||||
this.updatedAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to plain object for storage
|
||||
* @returns {Object} Plain object representation
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
profileId: this.profileId,
|
||||
userId: this.userId,
|
||||
username: this.username,
|
||||
nickname: this.nickname,
|
||||
name: this.name,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
ticket: this.ticket,
|
||||
avatar: this.avatar,
|
||||
country: this.country,
|
||||
platformId: this.platformId,
|
||||
alias: this.alias,
|
||||
aliasGender: this.aliasGender,
|
||||
jdPoints: this.jdPoints,
|
||||
portraitBorder: this.portraitBorder,
|
||||
rank: this.rank,
|
||||
scores: this.scores,
|
||||
favorites: this.favorites,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Account;
|
||||
59
core/models/MostPlayed.js
Normal file
59
core/models/MostPlayed.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* MostPlayed Model
|
||||
* Represents the most played songs data, structured by week.
|
||||
*/
|
||||
class MostPlayed {
|
||||
/**
|
||||
* Create a MostPlayed instance.
|
||||
* @param {Object} data - The raw data from mostplayed.json.
|
||||
*/
|
||||
constructor(data = {}) {
|
||||
this.data = data; // Stores the entire mostPlayed object { weekNumber: { mapName: count } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the play count for a specific map in a given week.
|
||||
* If the week or map does not exist, it will be initialized.
|
||||
* @param {number} weekNumber - The week number.
|
||||
* @param {string} mapName - The name of the map.
|
||||
*/
|
||||
incrementPlayCount(weekNumber, mapName) {
|
||||
if (!this.data[weekNumber]) {
|
||||
this.data[weekNumber] = {};
|
||||
}
|
||||
this.data[weekNumber][mapName] = (this.data[weekNumber][mapName] || 0) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the play count for a specific map in a given week.
|
||||
* @param {number} weekNumber - The week number.
|
||||
* @param {string} mapName - The name of the map.
|
||||
* @returns {number} The play count, or 0 if not found.
|
||||
*/
|
||||
getPlayCount(weekNumber, mapName) {
|
||||
return (this.data[weekNumber] && this.data[weekNumber][mapName]) || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all most played songs for a given week, sorted by play count.
|
||||
* @param {number} weekNumber - The week number.
|
||||
* @returns {Array<Array<string, number>>} An array of [mapName, count] pairs, sorted descending by count.
|
||||
*/
|
||||
getSongsForWeek(weekNumber) {
|
||||
if (!this.data[weekNumber]) {
|
||||
return [];
|
||||
}
|
||||
return Object.entries(this.data[weekNumber])
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the MostPlayed instance to a plain JavaScript object.
|
||||
* @returns {Object} A plain object representation of the most played data.
|
||||
*/
|
||||
toJSON() {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MostPlayed;
|
||||
36
core/models/Song.js
Normal file
36
core/models/Song.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Song Model
|
||||
* Represents a single song entry in the song database.
|
||||
*/
|
||||
class Song {
|
||||
/**
|
||||
* Create a Song instance.
|
||||
* @param {Object} data - The raw song data from songdbs.json.
|
||||
*/
|
||||
constructor(data) {
|
||||
this.mapName = data.mapName;
|
||||
this.title = data.title;
|
||||
this.artist = data.artist;
|
||||
this.originalJDVersion = data.originalJDVersion;
|
||||
this.tags = data.tags || [];
|
||||
// Add other properties as needed from the songdb structure
|
||||
this.data = data; // Store original data for flexibility
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the Song instance to a plain JavaScript object.
|
||||
* @returns {Object} A plain object representation of the song.
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
mapName: this.mapName,
|
||||
title: this.title,
|
||||
artist: this.artist,
|
||||
originalJDVersion: this.originalJDVersion,
|
||||
tags: this.tags,
|
||||
...this.data // Include all original data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Song;
|
||||
278
core/repositories/AccountRepository.js
Normal file
278
core/repositories/AccountRepository.js
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* AccountRepository handles data persistence for Account models
|
||||
*/
|
||||
const { getDb } = require('../database/sqlite');
|
||||
const Account = require('../models/Account');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class AccountRepository {
|
||||
/**
|
||||
* Create a new AccountRepository instance
|
||||
*/
|
||||
constructor() {
|
||||
// No need for file paths or secret key with SQLite
|
||||
this.logger = new Logger('AccountRepository');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all accounts from storage
|
||||
* @returns {Promise<Object>} Promise resolving to a Map of profileId to Account instances
|
||||
*/
|
||||
async loadAll() {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT * FROM user_profiles', [], (err, rows) => {
|
||||
if (err) {
|
||||
this.logger.error('Error loading accounts from DB:', err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
const accounts = {};
|
||||
rows.forEach(row => {
|
||||
try {
|
||||
const accountData = {
|
||||
profileId: row.profileId,
|
||||
name: row.name,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
};
|
||||
accounts[row.profileId] = new Account(accountData);
|
||||
} catch (parseError) {
|
||||
this.logger.error(`Error parsing JSON for profile ${row.profileId}:`, parseError.message);
|
||||
// Skip this row or handle error as appropriate
|
||||
}
|
||||
});
|
||||
this.logger.info(`Loaded ${Object.keys(accounts).length} accounts from DB.`);
|
||||
resolve(accounts);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an account to storage (insert or update)
|
||||
* @param {Account} account The account instance to save
|
||||
* @returns {Promise<Account>} Promise resolving to the saved account
|
||||
*/
|
||||
async save(account) {
|
||||
if (!account.profileId) {
|
||||
throw new Error('Cannot save account without profileId');
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const accountData = account.toJSON(); // Get plain object representation
|
||||
|
||||
// Stringify JSON fields
|
||||
const scoresJson = JSON.stringify(accountData.scores || {});
|
||||
const songsPlayedJson = JSON.stringify(accountData.songsPlayed || []);
|
||||
const favoritesJson = JSON.stringify(accountData.favorites || []);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(`INSERT OR REPLACE INTO user_profiles (
|
||||
profileId, name, alias, aliasGender, avatar, country, createdAt, ticket,
|
||||
platformId, jdPoints, portraitBorder, scores, songsPlayed, favorites
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
accountData.profileId,
|
||||
accountData.name,
|
||||
accountData.alias,
|
||||
accountData.aliasGender,
|
||||
accountData.avatar,
|
||||
accountData.country,
|
||||
accountData.createdAt,
|
||||
accountData.ticket,
|
||||
accountData.platformId,
|
||||
accountData.jdPoints,
|
||||
accountData.portraitBorder,
|
||||
scoresJson,
|
||||
songsPlayedJson,
|
||||
favoritesJson
|
||||
],
|
||||
function(err) {
|
||||
if (err) {
|
||||
this.logger.error(`Error saving account ${account.profileId} to DB:`, err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info(`Saved account ${account.profileId} to DB.`);
|
||||
resolve(account);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an account by profileId
|
||||
* @param {string} profileId The profile ID
|
||||
* @returns {Promise<Account|null>} Promise resolving to the account or null if not found
|
||||
*/
|
||||
async findById(profileId) {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM user_profiles WHERE profileId = ?', [profileId], (err, row) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error finding account by ID ${profileId}:`, err.message);
|
||||
reject(err);
|
||||
} else if (row) {
|
||||
try {
|
||||
const accountData = {
|
||||
profileId: row.profileId,
|
||||
name: row.name,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
};
|
||||
resolve(new Account(accountData));
|
||||
} catch (parseError) {
|
||||
this.logger.error(`Error parsing JSON for profile ${row.profileId}:`, parseError.message);
|
||||
resolve(null); // Return null if parsing fails
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an account by ticket
|
||||
* @param {string} ticket The ticket to search for
|
||||
* @returns {Promise<Account|null>} Promise resolving to the account or null if not found
|
||||
*/
|
||||
async findByTicket(ticket) {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM user_profiles WHERE ticket = ?', [ticket], (err, row) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error finding account by ticket ${ticket}:`, err.message);
|
||||
reject(err);
|
||||
} else if (row) {
|
||||
try {
|
||||
const accountData = {
|
||||
profileId: row.profileId,
|
||||
name: row.name,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
};
|
||||
resolve(new Account(accountData));
|
||||
} catch (parseError) {
|
||||
this.logger.error(`Error parsing JSON for profile ${row.profileId}:`, parseError.message);
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an account by nickname
|
||||
* @param {string} nickname The nickname to search for
|
||||
* @returns {Promise<Account|null>} Promise resolving to the account or null if not found
|
||||
*/
|
||||
async findByNickname(nickname) {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT * FROM user_profiles WHERE name = ?', [nickname], (err, row) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error finding account by nickname ${nickname}:`, err.message);
|
||||
reject(err);
|
||||
} else if (row) {
|
||||
try {
|
||||
const accountData = {
|
||||
profileId: row.profileId,
|
||||
name: row.name,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
};
|
||||
resolve(new Account(accountData));
|
||||
} catch (parseError) {
|
||||
this.logger.error(`Error parsing JSON for profile ${row.profileId}:`, parseError.message);
|
||||
resolve(null);
|
||||
}
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an account
|
||||
* @param {string} profileId The profile ID to delete
|
||||
* @returns {Promise<boolean>} Promise resolving to success status
|
||||
*/
|
||||
async delete(profileId) {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM user_profiles WHERE profileId = ?', [profileId], function(err) {
|
||||
if (err) {
|
||||
this.logger.error(`Error deleting account ${profileId} from DB:`, err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info(`Deleted account ${profileId} from DB.`);
|
||||
resolve(this.changes > 0); // True if a row was deleted
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an account with new data
|
||||
* @param {string} profileId The profile ID
|
||||
* @param {Object} data The data to update
|
||||
* @returns {Promise<Account|null>} Promise resolving to the updated account or null if not found
|
||||
*/
|
||||
async update(profileId, data) {
|
||||
const existingAccount = await this.findById(profileId);
|
||||
|
||||
if (!existingAccount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
existingAccount.update(data);
|
||||
await this.save(existingAccount); // Use the async save method
|
||||
|
||||
return existingAccount;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AccountRepository(); // Export a singleton instance
|
||||
95
core/repositories/MostPlayedRepository.js
Normal file
95
core/repositories/MostPlayedRepository.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* MostPlayed Repository
|
||||
* Handles loading and saving most played songs data.
|
||||
*/
|
||||
const { getDb } = require('../database/sqlite');
|
||||
const MostPlayed = require('../models/MostPlayed');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class MostPlayedRepository {
|
||||
constructor() {
|
||||
// No need to load data in constructor, will be fetched from DB on demand
|
||||
this.logger = new Logger('MostPlayedRepository');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load most played data from the SQLite database.
|
||||
* @returns {Promise<MostPlayed>} A promise that resolves to the MostPlayed instance.
|
||||
*/
|
||||
async load() {
|
||||
this.logger.info('Loading most played data from DB...');
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all('SELECT mapName, playCount FROM most_played', [], (err, rows) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error reading most_played from DB: ${err.message}`);
|
||||
reject(err);
|
||||
} else {
|
||||
const data = {};
|
||||
rows.forEach(row => {
|
||||
data[row.mapName] = row.playCount;
|
||||
});
|
||||
this.logger.info('Most played data loaded from DB.');
|
||||
resolve(new MostPlayed(data));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current most played data to the SQLite database.
|
||||
* @param {MostPlayed} mostPlayedInstance - The MostPlayed instance to save.
|
||||
* @returns {Promise<void>} A promise that resolves when data is saved.
|
||||
*/
|
||||
async save(mostPlayedInstance) {
|
||||
this.logger.info('Saving most played data to DB...');
|
||||
const db = getDb();
|
||||
const data = mostPlayedInstance.toJSON();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.serialize(() => {
|
||||
db.run('BEGIN TRANSACTION;');
|
||||
const stmt = db.prepare(`INSERT OR REPLACE INTO most_played (mapName, playCount) VALUES (?, ?)`);
|
||||
|
||||
for (const mapName in data) {
|
||||
stmt.run(mapName, data[mapName], (err) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error saving ${mapName}: ${err.message}`);
|
||||
db.run('ROLLBACK;');
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stmt.finalize((err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error finalizing statement:', err.message);
|
||||
db.run('ROLLBACK;');
|
||||
reject(err);
|
||||
} else {
|
||||
db.run('COMMIT;', (commitErr) => {
|
||||
if (commitErr) {
|
||||
this.logger.error('Error committing transaction:', commitErr.message);
|
||||
reject(commitErr);
|
||||
} else {
|
||||
this.logger.info('Most played data saved to DB.');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current MostPlayed instance from the database.
|
||||
* @returns {Promise<MostPlayed>} A promise that resolves to the MostPlayed instance.
|
||||
*/
|
||||
async getMostPlayed() {
|
||||
return this.load();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MostPlayedRepository(); // Export a singleton instance
|
||||
59
core/repositories/SongRepository.js
Normal file
59
core/repositories/SongRepository.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Song Repository
|
||||
* Handles loading and providing access to song data.
|
||||
*/
|
||||
const path = require('path');
|
||||
const { loadJsonFile } = require('../helper'); // Assuming helper has loadJsonFile
|
||||
const Song = require('../models/Song');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class SongRepository {
|
||||
constructor() {
|
||||
this.songs = {};
|
||||
this.logger = new Logger('SongRepository');
|
||||
this.loadSongs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all songs from the songdbs.json file.
|
||||
* @private
|
||||
*/
|
||||
loadSongs() {
|
||||
this.logger.info('Loading songs from songdbs.json...');
|
||||
const songdbData = loadJsonFile('Platforms/openparty-all/songdbs.json', '../database/Platforms/openparty-all/songdbs.json');
|
||||
|
||||
for (const mapName in songdbData) {
|
||||
if (songdbData.hasOwnProperty(mapName)) {
|
||||
this.songs[mapName] = new Song(songdbData[mapName]);
|
||||
}
|
||||
}
|
||||
this.logger.info(`Loaded ${Object.keys(this.songs).length} songs.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a song by its mapName.
|
||||
* @param {string} mapName - The mapName of the song.
|
||||
* @returns {Song|undefined} The Song instance or undefined if not found.
|
||||
*/
|
||||
findByMapName(mapName) {
|
||||
return this.songs[mapName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all songs.
|
||||
* @returns {Object<string, Song>} An object containing all Song instances, keyed by mapName.
|
||||
*/
|
||||
getAllSongs() {
|
||||
return this.songs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all song mapNames.
|
||||
* @returns {string[]} An array of all song mapNames.
|
||||
*/
|
||||
getAllMapNames() {
|
||||
return Object.keys(this.songs);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SongRepository(); // Export a singleton instance
|
||||
@@ -1,471 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const axios = require("axios");
|
||||
const path = require('path');
|
||||
const { getSavefilePath } = require('../helper');
|
||||
const { encrypt, decrypt } = require('../lib/encryptor');
|
||||
const { updateMostPlayed } = require('../carousel/carousel');
|
||||
|
||||
const secretKey = require('../../database/encryption.json').encrpytion.userEncrypt;
|
||||
const ubiwsurl = "https://public-ubiservices.ubi.com";
|
||||
const prodwsurl = "https://prod.just-dance.com";
|
||||
let decryptedData;
|
||||
let cachedLeaderboard;
|
||||
let cachedDotw;
|
||||
|
||||
const LEADERBOARD_PATH = path.join(getSavefilePath(), 'leaderboard/leaderboard.json');
|
||||
const DOTW_PATH = path.join(getSavefilePath(), 'leaderboard/dotw.json');
|
||||
|
||||
// Helper function to load user data
|
||||
function loadUserData(dataFilePath) {
|
||||
if (!decryptedData) { // Load data from disk only if not already in memory
|
||||
try {
|
||||
const encryptedData = fs.readFileSync(dataFilePath, 'utf8');
|
||||
decryptedData = JSON.parse(decrypt(encryptedData, secretKey));
|
||||
console.log('[ACC] User Data Loaded, Total: ', Object.keys(decryptedData).length);
|
||||
} catch (err) {
|
||||
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 is the first run');
|
||||
console.log('[ACC] Resetting All User Data...');
|
||||
console.log(err);
|
||||
decryptedData = {}; // Initialize as an empty object if file read fails
|
||||
}
|
||||
}
|
||||
return decryptedData;
|
||||
}
|
||||
|
||||
function 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 function to save user data
|
||||
function saveUserData(dataFilePath, data) {
|
||||
const encryptedUserProfiles = encrypt(JSON.stringify(data), secretKey);
|
||||
fs.writeFileSync(dataFilePath, encryptedUserProfiles);
|
||||
}
|
||||
|
||||
// Find user by ticket
|
||||
function findUserFromTicket(ticket) {
|
||||
const matchedProfileId = Object.keys(decryptedData).find(profileId => {
|
||||
const userProfile = decryptedData[profileId];
|
||||
return userProfile.ticket === ticket && userProfile.name;
|
||||
});
|
||||
return matchedProfileId
|
||||
}
|
||||
|
||||
// Find user by nickname
|
||||
function findUserFromNickname(nickname) {
|
||||
return Object.values(decryptedData).find(profile => profile.name === nickname);
|
||||
}
|
||||
|
||||
const getGameVersion = (req) => {
|
||||
const sku = req.header('X-SkuId') || "jd2019-pc-ww";
|
||||
return sku.substring(0, 6) || "jd2019";
|
||||
};
|
||||
|
||||
// Add a new user
|
||||
function addUser(profileId, userProfile) {
|
||||
decryptedData[profileId] = userProfile;
|
||||
console.log(`[ACC] Added User With UUID: `, profileId);
|
||||
const dataFilePath = path.join(getSavefilePath(), `/account/profiles/user.json`);
|
||||
saveUserData(dataFilePath, decryptedData);
|
||||
}
|
||||
// Add a new user
|
||||
function addUserId(profileId, userId) {
|
||||
if (decryptedData[profileId]) {
|
||||
decryptedData[profileId].userId = userId;
|
||||
const dataFilePath = path.join(getSavefilePath(), `/account/profiles/user.json`);
|
||||
saveUserData(dataFilePath, decryptedData);
|
||||
} else {
|
||||
console.log(`[ACC] User ${profileId} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserTicket(profileId, Ticket) {
|
||||
decryptedData[profileId.ticket] = Ticket;
|
||||
const dataFilePath = path.join(getSavefilePath(), `/account/profiles/user.json`);
|
||||
saveUserData(dataFilePath, decryptedData);
|
||||
}
|
||||
|
||||
// Update or override user data
|
||||
function updateUser(profileId, userProfile) {
|
||||
if (!decryptedData[profileId]) {
|
||||
console.log(`[ACC] User ${profileId} not found. Creating new user.`);
|
||||
decryptedData[profileId] = userProfile; // Create a new profile
|
||||
} else {
|
||||
// Merge new data into the existing profile
|
||||
decryptedData[profileId] = {
|
||||
...decryptedData[profileId], // Existing data
|
||||
...userProfile // New data to override specific fields
|
||||
};
|
||||
}
|
||||
|
||||
// Save the updated data
|
||||
const dataFilePath = path.join(getSavefilePath(), `/account/profiles/user.json`);
|
||||
saveUserData(dataFilePath, decryptedData);
|
||||
}
|
||||
|
||||
|
||||
// Retrieve user data
|
||||
function getUserData(profileId) {
|
||||
if (decryptedData[profileId]) {
|
||||
return decryptedData[profileId];
|
||||
} else {
|
||||
console.log(`[ACC] User ${profileId} not found.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper function to read the leaderboard
|
||||
function readLeaderboard(isDotw = false) {
|
||||
if (!isDotw) {
|
||||
if (!cachedLeaderboard) {
|
||||
if (fs.existsSync(LEADERBOARD_PATH)) {
|
||||
const data = fs.readFileSync(LEADERBOARD_PATH, 'utf-8');
|
||||
cachedLeaderboard = data
|
||||
return JSON.parse(data);
|
||||
} else {
|
||||
return cachedLeaderboard || {}
|
||||
}
|
||||
}
|
||||
return JSON.parse(Leaderboard); // Return empty object if file doesn't exist
|
||||
} else {
|
||||
if (!cachedDotw) {
|
||||
if (fs.existsSync(DOTW_PATH)) {
|
||||
const data = fs.readFileSync(DOTW_PATH, 'utf-8');
|
||||
cachedDotw = data
|
||||
return JSON.parse(data);
|
||||
} else {
|
||||
return cachedDotw || {}
|
||||
}
|
||||
}
|
||||
return JSON.parse(cachedDotw); // Return empty object if file doesn't exist
|
||||
}
|
||||
}
|
||||
function generateLeaderboard(UserDataList, req) {
|
||||
// Initialize an empty leaderboard object
|
||||
const leaderboard = {};
|
||||
// Iterate over each user profile
|
||||
Object.entries(UserDataList).forEach(([profileId, userProfile]) => {
|
||||
if (userProfile.scores) {
|
||||
// Iterate over the user's scores for each map
|
||||
Object.entries(userProfile.scores).forEach(([mapName, scoreData]) => {
|
||||
// Initialize the leaderboard for the map if it doesn't exist
|
||||
if (!leaderboard[mapName]) {
|
||||
leaderboard[mapName] = [];
|
||||
}
|
||||
|
||||
// Create a leaderboard entry for this user on this map
|
||||
const leaderboardEntry = {
|
||||
__class: "LeaderboardEntry",
|
||||
score: scoreData.highest, // Assuming 'highest' is the correct property for the highest score
|
||||
profileId: profileId,
|
||||
gameVersion: 'jd2019', // Implement the getGameVersion method if needed
|
||||
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
|
||||
};
|
||||
|
||||
// Add the leaderboard entry to the map's leaderboard
|
||||
leaderboard[mapName].push(leaderboardEntry);
|
||||
});
|
||||
}
|
||||
});
|
||||
console.log('[LEADERBOARD] Leaderboard List Regenerated')
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
|
||||
// Helper function to save the leaderboard
|
||||
function saveLeaderboard(leaderboard, isDotw = false) {
|
||||
fs.writeFileSync(isDotw ? DOTW_PATH : LEADERBOARD_PATH, JSON.stringify(leaderboard, null, 2));
|
||||
}
|
||||
|
||||
//Load User Data
|
||||
if (!decryptedData) {
|
||||
const dataFilePath = path.join(getSavefilePath(), `/account/profiles/user.json`);
|
||||
loadUserData(dataFilePath);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadUserData,
|
||||
addUser,
|
||||
addUserId,
|
||||
getUserData,
|
||||
updateUser,
|
||||
updateUserTicket,
|
||||
cachedLeaderboard,
|
||||
cachedDotw,
|
||||
initroute: (app) => {
|
||||
|
||||
app.post("/profile/v2/profiles", (req, res) => {
|
||||
try {
|
||||
|
||||
const ticket = req.header("Authorization");
|
||||
|
||||
if (!ticket) {
|
||||
return res.status(400).send("Authorization header is required");
|
||||
}
|
||||
|
||||
const content = req.body;
|
||||
content.ticket = ticket;
|
||||
const dataFilePath = path.join(getSavefilePath(), `/account/profiles/user.json`);
|
||||
|
||||
// Load user data if not already loaded
|
||||
if (!decryptedData) {
|
||||
loadUserData(dataFilePath);
|
||||
}
|
||||
|
||||
// Find matching profile by name or ticket
|
||||
const matchedProfileId = Object.keys(decryptedData).find(profileId => {
|
||||
const userProfile = decryptedData[profileId];
|
||||
return userProfile && (userProfile.name === content.name || userProfile.ticket === ticket);
|
||||
});
|
||||
|
||||
console.log('[ACC] Matched profile ID:', matchedProfileId);
|
||||
|
||||
if (matchedProfileId) {
|
||||
const userProfile = decryptedData[matchedProfileId];
|
||||
const previousName = userProfile.name;
|
||||
|
||||
if (!previousName && content.name) {
|
||||
console.log('[ACC] New User Registered:', content.name);
|
||||
}
|
||||
|
||||
// Merge new content into existing user profile
|
||||
Object.assign(userProfile, content);
|
||||
|
||||
decryptedData[matchedProfileId] = userProfile;
|
||||
console.log('[ACC] Updating user:', content.name);
|
||||
saveUserData(dataFilePath, decryptedData);
|
||||
|
||||
const leaderboardlist = generateLeaderboard(decryptedData);
|
||||
saveLeaderboard(leaderboardlist, false);
|
||||
|
||||
const sanitizedProfile = { ...decryptedData[matchedProfileId] };
|
||||
delete sanitizedProfile.ip;
|
||||
delete sanitizedProfile.ticket;
|
||||
delete sanitizedProfile.email;
|
||||
delete sanitizedProfile.password;
|
||||
res.status(200).send(sanitizedProfile);
|
||||
} else {
|
||||
console.log('[ACC] Profile not found for:', content.name || 'Unknown');
|
||||
res.status(404).send("Profile not found.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ACC] Error details:', {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
name: err.name
|
||||
});
|
||||
res.status(500).send("Internal server error");
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to get profiles based on profileIds
|
||||
app.get("/profile/v2/profiles", async (req, res) => {
|
||||
const ticket = req.header("Authorization");
|
||||
const profileIds = req.query.profileIds.split(',');
|
||||
const dataFilePath = path.join(getSavefilePath(), `/account/profiles/user.json`);
|
||||
|
||||
// Load user data if not already loaded
|
||||
if (!decryptedData) {
|
||||
loadUserData(dataFilePath);
|
||||
}
|
||||
|
||||
const responseProfiles = await Promise.all(profileIds.map(async (profileId) => {
|
||||
let userProfile = decryptedData[profileId];
|
||||
|
||||
// If the profile is found in the local data
|
||||
if (userProfile && userProfile.name) {
|
||||
console.log(`[ACC] Account Found For: `, profileId);
|
||||
if (!findUserFromTicket(ticket)) {
|
||||
decryptedData[profileId].ticket = ticket;
|
||||
console.log('[ACC] Updated Ticket For ', userProfile.name)
|
||||
}
|
||||
const sanitizedProfile = { ...userProfile };
|
||||
// Remove sensitive data
|
||||
delete sanitizedProfile.ip;
|
||||
delete sanitizedProfile.ticket;
|
||||
delete sanitizedProfile.email;
|
||||
delete sanitizedProfile.password;
|
||||
return { ...sanitizedProfile, profileId };
|
||||
} else {
|
||||
// If the profile is not found locally, fetch from external source
|
||||
console.log(`[ACC] Asking Official Server For: `, profileId);
|
||||
const url = `https://prod.just-dance.com/profile/v2/profiles?profileIds=${encodeURIComponent(profileId)}`;
|
||||
try {
|
||||
const profileResponse = await axios.get(url, {
|
||||
headers: {
|
||||
'Host': 'prod.just-dance.com',
|
||||
'User-Agent': req.headers['user-agent'],
|
||||
'Accept': req.headers['accept'] || '/',
|
||||
'Accept-Language': 'en-us,en',
|
||||
'Authorization': req.headers['authorization'],
|
||||
'X-SkuId': req.headers['x-skuid'],
|
||||
}
|
||||
});
|
||||
|
||||
const profileData = profileResponse.data[0];
|
||||
if (profileData) {
|
||||
console.log(`[ACC] Account Saved to the server: `, profileId);
|
||||
const defaultProfile = { ...profileData, ticket: ticket };
|
||||
addUser(profileId, defaultProfile);
|
||||
|
||||
// Remove sensitive data before sending
|
||||
delete profileData.ip;
|
||||
delete profileData.ticket;
|
||||
delete profileData.email;
|
||||
delete profileData.password;
|
||||
|
||||
return profileData;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ACC] Error fetching profile for ${profileId}:`, error.message);
|
||||
addUser(profileId, { ticket: ticket });
|
||||
return {
|
||||
"profileId": profileId,
|
||||
"isExisting": false
|
||||
};
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
res.send(responseProfiles);
|
||||
});
|
||||
|
||||
app.post("/profile/v2/map-ended", async (req, res) => {
|
||||
const ticket = req.header("Authorization");
|
||||
const SkuId = req.header("X-SkuId") || "jd2019";
|
||||
const clientIp = req.ip;
|
||||
|
||||
try {
|
||||
const mapList = req.body;
|
||||
var leaderboard = readLeaderboard(true); // Load the current leaderboard data
|
||||
|
||||
for (let song of mapList) {
|
||||
updateMostPlayed(song.mapName);
|
||||
|
||||
// Initialize the map in the leaderboard if it doesn't exist
|
||||
if (!leaderboard[song.mapName]) {
|
||||
console.log(`${JSON.stringify(leaderboard)} doesnt exist`)
|
||||
leaderboard[song.mapName] = [];
|
||||
}
|
||||
|
||||
// Find the user profile
|
||||
const profile = findUserFromTicket(ticket);
|
||||
if (!profile) {
|
||||
console.log('[DOTW] Unable to find the Profile')
|
||||
return res.send('1');
|
||||
}
|
||||
const currentProfile = decryptedData[profile]
|
||||
if (!currentProfile) {
|
||||
console.log('[DOTW] Unable to find Pid: ', currentProfile)
|
||||
return res.send('1');
|
||||
}
|
||||
|
||||
// Check if an entry for this profileId already exists
|
||||
const currentScores = leaderboard[song.mapName];
|
||||
const existingEntryIndex = currentScores.findIndex(entry => entry.profileId === profile);
|
||||
|
||||
if (existingEntryIndex !== -1) {
|
||||
// Entry exists for this profile, update if the new score is higher
|
||||
if ((currentScores[existingEntryIndex].score < song.score) && song.score <= 13334) {
|
||||
currentScores[existingEntryIndex].score = song.score;
|
||||
currentScores[existingEntryIndex].weekOptain = getWeekNumber()
|
||||
console.log(`[DOTW] Updated score dotw list on map ${song.mapName}`);
|
||||
} else {
|
||||
return res.send('1'); // Do nothing if the new score is lower
|
||||
}
|
||||
} else {
|
||||
// No existing entry for this profile, add a new one
|
||||
const newScoreEntry = {
|
||||
__class: "DancerOfTheWeek",
|
||||
profileId: profile,
|
||||
score: song.score,
|
||||
gameVersion: SkuId.split('-')[0] || "jd2019",
|
||||
rank: currentProfile.rank,
|
||||
name: currentProfile.name,
|
||||
avatar: currentProfile.avatar,
|
||||
country: currentProfile.country,
|
||||
platformId: currentProfile.platformId,
|
||||
alias: currentProfile.alias,
|
||||
aliasGender: currentProfile.aliasGender,
|
||||
jdPoints: currentProfile.jdPoints,
|
||||
portraitBorder: currentProfile.portraitBorder,
|
||||
weekOptain: getWeekNumber()
|
||||
}
|
||||
|
||||
currentScores.push(newScoreEntry);
|
||||
leaderboard[song.mapName] = currentScores
|
||||
console.log(`[DOTW] Added new score for ${currentProfile.name} on map ${song.mapName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the updated leaderboard back to the file
|
||||
saveLeaderboard(leaderboard, true);
|
||||
res.send('');
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(200).send(''); // Keep sending response even in case of error
|
||||
}
|
||||
});
|
||||
|
||||
// Delete favorite map
|
||||
app.delete("/profile/v2/favorites/maps/:MapName", async (req, res) => {
|
||||
try {
|
||||
const MapName = req.params.MapName;
|
||||
const ticket = req.header("Authorization");
|
||||
const SkuId = req.header("X-SkuId");
|
||||
|
||||
const response = await axios.delete(
|
||||
`${prodwsurl}/profile/v2/favorites/maps/${MapName}`, {
|
||||
headers: {
|
||||
"X-SkuId": SkuId,
|
||||
Authorization: ticket,
|
||||
},
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Get profile sessions
|
||||
app.get("/v3/profiles/sessions", async (req, res) => {
|
||||
try {
|
||||
const ticket = req.header("Authorization");
|
||||
const appid = req.header("Ubi-AppId");
|
||||
|
||||
const response = await axios.get(`${ubiwsurl}/v3/profiles/sessions`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Ubi-AppId": appid,
|
||||
Authorization: ticket,
|
||||
},
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
res.status(500).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to filter players
|
||||
app.post("/profile/v2/filter-players", (req, res) => {
|
||||
res.send(["00000000-0000-0000-0000-000000000000"]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,166 +0,0 @@
|
||||
console.log(`[LEADERBOARD] Initializing....`);
|
||||
|
||||
const fs = require("fs");
|
||||
const axios = require("axios");
|
||||
const path = require("path");
|
||||
const core = {
|
||||
main: require('../var').main,
|
||||
CloneObject: require('../helper').CloneObject, getSavefilePath: require('../helper').getSavefilePath,
|
||||
generateCarousel: require('../carousel/carousel').generateCarousel, generateSweatCarousel: require('../carousel/carousel').generateSweatCarousel, generateCoopCarousel: require('../carousel/carousel').generateCoopCarousel, updateMostPlayed: require('../carousel/carousel').updateMostPlayed
|
||||
}
|
||||
const LEADERBOARD_PATH = path.join(core.getSavefilePath(), 'leaderboard/leaderboard.json');
|
||||
const DOTW_PATH = path.join(core.getSavefilePath(), 'leaderboard/dotw.json');
|
||||
|
||||
const { getSavefilePath } = require('../helper');
|
||||
|
||||
const secretKey = require('../../database/encryption.json').encrpytion.userEncrypt;
|
||||
decryptedData = {};
|
||||
|
||||
function 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);
|
||||
}
|
||||
|
||||
const getGameVersion = (req) => {
|
||||
const sku = req.header('X-SkuId') || "jd2019-pc-ww";
|
||||
return sku.substring(0, 6) || "jd2019";
|
||||
};
|
||||
|
||||
const initroute = (app) => {
|
||||
const fs = require('fs');
|
||||
|
||||
app.get("/leaderboard/v1/maps/:mapName/:type", async (req, res) => {
|
||||
const { mapName } = req.params;
|
||||
const currentWeekNumber = getWeekNumber(); // Get the current week number
|
||||
|
||||
switch (req.params.type) {
|
||||
case "dancer-of-the-week":
|
||||
try {
|
||||
if (fs.existsSync(DOTW_PATH)) {
|
||||
const data = fs.readFileSync(DOTW_PATH, 'utf-8');
|
||||
const leaderboard = JSON.parse(data);
|
||||
|
||||
// Check if the map exists in the leaderboard
|
||||
if (leaderboard[mapName] && leaderboard[mapName].length > 0) {
|
||||
// Filter entries for the current week
|
||||
const currentWeekEntries = leaderboard[mapName].filter(
|
||||
entry => entry.weekOptain === currentWeekNumber
|
||||
);
|
||||
|
||||
// Check if there are any entries for the current week
|
||||
if (currentWeekEntries.length > 0) {
|
||||
// Find the highest score entry for this map and current week
|
||||
const highestEntry = currentWeekEntries.reduce((max, entry) =>
|
||||
entry.score > max.score ? entry : max
|
||||
);
|
||||
|
||||
const dancerOfTheWeek = {
|
||||
"__class": "DancerOfTheWeek",
|
||||
"profileId": highestEntry.profileId,
|
||||
"score": highestEntry.score,
|
||||
"gameVersion": highestEntry.gameVersion || "jd2020",
|
||||
"rank": highestEntry.rank || 1, // Since it's the highest, assign rank 1
|
||||
"name": highestEntry.name,
|
||||
"avatar": highestEntry.avatar,
|
||||
"country": highestEntry.country,
|
||||
"platformId": highestEntry.platformId,
|
||||
"alias": highestEntry.alias,
|
||||
"aliasGender": highestEntry.aliasGender,
|
||||
"jdPoints": highestEntry.jdPoints,
|
||||
"portraitBorder": highestEntry.portraitBorder
|
||||
};
|
||||
|
||||
res.json(dancerOfTheWeek);
|
||||
} else {
|
||||
// No entries for the current week, return default response
|
||||
res.json({
|
||||
"__class": "DancerOfTheWeek",
|
||||
"gameVersion": "jd2019",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.json({
|
||||
"__class": "DancerOfTheWeek",
|
||||
"gameVersion": "jd2019",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('[ACC] Unable to find DOTW Files');
|
||||
// If leaderboard file does not exist, return default "NO DOTW" response
|
||||
res.json({
|
||||
"__class": "DancerOfTheWeek",
|
||||
"gameVersion": "jd2019",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error.message);
|
||||
res.status(500).send("Internal Server Error");
|
||||
}
|
||||
break; // Ensure break is here
|
||||
|
||||
case "friends":
|
||||
res.send({ __class: "LeaderboardList", entries: [] });
|
||||
break;
|
||||
|
||||
case "world": {
|
||||
let leaderboardData = {
|
||||
"__class": "LeaderboardList",
|
||||
"entries": []
|
||||
};
|
||||
|
||||
try {
|
||||
// Read the leaderboard file
|
||||
const leaderboardFilePath = LEADERBOARD_PATH;
|
||||
if (fs.existsSync(leaderboardFilePath)) {
|
||||
const data = fs.readFileSync(leaderboardFilePath, 'utf-8');
|
||||
const leaderboard = JSON.parse(data);
|
||||
|
||||
// Check if there are entries for the mapName
|
||||
if (leaderboard[mapName]) {
|
||||
// Sort the leaderboard entries by score in descending order
|
||||
const sortedEntries = leaderboard[mapName].sort((a, b) => b.score - a.score);
|
||||
|
||||
// Limit the sorted entries to the first 6
|
||||
const topSixEntries = sortedEntries.slice(0, 6);
|
||||
let rank = 0;
|
||||
|
||||
leaderboardData.entries = topSixEntries.map(entry => {
|
||||
rank++;
|
||||
const newLeaderboard = {
|
||||
"__class": "LeaderboardEntry_Online",
|
||||
"profileId": entry.profileId,
|
||||
"score": entry.score,
|
||||
"name": entry.name || entry.nickname,
|
||||
"avatar": entry.avatar,
|
||||
"rank": rank,
|
||||
"country": entry.country,
|
||||
"platformId": entry.platformId,
|
||||
"alias": entry.alias,
|
||||
"aliasGender": entry.aliasGender,
|
||||
"jdPoints": entry.jdPoints,
|
||||
"portraitBorder": entry.portraitBorder,
|
||||
"mapName": mapName
|
||||
}
|
||||
return newLeaderboard;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json(leaderboardData);
|
||||
} catch (error) {
|
||||
console.error("Error:", error.message);
|
||||
res.status(500).send("Internal Server Error");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
module.exports = { initroute };
|
||||
@@ -1,409 +0,0 @@
|
||||
//Game
|
||||
console.log(`[DEFROUTE] Initializing....`)
|
||||
var requestCountry = require("request-country");
|
||||
const settings = require('../../settings.json');
|
||||
var md5 = require('md5');
|
||||
const fs = require('fs');
|
||||
const core = {
|
||||
main: require('../var').main,
|
||||
generatePlaylist: require('../lib/playlist').generatePlaylist,
|
||||
CloneObject: require('../helper').CloneObject,
|
||||
loadJsonFile: require('../helper').loadJsonFile,
|
||||
generateCarousel: require('../carousel/carousel').generateCarousel, generateSweatCarousel: require('../carousel/carousel').generateSweatCarousel, generateCoopCarousel: require('../carousel/carousel').generateCoopCarousel, updateMostPlayed: require('../carousel/carousel').updateMostPlayed,
|
||||
signer: require('../lib/signUrl')
|
||||
}
|
||||
const path = require('path');
|
||||
const signer = require('../lib/signUrl')
|
||||
const ipResolver = require('../lib/ipResolver')
|
||||
const deployTime = Date.now()
|
||||
|
||||
//load nohud list
|
||||
const chunk = core.loadJsonFile('nohud/chunk.json', '../database/nohud/chunk.json');
|
||||
|
||||
function checkAuth(req, res) {
|
||||
if (req.header('X-SkuId')) {
|
||||
if (!(req.header('X-SkuId').startsWith("jd") || req.header('X-SkuId').startsWith("JD")) || !req.headers["authorization"].startsWith("Ubi")) {
|
||||
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
||||
res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'Bad request! Oops you didn\'t specify what file should we give you, try again'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'Oopsie! We can\'t check that ur Request is valid',
|
||||
'headder': req.headers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function returnSongdb(input, res) {
|
||||
const songdb = core.main.songdb;
|
||||
const year = input.match(/jd(\d{4})/i)?.[1];
|
||||
const platform = input.match(/-(pc|durango|orbis|nx|wiiu)/i)?.[1];
|
||||
const isParty = input.startsWith("openparty");
|
||||
const isDreynMOD = input.startsWith("jd2023pc-next") || input.startsWith("jd2024pc-next");
|
||||
const isPCParty = input.startsWith("JD2021PC") || input.startsWith("jd2022-pc");
|
||||
|
||||
if (isParty && platform === 'pc') {
|
||||
res.send(songdb['2017'].pcparty);
|
||||
} else if (isParty && platform === 'nx') {
|
||||
res.send(songdb['2018'].nx);
|
||||
} else if (isDreynMOD) {
|
||||
res.send(songdb['2017'].pcparty);
|
||||
} else if (isPCParty) {
|
||||
res.send(songdb['2017'].pcparty);
|
||||
} else if (year && platform) {
|
||||
switch (year) {
|
||||
case '2017':
|
||||
if (platform === 'pc' || platform === 'durango' || platform === 'orbis') {
|
||||
res.send(songdb['2017'].pc);
|
||||
} else if (platform === 'nx') {
|
||||
res.send(songdb['2017'].nx);
|
||||
}
|
||||
break;
|
||||
case '2018':
|
||||
if (platform === 'nx') {
|
||||
res.send(songdb['2018'].nx);
|
||||
} else if (platform === 'pc') {
|
||||
res.send(songdb['2017'].pc);
|
||||
}
|
||||
break;
|
||||
case '2019':
|
||||
if (platform === 'nx') {
|
||||
res.send(songdb['2019'].nx);
|
||||
} else if (platform === 'wiiu') {
|
||||
res.send(songdb['2019'].wiiu);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
res.send('Invalid Game');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
res.send('Invalid Game');
|
||||
}
|
||||
}
|
||||
|
||||
const activeUsers = {};
|
||||
const resetTimeout = (ip, platform) => {
|
||||
if (activeUsers[ip]) {
|
||||
clearTimeout(activeUsers[ip].timeout);
|
||||
}
|
||||
activeUsers[ip] = {
|
||||
timestamp: Date.now(),
|
||||
platform: platform || null, // Store platform if it exists, otherwise null
|
||||
timeout: setTimeout(() => {
|
||||
delete activeUsers[ip];
|
||||
}, 20 * 60 * 1000) // 20 minutes
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
exports.initroute = (app, express, server) => {
|
||||
app.get("/songdb/v1/songs", (req, res) => {
|
||||
if (checkAuth(req, res)) {
|
||||
returnSongdb(req.header('X-SkuId') || "jd2022-pc-ww", res);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/songdb/v2/songs", (req, res) => {
|
||||
var sku = req.header('X-SkuId');
|
||||
var isHttps = settings.server.enableSSL ? "https" : "http";
|
||||
|
||||
// Parse the SKU ID
|
||||
var skuParts = sku ? sku.split('-') : [];
|
||||
var version = parseInt(skuParts[0].replace('jd', '')); // Example: jd2020 -> 2020
|
||||
var platform = skuParts[1]; // Example: nx
|
||||
|
||||
// Generate URLs
|
||||
const songDBUrl = signer.generateSignedURL(`${isHttps}://${settings.server.domain}/private/songdb/prod/${sku}.${md5(JSON.stringify(core.main.songdb[version][platform]))}.json`);
|
||||
const localizationDB = signer.generateSignedURL(`${isHttps}://${settings.server.domain}/private/songdb/prod/localisation.${md5(JSON.stringify(core.main.localisation))}.json`);
|
||||
|
||||
// Send response
|
||||
res.send({
|
||||
"requestSpecificMaps": require('../../database/db/requestSpecificMaps.json'),
|
||||
"localMaps": [],
|
||||
"songdbUrl": songDBUrl,
|
||||
"localisationUrl": localizationDB
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
app.get("/private/songdb/prod/:filename", (req, res) => {
|
||||
if (signer.verifySignedURL(req.originalUrl)) {
|
||||
if (req.path.split('/')[4].startsWith('localisation')) {
|
||||
res.send(core.main.localisation);
|
||||
} else {
|
||||
var filename = req.path.split('/')[4].split('.')[0];
|
||||
var sku = filename.split('.')[0];
|
||||
if (sku) {
|
||||
// Parse the SKU ID
|
||||
var skuParts = sku.split('-');
|
||||
var version = parseInt(skuParts[0].replace('jd', '')); // Example: jd2020 -> 2020
|
||||
var platform = skuParts[1]; // Example: nx
|
||||
|
||||
if (core.main.songdb[version] && core.main.songdb[version][platform]) {
|
||||
res.send(core.main.songdb[version][platform]);
|
||||
} else {
|
||||
res.status(404).send({
|
||||
'error': 404,
|
||||
'message': 'Song database not found for the given version and platform'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
res.status(400).send({
|
||||
'error': 400,
|
||||
'message': 'You are not permitted to receive a response'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.status(401).send('Unauthorized');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/packages/v1/sku-packages', function (req, res) {
|
||||
if (checkAuth(req, res)) {
|
||||
const skuId = req.header('X-SkuId');
|
||||
const skuPackages = core.main.skupackages;
|
||||
|
||||
const platforms = ['wiiu', 'nx', 'pc', 'durango', 'orbis'];
|
||||
|
||||
for (const platform of platforms) {
|
||||
if (skuId.includes(platform)) {
|
||||
res.send(skuPackages[platform]);
|
||||
return; // Exit the function once the response is sent
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/carousel/v2/pages/avatars", function (request, response) {
|
||||
response.send(core.main.avatars);
|
||||
});
|
||||
app.post("/carousel/v2/pages/dancerprofile", function (request, response) {
|
||||
response.send(core.main.dancerprofile);
|
||||
});
|
||||
app.post("/carousel/v2/pages/jdtv", function (request, response) {
|
||||
response.send(core.main.jdtv);
|
||||
});
|
||||
app.post("/carousel/v2/pages/jdtv-nx", function (request, response) {
|
||||
response.send(core.main.jdtv);
|
||||
});
|
||||
app.post("/carousel/v2/pages/quests", function (request, response) {
|
||||
response.send(core.main.quests);
|
||||
});
|
||||
|
||||
app.post("/sessions/v1/session", (request, response) => {
|
||||
response.send({
|
||||
"pairingCode": "000000",
|
||||
"sessionId": "00000000-0000-0000-0000-000000000000",
|
||||
"docId": "0000000000000000000000000000000000000000"
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/songdb/v1/localisation", function (request, response) {
|
||||
response.send(core.main.localisation);
|
||||
});
|
||||
|
||||
// Home
|
||||
app.post("/home/v1/tiles", function (request, response) {
|
||||
response.send(core.main.home);
|
||||
});
|
||||
|
||||
// Aliases
|
||||
app.get("/aliasdb/v1/aliases", function (request, response) {
|
||||
response.send(core.main.aliases);
|
||||
});
|
||||
|
||||
// Playlists
|
||||
app.get("/playlistdb/v1/playlists", function (request, response) {
|
||||
response.send(core.generatePlaylist().playlistdb);
|
||||
});
|
||||
app.post("/carousel/v2/pages/:mode", (req, res) => {
|
||||
var search = ""
|
||||
if (req.body.searchString && req.body.searchString != "") {
|
||||
search = req.body.searchString
|
||||
} else if (req.body.searchTags && req.body.searchTags != undefined) {
|
||||
search = req.body.searchTags[0]
|
||||
} else {
|
||||
search = ""
|
||||
}
|
||||
|
||||
let action = null
|
||||
let isPlaylist = false
|
||||
|
||||
switch (req.params.mode) {
|
||||
case "party":
|
||||
case "partycoop":
|
||||
action = "partyMap"
|
||||
break
|
||||
|
||||
case "sweat":
|
||||
action = "sweatMap"
|
||||
break
|
||||
|
||||
case "create-challenge":
|
||||
action = "create-challenge"
|
||||
break
|
||||
|
||||
case "jd2019-playlists":
|
||||
case "jd2020-playlists":
|
||||
case "jd2021-playlists":
|
||||
case "jd2022-playlists":
|
||||
isPlaylist = true
|
||||
break
|
||||
}
|
||||
|
||||
if (isPlaylist) return res.json(core.generatePlaylist().playlistcategory)
|
||||
|
||||
if (action != null)
|
||||
return res.send(core.CloneObject(core.generateCarousel(search, action)))
|
||||
else return res.json({})
|
||||
});
|
||||
|
||||
app.get("/profile/v2/country", function (request, response) {
|
||||
var country = requestCountry(request);
|
||||
if (country == false) {
|
||||
country = "US";
|
||||
}
|
||||
response.send({ "country": country });
|
||||
});
|
||||
|
||||
app.get('/leaderboard/v1/coop_points/mine', function (req, res) {
|
||||
res.send(core.main.leaderboard);
|
||||
});
|
||||
|
||||
app.get('/:version/spaces/:SpaceID/entities', function (req, res) {
|
||||
res.send(core.main.entities);
|
||||
});
|
||||
|
||||
app.post("/subscription/v1/refresh", (req, res) => {
|
||||
res.send(core.main.subscription);
|
||||
});
|
||||
|
||||
app.get("/questdb/v1/quests", (req, res) => {
|
||||
var sku = req.header('X-SkuId');
|
||||
if (sku && sku.startsWith('jd2017-nx-all')) {
|
||||
res.send(core.main.questsnx);
|
||||
} else {
|
||||
res.send(core.main.questspc);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get("/session-quest/v1/", (request, response) => {
|
||||
response.send({
|
||||
"__class": "SessionQuestService::QuestData",
|
||||
"newReleases": []
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/customizable-itemdb/v1/items", (req, res) => {
|
||||
res.send(core.main.items);
|
||||
});
|
||||
|
||||
app.post("/carousel/v2/pages/upsell-videos", (req, res) => {
|
||||
res.send(core.main.upsellvideos);
|
||||
});
|
||||
|
||||
app.get("/constant-provider/v1/sku-constants", (req, res) => {
|
||||
res.send(core.main.block);
|
||||
});
|
||||
|
||||
app.get("/dance-machine/v1/blocks", (req, res) => {
|
||||
if (req.header('X-SkuId').includes("pc")) {
|
||||
res.send(core.main.dancemachine_pc);
|
||||
}
|
||||
else if (req.header('X-SkuId').includes("pc")) {
|
||||
res.send(core.main.dancemachine_nx);
|
||||
}
|
||||
else {
|
||||
res.send('Invalid Game')
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/content-authorization/v1/maps/*', (req, res) => {
|
||||
if (checkAuth(req, res)) {
|
||||
var maps = req.url.split("/").pop();
|
||||
try {
|
||||
if (chunk[maps]) {
|
||||
var placeholder = core.CloneObject(require('../../database/nohud/placeholder.json'))
|
||||
placeholder.urls = chunk[maps]
|
||||
res.send(placeholder);
|
||||
} else {
|
||||
var placeholder = JSON.stringify(core.CloneObject(require('../../database/nohud/placeholder.json')))
|
||||
var placeholder = core.CloneObject(require('../../database/nohud/placeholder.json'))
|
||||
placeholder.urls = {}
|
||||
res.send(placeholder);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.post("/carousel/:version/packages", (req, res) => {
|
||||
res.send(core.main.packages);
|
||||
});
|
||||
|
||||
app.get("/com-video/v1/com-videos-fullscreen", (req, res) => {
|
||||
res.send([]);
|
||||
});
|
||||
|
||||
// Add ServerStats
|
||||
app.get("/status/v1/ping", (req, res) => {
|
||||
const ip = ipResolver.getClientIp(req);
|
||||
const platform = req.header('X-SkuId') || "unknown";
|
||||
resetTimeout(ip, platform);
|
||||
res.send([]);
|
||||
});
|
||||
|
||||
app.get("/status/v1/serverstats", (req, res) => {
|
||||
const activeUserCount = Object.keys(activeUsers).length;
|
||||
let platformCounts = { pc: 0, nx: 0, wiiu: 0, ps4: 0, x1: 0, unknown: 0 };
|
||||
|
||||
// Iterate over activeUsers to count platforms
|
||||
for (const user of Object.values(activeUsers)) {
|
||||
if (user.platform.includes('pc')) {
|
||||
platformCounts.pc += 1;
|
||||
} else if (user.platform.includes('nx')) {
|
||||
platformCounts.nx += 1;
|
||||
} else if (user.platform.includes('wiiu')) {
|
||||
platformCounts.wiiu += 1;
|
||||
} else if (user.platform.includes('ps4')) {
|
||||
platformCounts.ps4 += 1;
|
||||
} else if (user.platform.includes('x1')) {
|
||||
platformCounts.x1 += 1;
|
||||
} else if (user.platform.includes('unknown')) {
|
||||
platformCounts.unknown += 1;
|
||||
}
|
||||
}
|
||||
|
||||
res.send({
|
||||
status: true,
|
||||
onlineUser: activeUserCount,
|
||||
deployTime: deployTime,
|
||||
currentOnlinePlatform: platformCounts
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
app.get('/openparty/server/:page', function (req, res) {
|
||||
const filePath = path.join(__dirname, '../page/', req.params.page);
|
||||
|
||||
// Check if the file exists before trying to send it
|
||||
if (fs.existsSync(filePath)) {
|
||||
res.sendFile(filePath);
|
||||
} else {
|
||||
res.status(404).send('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
@@ -1,230 +0,0 @@
|
||||
const axios = require('axios');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const core = {
|
||||
main: require('../var').main,
|
||||
CloneObject: require('../helper').CloneObject,
|
||||
generateCarousel: require('../carousel/carousel').generateCarousel,
|
||||
generateSweatCarousel: require('../carousel/carousel').generateSweatCarousel,
|
||||
generateCoopCarousel: require('../carousel/carousel').generateCoopCarousel,
|
||||
updateMostPlayed: require('../carousel/carousel').updateMostPlayed,
|
||||
signer: require('../lib/signUrl'),
|
||||
ipResolver: require('../lib/ipResolver'),
|
||||
};
|
||||
const { addUserId, updateUserTicket, getUserData, updateUser } = require('./account');
|
||||
const settings = require('../../settings.json');
|
||||
const cachedTicket = {};
|
||||
const ipCache = {}; // Cache untuk menyimpan ticket berdasarkan IP
|
||||
|
||||
const prodwsurl = "https://public-ubiservices.ubi.com/";
|
||||
|
||||
// Replace placeholders in the object
|
||||
const replaceDomainPlaceholder = (obj, domain) => {
|
||||
if (typeof obj === 'string') {
|
||||
return obj.replace('{SettingServerDomainVarOJDP}', domain);
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(item => replaceDomainPlaceholder(item, domain));
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.keys(obj).reduce((acc, key) => {
|
||||
acc[key] = replaceDomainPlaceholder(obj[key], domain);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Get client IP from request
|
||||
const getClientIp = (req) => core.ipResolver.getClientIp(req);
|
||||
|
||||
// Placeholder function for getting the country based on IP
|
||||
const getCountryFromIp = (ip) => 'US';
|
||||
|
||||
// Generate a fake ticket for fallback
|
||||
const generateFalseTicket = () => {
|
||||
const start = "ew0KIC";
|
||||
const end = "KfQ==";
|
||||
const middleLength = 60;
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let middle = '';
|
||||
|
||||
for (let i = 0; i < middleLength; i++) {
|
||||
middle += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return start + middle + end;
|
||||
};
|
||||
|
||||
|
||||
const atob = (base64) => Buffer.from(base64, 'base64').toString('utf-8');
|
||||
const parseCustomAuthHeader = (authorization) => {
|
||||
const [method, encoded] = authorization.split(" ");
|
||||
if (method !== "uplaypc_v1" || !encoded.includes("t=")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ambil bagian setelah "t=" pertama saja
|
||||
const encodedPart = encoded.substring(encoded.indexOf("t=") + 2);
|
||||
|
||||
// Decode Base64 setelah "t="
|
||||
const decoded = atob(encodedPart);
|
||||
const [tag, encodedProfileId, profileId, username, email, encodedPassword] = decoded.split(":");
|
||||
|
||||
if (tag !== "JDParty") return null;
|
||||
|
||||
return {
|
||||
profileId,
|
||||
username,
|
||||
email,
|
||||
password: atob(encodedPassword),
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize routes
|
||||
exports.initroute = (app, express, server) => {
|
||||
|
||||
// Serve application configuration
|
||||
app.get('/:version/applications/:appId/configuration', (req, res) => {
|
||||
res.send(core.main.configuration);
|
||||
});
|
||||
|
||||
// Serve alternative application configuration
|
||||
app.get('/:version/applications/:appId', (req, res) => {
|
||||
res.send(core.main.configurationnx);
|
||||
});
|
||||
|
||||
// Handle session creation
|
||||
app.post("/v3/profiles/sessions", async (req, res) => {
|
||||
const clientIp = getClientIp(req);
|
||||
const clientIpCountry = getCountryFromIp(clientIp);
|
||||
|
||||
// Helper to generate session data
|
||||
const generateSessionData = (profileId, username, clientIp, clientIpCountry, ticket) => {
|
||||
const now = new Date();
|
||||
const expiration = new Date(now.getTime() + 3 * 60 * 60 * 1000); // 3 hours
|
||||
|
||||
const data = {
|
||||
platformType: "uplay",
|
||||
ticket,
|
||||
twoFactorAuthenticationTicket: null,
|
||||
profileId,
|
||||
userId: profileId,
|
||||
nameOnPlatform: username,
|
||||
environment: "Prod",
|
||||
expiration: expiration.toISOString(),
|
||||
spaceId: uuidv4(),
|
||||
clientIp,
|
||||
clientIpCountry,
|
||||
serverTime: now.toISOString(),
|
||||
sessionId: uuidv4(),
|
||||
sessionKey: "TqCz5+J0w9e8qpLp/PLr9BCfAc30hKlEJbN0Xr+mbZa=",
|
||||
rememberMeTicket: null,
|
||||
};
|
||||
return data;
|
||||
};
|
||||
|
||||
// Remove Host header
|
||||
const headers = { ...req.headers };
|
||||
delete headers.host;
|
||||
|
||||
const customAuthData = parseCustomAuthHeader(headers.authorization);
|
||||
|
||||
if (customAuthData) {
|
||||
console.log("[ACC] CustomAuth detected, verifying...");
|
||||
|
||||
const { profileId, username, email, password } = customAuthData;
|
||||
const userData = getUserData(profileId);
|
||||
const ticket = `CustomAuth${headers.authorization.split(" t=")[1]}`
|
||||
|
||||
if (userData && userData.password && userData.email && userData.password === password) {
|
||||
console.log("[ACC] CustomAuth login: ", atob(username));
|
||||
updateUser(profileId, { username: atob(username), nickname: atob(username), email, password, userId: profileId, ticket: `Ubi_v1 ${ticket}` });
|
||||
const sessionData = generateSessionData(profileId, username, clientIp, clientIpCountry, ticket)
|
||||
res.send(sessionData);
|
||||
return;
|
||||
} else if (!userData || !userData.password || !userData.email) {
|
||||
console.log("[ACC] CustomAuth register: ", atob(username));
|
||||
updateUser(profileId, { username: atob(username), nickname: atob(username), email, password, userId: profileId, ticket: `Ubi_v1 ${ticket}` });
|
||||
res.send(generateSessionData(profileId, atob(username), clientIp, clientIpCountry, ticket));
|
||||
return;
|
||||
} else {
|
||||
console.log("[ACC] CustomAuth login, Invalid Credentials: ", atob(username));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("[ACC] Fetching Ticket From Official Server");
|
||||
const response = await axios.post(`${prodwsurl}/v3/profiles/sessions`, req.body, { headers });
|
||||
|
||||
res.send(response.data);
|
||||
console.log("[ACC] Using Official Ticket");
|
||||
|
||||
// Update user mappings
|
||||
addUserId(response.data.profileId, response.data.userId);
|
||||
updateUserTicket(response.data.profileId, `Ubi_v1 ${response.data.ticket}`);
|
||||
} catch (error) {
|
||||
console.log("[ACC] Error fetching from Ubisoft services", error.message);
|
||||
|
||||
if (ipCache[clientIp]) {
|
||||
console.log(`[ACC] Returning cached session for IP ${clientIp}`);
|
||||
return res.send(ipCache[clientIp]);
|
||||
}
|
||||
|
||||
const profileId = uuidv4();
|
||||
const userTicket = generateFalseTicket();
|
||||
cachedTicket[userTicket] = profileId;
|
||||
|
||||
console.log("[ACC] Generating Fake Session for", profileId);
|
||||
|
||||
const fakeSession = generateSessionData(profileId, "NintendoSwitch", clientIp, clientIpCountry, userTicket);
|
||||
ipCache[clientIp] = fakeSession;
|
||||
|
||||
res.send(fakeSession);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Handle session deletion
|
||||
app.delete("/v3/profiles/sessions", (req, res) => {
|
||||
res.send();
|
||||
});
|
||||
|
||||
// Retrieve profiles based on query parameters
|
||||
app.get("/v3/profiles", (req, res) => {
|
||||
const profileId = `${cachedTicket[req.header('Authorization')] || 'UnknownTicket'}/userId/${req.query.idOnPlatform}`;
|
||||
res.send({
|
||||
profiles: [{
|
||||
profileId,
|
||||
userId: profileId,
|
||||
platformType: "uplay",
|
||||
idOnPlatform: profileId,
|
||||
nameOnPlatform: "Ryujinx"
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
// Serve population data
|
||||
app.get("/v1/profiles/me/populations", (req, res) => {
|
||||
const spaceId = req.query.spaceIds || uuidv4();
|
||||
res.send({
|
||||
spaceId,
|
||||
data: {
|
||||
US_SDK_APPLICATION_BUILD_ID: "202007232022",
|
||||
US_SDK_DURABLES: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Serve application parameters for JD22
|
||||
app.get("/v1/applications/34ad0f04-b141-4793-bdd4-985a9175e70d/parameters", (req, res) => {
|
||||
res.send(replaceDomainPlaceholder(require("../../database/v1/parameters.json"), settings.server.domain));
|
||||
});
|
||||
|
||||
// Serve application parameters for JD18
|
||||
app.get("/v1/spaces/041c03fa-1735-4ea7-b5fc-c16546d092ca/parameters", (req, res) => {
|
||||
res.send(replaceDomainPlaceholder(require("../../database/v1/parameters2.json"), settings.server.domain));
|
||||
});
|
||||
|
||||
// Handle user-related requests (stubbed for now)
|
||||
app.post("/v3/users/:user", (req, res) => {
|
||||
res.send();
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class ProcessManager {
|
||||
constructor() {
|
||||
@@ -10,7 +11,8 @@ class ProcessManager {
|
||||
this.restartCount = 0;
|
||||
this.maxRestarts = 10;
|
||||
this.restartDelay = 1000;
|
||||
this.logPath = path.join(__dirname, '../database/tmp/logs.txt');
|
||||
this.logPath = path.join(__dirname, '../database/data/tmp/logs.txt');
|
||||
this.logger = new Logger('PARTY');
|
||||
|
||||
// Ensure log directory exists
|
||||
const logDir = path.dirname(this.logPath);
|
||||
@@ -34,23 +36,27 @@ class ProcessManager {
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
console.log(`[PARTY] Process exited with code ${code}`);
|
||||
this.logger.info(`Process exited with code ${code}`);
|
||||
|
||||
if (code === 42) {
|
||||
if (this.restartCount < this.maxRestarts) {
|
||||
console.log(`[PARTY] Restarting process in ${this.restartDelay}ms...`);
|
||||
this.logger.info(`Restarting process in ${this.restartDelay}ms...`);
|
||||
setTimeout(() => this.start(), this.restartDelay);
|
||||
this.restartCount++;
|
||||
} else {
|
||||
console.error('[PARTY] Max restart attempts reached');
|
||||
this.logger.error('Max restart attempts reached');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logOutput(level, message) {
|
||||
// Write directly to console
|
||||
console.log(`${message}`);
|
||||
// Use the logger for output
|
||||
if (level === 'INFO') {
|
||||
this.logger.info(message);
|
||||
} else if (level === 'ERROR') {
|
||||
this.logger.error(message);
|
||||
}
|
||||
|
||||
const log = {
|
||||
level,
|
||||
@@ -98,13 +104,13 @@ manager.start();
|
||||
|
||||
// Handle process signals
|
||||
process.on('SIGINT', () => {
|
||||
console.log('[PARTY] Gracefully shutting down...');
|
||||
manager.logger.info('Gracefully shutting down...');
|
||||
manager.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('[PARTY] Terminating...');
|
||||
manager.logger.info('Terminating...');
|
||||
manager.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
@@ -1,52 +1,55 @@
|
||||
const Logger = require('../utils/logger');
|
||||
const logger = new Logger('UPDATER');
|
||||
|
||||
function restartPM2() {
|
||||
const { exec } = require('child_process');
|
||||
console.log('[UPDATER] PM2 Process Detected, Running In PM2 Way ...');
|
||||
console.log('[UPDATER] Pulling Upstream Before Restarting ...');
|
||||
logger.info('PM2 Process Detected, Running In PM2 Way ...');
|
||||
logger.info('Pulling Upstream Before Restarting ...');
|
||||
|
||||
// Execute Git pull
|
||||
exec('git pull', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to execute git pull: ${err.message}`);
|
||||
logger.error(`Failed to execute git pull: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if package.json has been modified
|
||||
console.log('[UPDATER] Checking package.json ...');
|
||||
logger.info('Checking package.json ...');
|
||||
exec('git diff --name-only package.json', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to check for package.json modifications: ${err.message}`);
|
||||
logger.error(`Failed to check for package.json modifications: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout.trim() !== '') {
|
||||
console.log('[UPDATER] package.json has been modified. Running npm install...');
|
||||
logger.info('package.json has been modified. Running npm install...');
|
||||
// Execute npm install
|
||||
exec('npm install', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to execute npm install: ${err.message}`);
|
||||
logger.error(`Failed to execute npm install: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[UPDATER] npm install completed successfully.');
|
||||
logger.info('npm install completed successfully.');
|
||||
});
|
||||
} else {
|
||||
console.log('[UPDATER] No modifications detected in packages.json. Skipping npm install.');
|
||||
logger.info('No modifications detected in packages.json. Skipping npm install.');
|
||||
}
|
||||
|
||||
// Restart Node.js process using PM2
|
||||
console.log('[UPDATER] Restarting Server');
|
||||
logger.info('Restarting Server');
|
||||
pm2.connect((err) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to connect to PM2: ${err.message}`);
|
||||
logger.error(`Failed to connect to PM2: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[UPDATER] restarting Node.js process...');
|
||||
logger.info('restarting Node.js process...');
|
||||
pm2.restart('JDPartyServer', (err) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to restart Node.js process: ${err.message}`);
|
||||
logger.error(`Failed to restart Node.js process: ${err.message}`);
|
||||
} else {
|
||||
console.log('[UPDATER] Node.js process restarted successfully.');
|
||||
logger.info('Node.js process restarted successfully.');
|
||||
}
|
||||
pm2.disconnect();
|
||||
});
|
||||
@@ -57,36 +60,36 @@ function restartPM2() {
|
||||
function updateNormal(server) {
|
||||
const { exec } = require('child_process');
|
||||
const pm2 = require('pm2');
|
||||
console.log('[UPDATER] Pulling Upstream Before Starting ...');
|
||||
logger.info('Pulling Upstream Before Starting ...');
|
||||
|
||||
// Execute Git pull
|
||||
exec('git pull', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to execute git pull: ${err.message}`);
|
||||
logger.error(`Failed to execute git pull: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if package.json has been modified
|
||||
console.log('[UPDATER] Checking package.json ...');
|
||||
logger.info('Checking package.json ...');
|
||||
exec('git diff --name-only package.json', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to check for package.json modifications: ${err.message}`);
|
||||
logger.error(`Failed to check for package.json modifications: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stdout.trim() !== '') {
|
||||
console.log('[UPDATER] package.json has been modified. Running npm install...');
|
||||
logger.info('package.json has been modified. Running npm install...');
|
||||
// Execute npm install
|
||||
exec('npm install', (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(`[UPDATER] Failed to execute npm install: ${err.message}`);
|
||||
logger.error(`Failed to execute npm install: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[UPDATER] npm install completed successfully.');
|
||||
logger.info('npm install completed successfully.');
|
||||
});
|
||||
} else {
|
||||
console.log('[UPDATER] No modifications detected in packages.json. Skipping npm install.');
|
||||
logger.info('No modifications detected in packages.json. Skipping npm install.');
|
||||
}
|
||||
process.exit(42)
|
||||
});
|
||||
|
||||
171
core/services/AccountService.js
Normal file
171
core/services/AccountService.js
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Service for handling account-related business logic
|
||||
*/
|
||||
const Account = require('../models/Account');
|
||||
const AccountRepository = require('../repositories/AccountRepository');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class AccountService {
|
||||
constructor() {
|
||||
this.logger = new Logger('AccountService');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user data by profile ID
|
||||
* @param {string} profileId The profile ID
|
||||
* @returns {Promise<Account|null>} The account or null if not found
|
||||
*/
|
||||
async getUserData(profileId) {
|
||||
this.logger.info(`Getting user data for ${profileId}`);
|
||||
return AccountRepository.findById(profileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by ticket
|
||||
* @param {string} ticket The ticket to search for
|
||||
* @returns {Promise<string|null>} Profile ID if found, null otherwise
|
||||
*/
|
||||
async findUserFromTicket(ticket) {
|
||||
this.logger.info(`Finding user from ticket`);
|
||||
const account = await AccountRepository.findByTicket(ticket);
|
||||
return account ? account.profileId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by nickname
|
||||
* @param {string} nickname The nickname to search for
|
||||
* @returns {Promise<Account|null>} The account or null if not found
|
||||
*/
|
||||
async findUserFromNickname(nickname) {
|
||||
this.logger.info(`Finding user with nickname ${nickname}`);
|
||||
return AccountRepository.findByNickname(nickname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new user ID mapping
|
||||
* @param {string} profileId The profile ID
|
||||
* @param {string} userId The user ID to map
|
||||
* @returns {Promise<Account>} The updated account
|
||||
*/
|
||||
async addUserId(profileId, userId) {
|
||||
this.logger.info(`Adding user ID ${userId} for profile ${profileId}`);
|
||||
let account = await AccountRepository.findById(profileId);
|
||||
|
||||
if (!account) {
|
||||
account = new Account({ profileId });
|
||||
}
|
||||
|
||||
account.update({ userId });
|
||||
return AccountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's ticket
|
||||
* @param {string} profileId The profile ID
|
||||
* @param {string} ticket The new ticket value
|
||||
* @returns {Promise<Account>} The updated account
|
||||
*/
|
||||
async updateUserTicket(profileId, ticket) {
|
||||
this.logger.info(`Updating ticket for profile ${profileId}`);
|
||||
let account = await AccountRepository.findById(profileId);
|
||||
|
||||
if (!account) {
|
||||
account = new Account({ profileId });
|
||||
}
|
||||
|
||||
account.update({ ticket });
|
||||
return AccountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user information
|
||||
* @param {string} profileId The profile ID
|
||||
* @param {Object} userData The user data to update
|
||||
* @returns {Promise<Account>} The updated account
|
||||
*/
|
||||
async updateUser(profileId, userData) {
|
||||
this.logger.info(`Updating user ${profileId}`);
|
||||
let account = await AccountRepository.findById(profileId);
|
||||
|
||||
if (!account) {
|
||||
account = new Account({
|
||||
profileId,
|
||||
...userData
|
||||
});
|
||||
this.logger.info(`Created new user ${profileId}`);
|
||||
} else {
|
||||
account.update(userData);
|
||||
this.logger.info(`Updated existing user ${profileId}`);
|
||||
}
|
||||
|
||||
return AccountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user score for a map
|
||||
* @param {string} profileId The profile ID
|
||||
* @param {string} mapName The map name
|
||||
* @param {Object} scoreData The score data
|
||||
* @returns {Promise<Account>} The updated account
|
||||
*/
|
||||
async updateUserScore(profileId, mapName, scoreData) {
|
||||
this.logger.info(`Updating score for ${profileId} on ${mapName}`);
|
||||
const account = await AccountRepository.findById(profileId);
|
||||
|
||||
if (!account) {
|
||||
this.logger.info(`User ${profileId} not found, cannot update score`);
|
||||
return null;
|
||||
}
|
||||
|
||||
account.updateScore(mapName, scoreData);
|
||||
return AccountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add map to user favorites
|
||||
* @param {string} profileId The profile ID
|
||||
* @param {string} mapName The map name
|
||||
* @returns {Promise<Account>} The updated account
|
||||
*/
|
||||
async addMapToFavorites(profileId, mapName) {
|
||||
this.logger.info(`Adding ${mapName} to favorites for ${profileId}`);
|
||||
const account = await AccountRepository.findById(profileId);
|
||||
|
||||
if (!account) {
|
||||
this.logger.info(`User ${profileId} not found, cannot add favorite`);
|
||||
return null;
|
||||
}
|
||||
|
||||
account.addFavorite(mapName);
|
||||
return AccountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove map from user favorites
|
||||
* @param {string} profileId The profile ID
|
||||
* @param {string} mapName The map name
|
||||
* @returns {Promise<Account>} The updated account
|
||||
*/
|
||||
async removeMapFromFavorites(profileId, mapName) {
|
||||
this.logger.info(`Removing ${mapName} from favorites for ${profileId}`);
|
||||
const account = await AccountRepository.findById(profileId);
|
||||
|
||||
if (!account) {
|
||||
this.logger.info(`User ${profileId} not found, cannot remove favorite`);
|
||||
return null;
|
||||
}
|
||||
|
||||
account.removeFavorite(mapName);
|
||||
return AccountRepository.save(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all accounts
|
||||
* @returns {Promise<Object>} Map of profileId to Account instances
|
||||
*/
|
||||
async getAllAccounts() {
|
||||
return AccountRepository.loadAll();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AccountService(); // Export a singleton instance
|
||||
46
core/services/CarouselService.js
Normal file
46
core/services/CarouselService.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Carousel Service
|
||||
* Provides business logic for carousel generation and related operations.
|
||||
* It acts as an orchestrator for SongService and MostPlayedService.
|
||||
*/
|
||||
const { generateCarousel, generateSweatCarousel, generateCoopCarousel, updateMostPlayed } = require('../carousel/carousel');
|
||||
|
||||
class CarouselService {
|
||||
/**
|
||||
* Generate a carousel based on search criteria and type.
|
||||
* @param {string} search - The search string or tag.
|
||||
* @param {string} type - The type of carousel (e.g., "partyMap", "sweatMap").
|
||||
* @returns {Object} The generated carousel object.
|
||||
*/
|
||||
generateCarousel(search, type) {
|
||||
return generateCarousel(search, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cooperative carousel.
|
||||
* @param {string} search - The search string or tag.
|
||||
* @returns {Object} The generated cooperative carousel object.
|
||||
*/
|
||||
generateCoopCarousel(search) {
|
||||
return generateCoopCarousel(search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sweat carousel.
|
||||
* @param {string} search - The search string or tag.
|
||||
* @returns {Object} The generated sweat carousel object.
|
||||
*/
|
||||
generateSweatCarousel(search) {
|
||||
return generateSweatCarousel(search);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the most played count for a given map.
|
||||
* @param {string} mapName - The name of the map played.
|
||||
*/
|
||||
updateMostPlayed(mapName) {
|
||||
updateMostPlayed(mapName);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CarouselService(); // Export a singleton instance
|
||||
54
core/services/MostPlayedService.js
Normal file
54
core/services/MostPlayedService.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* MostPlayed Service
|
||||
* Provides business logic for most played song operations, interacting with MostPlayedRepository.
|
||||
*/
|
||||
const MostPlayedRepository = require('../repositories/MostPlayedRepository');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class MostPlayedService {
|
||||
constructor() {
|
||||
this.mostPlayedRepository = MostPlayedRepository;
|
||||
this.logger = new Logger('MostPlayedService');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the play count for a map.
|
||||
* @param {string} mapName - The name of the map.
|
||||
*/
|
||||
async updateMostPlayed(mapName) {
|
||||
const currentWeek = this.getWeekNumber();
|
||||
const mostPlayedInstance = await this.mostPlayedRepository.getMostPlayed();
|
||||
mostPlayedInstance.incrementPlayCount(currentWeek, mapName);
|
||||
await this.mostPlayedRepository.save(mostPlayedInstance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get globally most played songs for the current week.
|
||||
* @returns {Promise<string[]>} A promise that resolves to an array of mapNames, sorted by play count (descending).
|
||||
*/
|
||||
async getGlobalPlayedSong() {
|
||||
try {
|
||||
const currentWeek = this.getWeekNumber();
|
||||
const mostPlayedInstance = await this.mostPlayedRepository.getMostPlayed();
|
||||
return mostPlayedInstance.getSongsForWeek(currentWeek)
|
||||
.map(item => item[0]);
|
||||
} catch (err) {
|
||||
this.logger.error('Error getting global played songs:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MostPlayedService(); // Export a singleton instance
|
||||
161
core/services/SongService.js
Normal file
161
core/services/SongService.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Song Service
|
||||
* Provides business logic for song-related operations, interacting with SongRepository.
|
||||
*/
|
||||
const SongRepository = require('../repositories/SongRepository');
|
||||
|
||||
class SongService {
|
||||
constructor() {
|
||||
this.songRepository = SongRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a song by its mapName.
|
||||
* @param {string} mapName - The mapName of the song.
|
||||
* @returns {Song|undefined} The Song instance or undefined if not found.
|
||||
*/
|
||||
getSongByMapName(mapName) {
|
||||
return this.songRepository.findByMapName(mapName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all songs.
|
||||
* @returns {Object<string, Song>} An object containing all Song instances, keyed by mapName.
|
||||
*/
|
||||
getAllSongs() {
|
||||
return this.songRepository.getAllSongs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all song mapNames.
|
||||
* @returns {string[]} An array of all song mapNames.
|
||||
*/
|
||||
getAllMapNames() {
|
||||
return this.songRepository.getAllMapNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter songs based on a provided function.
|
||||
* @param {Function} filterFunction - A function that takes a Song instance and returns true if it should be included.
|
||||
* @returns {Song[]} An array of filtered Song instances.
|
||||
*/
|
||||
filterSongs(filterFunction) {
|
||||
return Object.values(this.songRepository.getAllSongs()).filter(filterFunction).sort((a, b) => {
|
||||
const titleA = (a.title + a.mapName).toLowerCase();
|
||||
const titleB = (b.title + b.mapName).toLowerCase();
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a list of mapNames by title, prioritizing those containing a specific word.
|
||||
* @param {string[]} mapNames - An array of song mapNames.
|
||||
* @param {string} word - The word to prioritize in titles.
|
||||
* @returns {string[]} A sorted array of mapNames.
|
||||
*/
|
||||
sortByTitle(mapNames, word) {
|
||||
const songs = this.songRepository.getAllSongs();
|
||||
const x = [];
|
||||
const doesntContainWord = [];
|
||||
|
||||
for (const mapName of mapNames) {
|
||||
const song = songs[mapName];
|
||||
if (song && song.title) {
|
||||
const titleIndex = song.title.toLowerCase().indexOf(word);
|
||||
if (titleIndex === -1) {
|
||||
doesntContainWord.push(mapName);
|
||||
} else {
|
||||
x.push([mapName, titleIndex]);
|
||||
}
|
||||
} else {
|
||||
doesntContainWord.push(mapName); // Handle cases where song or title might be missing
|
||||
}
|
||||
}
|
||||
|
||||
doesntContainWord.sort();
|
||||
x.sort((a, b) => a[1] - b[1]); // Sort by titleIndex
|
||||
|
||||
const toReturn = x.map(item => item[0]);
|
||||
return toReturn.concat(doesntContainWord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter songs by search query.
|
||||
* @param {string[]} mapNames - An array of song mapNames to filter.
|
||||
* @param {string} search - The search query.
|
||||
* @returns {string[]} An array of filtered song mapNames.
|
||||
*/
|
||||
filterSongsBySearch(mapNames, search) {
|
||||
const songs = this.songRepository.getAllSongs();
|
||||
const filteredMapNames = mapNames.filter(mapName => {
|
||||
const song = songs[mapName];
|
||||
if (!song) return false;
|
||||
const lowerSearch = search.toLowerCase();
|
||||
return (
|
||||
(song.title && song.title.toLowerCase().includes(lowerSearch)) ||
|
||||
(song.artist && song.artist.toLowerCase().includes(lowerSearch)) ||
|
||||
(song.mapName && song.mapName.toLowerCase().includes(lowerSearch)) ||
|
||||
(song.originalJDVersion && String(song.originalJDVersion) === lowerSearch) || // Convert to string for comparison
|
||||
(song.tags && song.tags.includes(search)) // Tags might be exact match
|
||||
);
|
||||
});
|
||||
return this.sortByTitle(filteredMapNames, search.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter songs by first letter of their title.
|
||||
* @param {string[]} mapNames - An array of song mapNames to filter.
|
||||
* @param {string} filter - The first letter(s) to filter by.
|
||||
* @returns {string[]} An array of filtered song mapNames.
|
||||
*/
|
||||
filterSongsByFirstLetter(mapNames, filter) {
|
||||
const songs = this.songRepository.getAllSongs();
|
||||
const regex = new RegExp(`^[${filter}].*`, 'i'); // Case-insensitive
|
||||
return mapNames.filter(mapName => {
|
||||
const song = songs[mapName];
|
||||
return song && song.title && regex.test(song.title);
|
||||
}).sort((a, b) => {
|
||||
const titleA = songs[a].title.toLowerCase();
|
||||
const titleB = songs[b].title.toLowerCase();
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter songs by Just Dance version.
|
||||
* @param {string[]} mapNames - An array of song mapNames to filter.
|
||||
* @param {number} version - The JD version.
|
||||
* @returns {string[]} An array of filtered song mapNames.
|
||||
*/
|
||||
filterSongsByJDVersion(mapNames, version) {
|
||||
const songs = this.songRepository.getAllSongs();
|
||||
return mapNames.filter(mapName => {
|
||||
const song = songs[mapName];
|
||||
return song && song.originalJDVersion === version;
|
||||
}).sort((a, b) => {
|
||||
const titleA = songs[a].title.toLowerCase();
|
||||
const titleB = songs[b].title.toLowerCase();
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter songs by tags.
|
||||
* @param {string[]} mapNames - An array of song mapNames to filter.
|
||||
* @param {string} tag - The tag to filter by.
|
||||
* @returns {string[]} An array of filtered song mapNames.
|
||||
*/
|
||||
filterSongsByTags(mapNames, tag) {
|
||||
const songs = this.songRepository.getAllSongs();
|
||||
return mapNames.filter(mapName => {
|
||||
const song = songs[mapName];
|
||||
return song && song.tags && song.tags.includes(tag);
|
||||
}).sort((a, b) => {
|
||||
const titleA = songs[a].title.toLowerCase();
|
||||
const titleB = songs[b].title.toLowerCase();
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SongService(); // Export a singleton instance
|
||||
66
core/utils/logger.js
Normal file
66
core/utils/logger.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
class Logger {
|
||||
constructor(moduleName = 'APP') {
|
||||
this.moduleName = moduleName;
|
||||
this.moduleColor = this._getModuleColor(moduleName);
|
||||
}
|
||||
|
||||
_getModuleColor(moduleName) {
|
||||
const colors = [
|
||||
chalk.default.cyanBright,
|
||||
chalk.default.magentaBright,
|
||||
chalk.default.yellowBright,
|
||||
chalk.default.blueBright,
|
||||
chalk.default.greenBright,
|
||||
chalk.default.redBright,
|
||||
chalk.default.whiteBright,
|
||||
chalk.default.gray,
|
||||
chalk.default.blackBright
|
||||
];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < moduleName.length; i++) {
|
||||
hash = moduleName.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
const index = Math.abs(hash % colors.length);
|
||||
return colors[index];
|
||||
}
|
||||
|
||||
_formatMessage(level, message, ...args) {
|
||||
const coloredModuleName = this.moduleColor(`[${this.moduleName}]`);
|
||||
let formattedMessage = `${coloredModuleName} ${message}`;
|
||||
|
||||
switch (level) {
|
||||
case 'INFO':
|
||||
return chalk.default.greenBright(formattedMessage, ...args);
|
||||
case 'WARN':
|
||||
return chalk.default.yellowBright(formattedMessage, ...args);
|
||||
case 'ERROR':
|
||||
return chalk.default.redBright(formattedMessage, ...args);
|
||||
case 'DEBUG':
|
||||
return chalk.default.blueBright(formattedMessage, ...args);
|
||||
default:
|
||||
return formattedMessage;
|
||||
}
|
||||
}
|
||||
|
||||
info(message, ...args) {
|
||||
console.log(this._formatMessage('INFO', message, ...args));
|
||||
}
|
||||
|
||||
warn(message, ...args) {
|
||||
console.warn(this._formatMessage('WARN', message, ...args));
|
||||
}
|
||||
|
||||
error(message, ...args) {
|
||||
console.error(this._formatMessage('ERROR', message, ...args));
|
||||
}
|
||||
|
||||
debug(message, ...args) {
|
||||
if (process.env.NODE_ENV === 'development') { // Only show debug logs in development
|
||||
console.log(this._formatMessage('DEBUG', message, ...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
||||
48
core/var.js
48
core/var.js
@@ -1,10 +1,13 @@
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path'); // Add path module
|
||||
const { loadJsonFile } = require('./helper');
|
||||
const songdb = require('./lib/songdb').songdbF;
|
||||
const settings = require('../settings.json');
|
||||
const Logger = require('./utils/logger');
|
||||
const logger = new Logger('VAR');
|
||||
|
||||
console.log('[VAR] Initializing....');
|
||||
logger.info('Initializing....');
|
||||
|
||||
const replaceDomainPlaceholder = (obj, domain) => {
|
||||
if (typeof obj === 'string') {
|
||||
@@ -29,27 +32,26 @@ const main = {
|
||||
durango: loadJsonFile('Platforms/jd2017-durango/sku-packages.json', '../database/Platforms/jd2017-durango/sku-packages.json'),
|
||||
orbis: loadJsonFile('Platforms/jd2017-orbis/sku-packages.json', '../database/Platforms/jd2017-orbis/sku-packages.json'),
|
||||
},
|
||||
entities: replaceDomainPlaceholder(require('../database/v2/entities.json'), settings.server.domain),
|
||||
configuration: replaceDomainPlaceholder(require('../database/v1/configuration.json'), settings.server.domain),
|
||||
subscription: require('../database/db/subscription.json'),
|
||||
packages: require('../database/packages.json'),
|
||||
block: require('../database/carousel/block.json'),
|
||||
leaderboard: require("../database/db/leaderboard.json"),
|
||||
quests: require("../database/db/quests.json"),
|
||||
questsnx: require("../database/db/quests-nx.json"),
|
||||
questspc: require("../database/db/quests-pc.json"),
|
||||
items: require("../database/db/items.json"),
|
||||
upsellvideos: require("../database/carousel/pages/upsell-videos.json"),
|
||||
dancemachine_pc: require("../database/db/dancemachine_pc.json"),
|
||||
dancemachine_nx: require("../database/db/dancemachine_nx.json"),
|
||||
avatars: require("../database/db/avatars.json"),
|
||||
dancerprofile: require("../database/db/dancerprofile.json"),
|
||||
aliases: require("../database/db/aliases.json"),
|
||||
home: require("../database/db/home.json"),
|
||||
jdtv: require("../database/db/jdtv.json"),
|
||||
playlistdb: require("../database/db/playlistdb.json"),
|
||||
playlists: require("../database/db/playlists.json"),
|
||||
create_playlist: require("../database/carousel/pages/create_playlist.json"),
|
||||
entities: replaceDomainPlaceholder(require('../database/config/v2/entities.json'), settings.server.domain),
|
||||
configuration: replaceDomainPlaceholder(require('../database/config/v1/configuration.json'), settings.server.domain),
|
||||
subscription: require('../database/data/db/subscription.json'),
|
||||
packages: require('../database/config/packages.json'),
|
||||
block: require('../database/data/carousel/block.json'),
|
||||
quests: require("../database/data/db/quests.json"),
|
||||
questsnx: require("../database/data/db/quests-nx.json"),
|
||||
questspc: require("../database/data/db/quests-pc.json"),
|
||||
items: require("../database/data/db/items.json"),
|
||||
upsellvideos: require("../database/data/carousel/pages/upsell-videos.json"),
|
||||
dancemachine_pc: require("../database/data/db/dancemachine_pc.json"),
|
||||
dancemachine_nx: require("../database/data/db/dancemachine_nx.json"),
|
||||
avatars: require("../database/data/db/avatars.json"),
|
||||
dancerprofile: require("../database/data/db/dancerprofile.json"),
|
||||
aliases: require("../database/data/db/aliases.json"),
|
||||
home: require("../database/data/db/home.json"),
|
||||
jdtv: require("../database/data/db/jdtv.json"),
|
||||
playlistdb: require("../database/data/db/playlistdb.json"),
|
||||
playlists: require("../database/data/db/playlists.json"),
|
||||
create_playlist: require("../database/data/carousel/pages/create_playlist.json"),
|
||||
songdb: { "2016": {}, "2017": {}, "2018": {}, "2019": {}, "2020": {}, "2021": {}, "2022": {} },
|
||||
localisation: loadJsonFile('Platforms/openparty-all/localisation.json', '../database/Platforms/openparty-all/localisation.json')
|
||||
};
|
||||
@@ -58,4 +60,4 @@ main.songdb = songdb.generateSonglist();
|
||||
|
||||
module.exports = {
|
||||
main
|
||||
}
|
||||
}
|
||||
|
||||
528
core/wdf/FakeWdfPlugin.js
Normal file
528
core/wdf/FakeWdfPlugin.js
Normal file
@@ -0,0 +1,528 @@
|
||||
/**
|
||||
* WDF Plugin for OpenParty
|
||||
* Handles World Dance Floor (WDF) related routes as a plugin
|
||||
*/
|
||||
const axios = require('axios');
|
||||
const Plugin = require('../classes/Plugin'); // Assuming Plugin is located at ../core/classes/Plugin.js
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class WDFPlugin extends Plugin {
|
||||
/**
|
||||
* Create a new WDF plugin
|
||||
*/
|
||||
constructor() {
|
||||
super('WDFPlugin', 'Handles World Dance Floor (WDF) related routes for Just Dance.');
|
||||
this.logger = new Logger('WDFPlugin');
|
||||
|
||||
// Bind handler methods to maintain 'this' context.
|
||||
// This is crucial when these methods are passed as callbacks to Express routes.
|
||||
this.handleAssignRoom = this.handleAssignRoom.bind(this);
|
||||
this.handleServerTime = this.handleServerTime.bind(this);
|
||||
this.handleScreens = this.handleScreens.bind(this);
|
||||
this.handleNewsfeed = this.handleNewsfeed.bind(this);
|
||||
this.handleOnlineBosses = this.handleOnlineBosses.bind(this);
|
||||
this.handleNextHappyHours = this.handleNextHappyHours.bind(this);
|
||||
this.handleGetNotification = this.handleGetNotification.bind(this);
|
||||
this.handlePostNotification = this.handlePostNotification.bind(this);
|
||||
this.handleGetSessionRecap = this.handleGetSessionRecap.bind(this);
|
||||
this.handlePostSessionRecap = this.handlePostSessionRecap.bind(this);
|
||||
this.handleGetScoreRecap = this.handleGetScoreRecap.bind(this);
|
||||
this.handlePostScoreRecap = this.handlePostScoreRecap.bind(this);
|
||||
this.handleGetOnlineRankWidget = this.handleGetOnlineRankWidget.bind(this);
|
||||
this.handlePostOnlineRankWidget = this.handlePostOnlineRankWidget.bind(this);
|
||||
this.handleGetSession = this.handleGetSession.bind(this);
|
||||
this.handlePostSession = this.handlePostSession.bind(this);
|
||||
this.handleGetCcu = this.handleGetCcu.bind(this);
|
||||
this.handleDeleteSession = this.handleDeleteSession.bind(this);
|
||||
this.handleGetTournamentScoreRecap = this.handleGetTournamentScoreRecap.bind(this);
|
||||
this.handlePostTournamentScoreRecap = this.handlePostTournamentScoreRecap.bind(this);
|
||||
this.handleGetTournamentUpdateScores = this.handleGetTournamentUpdateScores.bind(this);
|
||||
this.handlePostTournamentUpdateScores = this.handlePostTournamentUpdateScores.bind(this);
|
||||
this.handleWildcardGet = this.handleWildcardGet.bind(this);
|
||||
this.handleWildcardPost = this.handleWildcardPost.bind(this);
|
||||
|
||||
// Initialize properties
|
||||
this.prodwsurl = "https://jmcs-prod.just-dance.com";
|
||||
this.FAKEWDF_ROOM = "FAKEWDF"; // Constant for the room ID
|
||||
|
||||
this.fakerecap = {
|
||||
"uniquePlayerCount": 0,
|
||||
"countries": [
|
||||
"0"
|
||||
],
|
||||
"__class": "SessionRecapInfo"
|
||||
};
|
||||
|
||||
// Pre-load static JSON data if they are small and frequently accessed
|
||||
// IMPORTANT: Adjust these paths based on your actual project structure.
|
||||
// Assuming 'database' is a sibling directory to 'plugins' if this plugin is in 'plugins'
|
||||
this.assignRoomPcData = require("../../database/data/wdf/assign-room-pc.json");
|
||||
this.newsfeedData = require("../../database/data/wdf/newsfeed.json");
|
||||
this.nextHappyHoursData = require("../../database/data/wdf/next-happyhours.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin's routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
this.logger.info(`Initializing routes...`);
|
||||
|
||||
// Register all the WDF routes using Express app methods directly
|
||||
app.post("/wdf/v1/assign-room", this.handleAssignRoom);
|
||||
app.get("/wdf/v1/server-time", this.handleServerTime);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/screens`, this.handleScreens);
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/newsfeed`, this.handleNewsfeed);
|
||||
app.get("/wdf/v1/online-bosses", this.handleOnlineBosses);
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/next-happyhours`, this.handleNextHappyHours);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/notification`, this.handleGetNotification);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/notification`, this.handlePostNotification);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session-recap`, this.handleGetSessionRecap);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session-recap`, this.handlePostSessionRecap);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/score-recap`, this.handleGetScoreRecap);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/score-recap`, this.handlePostScoreRecap);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/online-rank-widget`, this.handleGetOnlineRankWidget);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/online-rank-widget`, this.handlePostOnlineRankWidget);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session`, this.handleGetSession);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session`, this.handlePostSession);
|
||||
app.delete(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session`, this.handleDeleteSession);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/ccu`, this.handleGetCcu);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/score-recap`, this.handleGetTournamentScoreRecap);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/score-recap`, this.handlePostTournamentScoreRecap);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/update-scores`, this.handleGetTournamentUpdateScores);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/update-scores`, this.handlePostTournamentUpdateScores);
|
||||
|
||||
// Wildcard routes for forwarding requests to the official server
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/*`, this.handleWildcardGet);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/*`, this.handleWildcardPost);
|
||||
|
||||
this.logger.info(`Routes initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/assign-room
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleAssignRoom(req, res) {
|
||||
res.send(this.assignRoomPcData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/server-time
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleServerTime(req, res) {
|
||||
res.send({
|
||||
"time": Date.now() / 1000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/screens
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleScreens(req, res) {
|
||||
res.send({
|
||||
"__class": "ScreenList",
|
||||
"screens": [{
|
||||
"__class": "Screen",
|
||||
"type": "in-game",
|
||||
"startTime": Date.now() / 1000,
|
||||
"endTime": (Date.now() / 1000) + 300,
|
||||
"theme": "vote",
|
||||
"mapName": "Despacito",
|
||||
"schedule": {
|
||||
"type": "probability",
|
||||
"theme": "MapVote",
|
||||
"occurance": {
|
||||
"next": (Date.now() / 1000) + 400,
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/newsfeed
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleNewsfeed(req, res) {
|
||||
res.send(this.newsfeedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/online-bosses
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleOnlineBosses(req, res) {
|
||||
res.send({ __class: "OnlineBossDb", bosses: {} });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/next-happyhours
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleNextHappyHours(req, res) {
|
||||
res.send(this.nextHappyHoursData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/notification
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetNotification(req, res) {
|
||||
res.send({ "__class": "Notification" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/notification
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostNotification(req, res) {
|
||||
res.send({ "__class": "Notification" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/session-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetSessionRecap(req, res) {
|
||||
res.send(this.fakerecap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/session-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostSessionRecap(req, res) {
|
||||
res.send(this.fakerecap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/online-rank-widget
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetOnlineRankWidget(req, res) {
|
||||
res.send({
|
||||
"currentSeasonEndTime": 1714255200,
|
||||
"seasonNumber": 1,
|
||||
"currentSeasonDancerCount": 1,
|
||||
"previousSeasonWinner": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"currentUserOnlineRankInfo": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"__class": "OnlineRankWidgetInfo"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/online-rank-widget
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostOnlineRankWidget(req, res) {
|
||||
res.send({
|
||||
"currentSeasonEndTime": 1714255200,
|
||||
"seasonNumber": 1,
|
||||
"currentSeasonDancerCount": 1,
|
||||
"previousSeasonWinner": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"currentUserOnlineRankInfo": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"__class": "OnlineRankWidgetInfo"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/session
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetSession(req, res) {
|
||||
res.send('OK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/session
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostSession(req, res) {
|
||||
res.send('OK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/ccu
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetCcu(req, res) {
|
||||
res.send('0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle DELETE /wdf/v1/rooms/FAKEWDF/session
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleDeleteSession(req, res) {
|
||||
res.send('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/themes/tournament/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetTournamentScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/themes/tournament/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostTournamentScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/themes/tournament/update-scores
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetTournamentUpdateScores(req, res) {
|
||||
res.send({
|
||||
"__class": "UpdateScoreResult",
|
||||
"currentRank": 1,
|
||||
"scoreEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "ScoreEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/themes/tournament/update-scores
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostTournamentUpdateScores(req, res) {
|
||||
res.send({
|
||||
"__class": "UpdateScoreResult",
|
||||
"currentRank": 1,
|
||||
"scoreEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "ScoreEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle wildcard GET requests for WDF rooms, forwarding to official server.
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleWildcardGet(req, res) {
|
||||
try {
|
||||
const ticket = req.header("Authorization");
|
||||
const result = req.url; // This gets the full URL path including the FAKEWDF and additional path segments
|
||||
|
||||
const response = await axios.get(this.prodwsurl + result, {
|
||||
headers: {
|
||||
'X-SkuId': '',
|
||||
'Authorization': ticket,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
this.logger.error(`Wildcard GET error:`, error.message);
|
||||
res.status(error.response ? error.response.status : 500).send(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle wildcard POST requests for WDF rooms, forwarding to official server.
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleWildcardPost(req, res) {
|
||||
try {
|
||||
const ticket = req.header("Authorization");
|
||||
const result = req.url; // This gets the full URL path including the FAKEWDF and additional path segments
|
||||
|
||||
const response = await axios.post(this.prodwsurl + result, req.body, {
|
||||
headers: {
|
||||
'X-SkuId': '',
|
||||
'Authorization': ticket,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
this.logger.error(`Wildcard POST error:`, error.message);
|
||||
res.status(error.response ? error.response.status : 500).send(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance of the plugin
|
||||
module.exports = new WDFPlugin();
|
||||
@@ -1,315 +0,0 @@
|
||||
var axios = require("axios");
|
||||
|
||||
function initroute(app) {
|
||||
var prodwsurl = "https://jmcs-prod.just-dance.com"
|
||||
app.post("/wdf/v1/assign-room", (req, res) => {
|
||||
res.send(require("../../database/wdf/assign-room-pc.json"));
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/server-time", (req, res) => {
|
||||
res.send({
|
||||
"time": Date.now() / 1000
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
app.post("/wdf/v1/rooms/PCJD2017/screens", (req, res) => {
|
||||
res.send({ "__class": "ScreenList",
|
||||
"screens": [{
|
||||
"__class": "Screen",
|
||||
"type": "in-game",
|
||||
"startTime": Date.now() / 1000,
|
||||
"endTime": (Date.now() / 1000) + 300,
|
||||
"theme": "vote",
|
||||
"mapName": "Despacito",
|
||||
"schedule": {
|
||||
"type": "probability",
|
||||
"theme": "MapVote",
|
||||
"occurance": {
|
||||
"next": (Date.now() / 1000) + 400,
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
||||
app.get("wdf/v1/rooms/PCJD2017/newsfeed", (req, res) => {
|
||||
res.send(require("../../database/wdf/newsfeed.json"));
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/online-bosses", (req, res) => {
|
||||
res.send({ __class: "OnlineBossDb", bosses: {} });
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/rooms/PCJD2017/next-happyhours", (req, res) => {
|
||||
res.send(require("../../database/wdf/next-happyhours.json"));
|
||||
});
|
||||
|
||||
var fakerecap = {
|
||||
"uniquePlayerCount": 0,
|
||||
"countries": [
|
||||
"0"
|
||||
],
|
||||
"__class": "SessionRecapInfo"
|
||||
}
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/notification", (req, res) => {
|
||||
res.send({ "__class": "Notification" })
|
||||
});
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/notification", (req, res) => {
|
||||
res.send({ "__class": "Notification" })
|
||||
});
|
||||
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/session-recap", (req, res) => {
|
||||
res.send(fakerecap)
|
||||
});
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/session-recap", (req, res) => {
|
||||
res.send(fakerecap)
|
||||
});
|
||||
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/score-recap", (req, res) => {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [
|
||||
{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}
|
||||
],
|
||||
"totalPlayerCount": 1
|
||||
})
|
||||
});
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/score-recap", (req, res) => {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [
|
||||
{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}
|
||||
],
|
||||
"totalPlayerCount": 1
|
||||
})
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/online-rank-widget", (req, res) => {
|
||||
res.send({
|
||||
"currentSeasonEndTime": 1714255200,
|
||||
"seasonNumber": 1,
|
||||
"currentSeasonDancerCount": 1,
|
||||
"previousSeasonWinner": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"currentUserOnlineRankInfo": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"__class": "OnlineRankWidgetInfo"
|
||||
})
|
||||
});
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/online-rank-widget", (req, res) => {
|
||||
res.send({
|
||||
"currentSeasonEndTime": 1714255200,
|
||||
"seasonNumber": 1,
|
||||
"currentSeasonDancerCount": 1,
|
||||
"previousSeasonWinner": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"currentUserOnlineRankInfo": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"__class": "OnlineRankWidgetInfo"
|
||||
})
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/session", (req, res) => {
|
||||
res.send('OK')
|
||||
});
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/session", (req, res) => {
|
||||
res.send('OK')
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/ccu", (req, res) => {
|
||||
res.send('0');
|
||||
});
|
||||
|
||||
app.delete("/wdf/v1/rooms/" + "FAKEWDF" + "/session", (req, res) => {
|
||||
res.send('');
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/themes/tournament/score-recap", (req, res) => {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [
|
||||
{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}
|
||||
],
|
||||
"totalPlayerCount": 1
|
||||
})
|
||||
});
|
||||
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/themes/tournament/score-recap", (req, res) => {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [
|
||||
{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}
|
||||
],
|
||||
"totalPlayerCount": 1
|
||||
})
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/themes/tournament/update-scores", (req, res) => {
|
||||
res.send({
|
||||
"__class": "UpdateScoreResult",
|
||||
"currentRank": 1,
|
||||
"scoreEntries": [
|
||||
{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "ScoreEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}
|
||||
],
|
||||
"totalPlayerCount": 1
|
||||
})
|
||||
});
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/themes/tournament/update-scores", (req, res) => {
|
||||
res.send({
|
||||
"__class": "UpdateScoreResult",
|
||||
"currentRank": 1,
|
||||
"scoreEntries": [
|
||||
{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "ScoreEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}
|
||||
],
|
||||
"totalPlayerCount": 1
|
||||
})
|
||||
});
|
||||
|
||||
app.get("/wdf/v1/rooms/" + "FAKEWDF" + "/*", async (req, res) => {
|
||||
try {
|
||||
const ticket = req.header("Authorization");
|
||||
const result = req.url; // This gets the full URL path including the FAKEWDF and additional path segments
|
||||
|
||||
const response = await axios.get(prodwsurl + result, {
|
||||
headers: {
|
||||
'X-SkuId': '',
|
||||
'Authorization': ticket,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
res.status(error.response ? error.response.status : 500).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/wdf/v1/rooms/" + "FAKEWDF" + "/*", async (req, res) => {
|
||||
try {
|
||||
const ticket = req.header("Authorization");
|
||||
const result = req.url; // This gets the full URL path including the FAKEWDF and additional path segments
|
||||
|
||||
const response = await axios.post(prodwsurl + result, req.body, {
|
||||
headers: {
|
||||
'X-SkuId': '',
|
||||
'Authorization': ticket,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
res.status(error.response ? error.response.status : 500).send(error.message);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
module.exports = { initroute }
|
||||
@@ -1 +0,0 @@
|
||||
eyJ1bmRlZmluZWQiOiJbb2JqZWN0IE9iamVjdF1VYmlfdjEgZXdvZ0lDSjJaWElpT2lBaU1TSXNDaUFnSW1GcFpDSTZJQ0l6TkRFM09EbGtOQzFpTkRGbUxUUm1OREF0WVdNM09TMWxNbUpqTkdNNU5HVmhaRFFpTEFvZ0lDSmxibllpT2lBaVVISnZaQ0lzQ2lBZ0luTnBaQ0k2SUNJMk1tTXlPVGhrTXkxaU1HVTFMVFE1WVRBdFlXTmxaUzB6Tm1NeE9EQTJOVGczWVRjaUxBb2dJQ0owZVhBaU9pQWlTbGRGSWl3S0lDQWlaVzVqSWpvZ0lrRXhNamhEUWtNaUxBb2dJQ0pwZGlJNklDSkxRMmR5ZFZoT2F6ZHlXbGxJUm1ZMFZWaFBPSFJSSWl3S0lDQWlhVzUwSWpvZ0lraFRNalUySWl3S0lDQWlhMmxrSWpvZ0lqaGhNV1l3T1RGakxURXdOVEV0TkdSbFlpMDRNelZtTFRVNE0yRmpOekE0TVdZMU1TSUtmUS5sNTA3WWQ5Y2tiSW8wSlBlV3hla3hERGt4RXA3ZFFZN0dZcHN4bE45VDJiNS1ldmNwT0R3Z0tvTTBXVXN5UXJvZWh4T1l6SHg5UjFVRnpnZWROdWVwRTN3N1JjY0g5bTk3VVRXOXowQ2w3WVRBb0xReHRKbThZdXFtcWhSc0JuWGRDRjhvOWU1Y3hRMnU1clJvZzJWclk0UnM2TWxJRFJ5bEhBZWZZSjR6WV9jRVdqSS0xTTNqb3ozVjlkNHJNTVZHZmNhQTZOZERXU1VZeGMxRzIxV3p1VTdmeGhlNEhQVTV3dFFyTXVUMzJlMHNSUmt0NlpxU0QwYkVuUlp3Q2hFODg2ZGcxbG5idjR2cncyZTFFYXVLamt6RnBjNG5qc1c5QU9ub3VVTUFSVVNTbTNGNTdnc1lLendMb0pURGJVaXd6VE9rdm90cFBFZHlzejR3UUo4LTFtTUdtaGpDLWtVU1Z6UXNxWGRlOTFhSzJoSmRrWFlyWmx2X1lVX3NnNFJHWXF1ZHVEeXlGSVFhSWtqWlZxd1M3Szk1Q2VsSHpoSVBoUUozelpMZjZBbHRsYkFGUEpLSk41QV9CTUtpU0ExVGpFUEtmQWU3aXpiU0syWE02MFUweHp4WUw3X1NnaW9yUURvbW5aVy1oTXlIUHM3RDgzNWwtazhsd0VtY2o1YlFTNG45eEpOVHZidVE5S3BVWHlZRm1maXNUVHJFTzVfNk9md0xIbld1b3lIZ0NBZi1mZ3RvamJaMEVpbFVUYlNlMmtvUDNfejJpZzloNy0yVDQyZjhXQ3NXdFE2ekNIazJGSDk3MEVwOG1GN0x4TkZ0aVc5VHFlYzZ3THhjWmpIcGdfbGZPZ1FCcm9rdzVrNlVCbUs5YmpYOF9xTDBya1VXZ2RCc2pIRUJfOE50bVlueHU5ZXh6TFFreGV6bTlRLWM0LTczTWJLMk9kem1hd0U5ODlRVVRmblNVd3UyNVhqanlXSWtqOEFzSndQSHhUMEU5NGpvdzBpVXlRd3lsU0RBa0JTYTkzNEJZT19DdGpYYWNBRW1MbHU3RWJwYTB0Q3lfSy13WEtYS0gwT1JZM3pOMnBnVHRQNjNvNnZnci14eEhxVnZISkdXdW1QS092SGVieFJfeGVBNjM0SmRuNjV6NHdyVFZ3V3A3SnFESzhkWmNFWndvZFd1NnFlMEdjNXpRSW80Q3p0OVNFcENKc21ad0h2cV92SWQwaXFEZW5tb1QxODdMM3d0eElNbTV0dC1qRUNldlpmOGkxMGpqUnhOOU96ckJWS0NZTlBuXzBrS3ZIMXBvZDRpQ2VWMUxSLU1rTlhiTjhsRTNjRnR1WmZweG1URzNhUXdYT3lqclZvT3RFSDBnbVBZVElqWVZjMl8zV1lFLUp6c1NKaTBnQm9ZUnQwcHRfel9oa3p5cHp4blFHeUxUWjY3b09vUWczT2V3d3lwUlp2eEVrNUVydFd6b2RPS0RKNDRjb09wQVh1ZDJ1TEMtN1VTdFJXUXJCTG1FT3JFRE1GMFZGa1FfVVIxVDgxOHM0aUlTSHpvYmdLY0Y4dGxFTm9YSmNOMmFIN0l2Mk1QT2hINVB5OXlraEtIcWZLWlZYbFpoaVNlNWh3cHBqMHhkZDNOcFUyaW9nNXZtQXJHSG52MlB3QjZZRFZwQ1VVZ0QtVnZaLUN2blNlOWdHbk1WdmlKUnktNmZxSHQ5cE53MHFZSVFpRlRiRjFMY2o2Ukxpa1dGMmlnSkJJYkZ5b3dUQVdIaDc3SEcxbng5MThfdGlzQXI1VEtHM2lhTlh1WS1JWUZvakRSOE9kSldTeTVUdVVpb0gzaGZELUd6enNTaDNPbFFxU0JBWlRlZzdJYmhuVGNiMHU2bFBuMmFhc3V3WG1ZT1Z0Ym5yQ1pBbTNROFp6MEZCOEhNWmwxcDdsTVdSRHJtN2tPcFVJVHhHTGFEcWJGa1JvdDVQT1hSTllCdVEtY2lVd0MwRFRVX1NLU21UYzNkYWVhZGRRc1owdTQzbkQwci1DaDVKdUowb3RmNld1X0w0Q19WVVl1bmctYTNHTEQ5ZnpHeFZaX0FkYi1aekdVTUZBM2RuWUNJakl4aWdGSHRvQUF2cFg3ZWpJX3h4UWNfbDlMNGt4UnZiQWJMQWgtekt1RExaWjgzSXNSOUJqUmUzcjZsM1VFQTk2OWIyZlpDaVRMWnA4OFdrdDdPTThoUEYtUF9HVHhhOTVPcTBrNkkzVnRRVEZrQURSSU5PRC1lcU42X3BFN0FsQ2txa1BpUjJvOGJUcl9zOWUzT0VOblMzSFpDSDBBSFhkMmhWWVE4V0xOb1ZEeFB2Rk1UMEs5SnI2U3JoWTlIdm5sdFBxR09PcjR4ZGlRN0RZQTc5ZEg5MEtvdWN5MGp3OFBZQ2xiMFNBM1dMM1ZnSVVub3JyaExWS2NTNFp4bjVoQTlMdldxZVBBZ1phX1lVM0o4ajNhc3lnWFdWMFRQU1RJdE1xSlNsdzFDU2hKeGZXc0FhQkJMWXNBS05QUzlzalpFMjZLNHFiQ2ZVZUMzb2pMS1huRjB0T1dDT3QyQTB5enBQLVhuQTd5VW9JV3dCN290OF9tRXhwVGRfUzFBdkNNQmh3RHJWalJ6YzNQRDFOQks2amhkY09zbkRDeDBydEN1ZU1rQ0FUX1RqMFVwdTItdV9vSWd0YWY2UXUwRUM5YVp2S0F3N2s4RlNjdGFDNjdhYk90V3lSQnh0MUhESVhjc0t2d0FvOU12bXRLVmVrMTdfWXEyazZFdTFGTW9YLTRoMnJpTGhSZVBkX0toUk50Y0s3WVZ3d29wQ29peEpoQjZ1d18yX09PX3VyaWxHM1NWRkUyZ1JQUFN1UThZU2dpS21fRkhUdGZ2R0REQjIwdmY4a21Ya3c5WHZqM2swWDZ4c2VVMVRBZ2xZMGZiY0pTUVNaeElwSGUteWpmdlJuTFpuSGZ5RzYtMm9IU1kzdmMwd1pfU2NNWS05bnRDYVJpelkwZUVXLVNyTlpRenhFdDBuYjdwOUI4dmRsVDNYdnc4dUl1NGZlMHE1bV9BRzVKS293bTNnR1VNTmJvcDBiTHFmNEZMeGN6Q3ZDenl6cG1tUkVvanlmSzgySy01Q2NXckMtRmw5OHRmTjZCR0lkVVZuSWxpRlk3MkdUN2xSVmhkWXVXeEZ0MXN2Zk1BRnBwOXBpWXpoWnkyU2thSWpoTjlVVVdDUlM5S2ZLT195NWt4TDdqdDlLd2Iwb1ZkajQ3WHkteVFremVlazBxcFhnQ05keHRBWWwxN1lycHhrSDlvblNaN3NtRzVhMDM2MXYtMVFodEp3dmlZbi1Vc0JBQ3hNeTZmQkJBUndlTFlCR3l1SFNzTm5NWE5xNWJqMWR1MG1WSmhBLlJxZFRtbi1EWHRtT2poaVMtTFdGTnFpQVdqbjNrVEpxNWMzV2xNaktPYjAifQ==
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"1": {
|
||||
}
|
||||
}
|
||||
1
database/data/tmp/logs.txt
Normal file
1
database/data/tmp/logs.txt
Normal file
@@ -0,0 +1 @@
|
||||
[{"method":"LOG","url":"","timestamp":"2025-03-21T07:14:47.910Z"},{"method":"LOG ERROR","url":"node:internal/modules/cjs/loader:1247\r\n throw err;\r\n ^\r\n\r\nError: Cannot find module 'E:\\ibra\\OpenParty\\jduparty.js'\r\n at Function._resolveFilename (node:internal/modules/cjs/loader:1244:15)\r\n at Function._load (node:internal/modules/cjs/loader:1070:27)\r\n at TracingChannel.traceSync (node:diagnostics_channel:322:14)\r\n at wrapModuleLoad (node:internal/modules/cjs/loader:217:24)\r\n at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)\r\n at node:internal/main/run_main_module:36:49 {\r\n code: 'MODULE_NOT_FOUND',\r\n requireStack: []\r\n}\r\n\r\nNode.js v22.13.1","timestamp":"2025-03-21T07:14:49.565Z"}]
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"__class": "LeaderboardList",
|
||||
"entries": [{
|
||||
"__class": "LeaderboardEntry_Online",
|
||||
"profileId": "acfe6905-f1e5-4d37-afbf-088794a71938",
|
||||
"rank": 751,
|
||||
"score": 251,
|
||||
"name": "Adrian_Flopper",
|
||||
"avatar": 376,
|
||||
"country": 8521,
|
||||
"platformId": "",
|
||||
"jdPoints": 23210,
|
||||
"portraitBorder": 26
|
||||
}
|
||||
],
|
||||
"playerCount": 1
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
2228
package-lock.json
generated
2228
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"name": "open-party",
|
||||
"version": "1.1.0",
|
||||
"version": "3.0.0",
|
||||
"description": "Ibratabian17's JDU Code",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node core/scripts/run.js",
|
||||
"startPM2": "pm2 start server.js --name server --no-daemon",
|
||||
"maintenancea": "node core/scripts/update.js"
|
||||
"dev": "nodemon server.js",
|
||||
"start:pm2": "pm2 start server.js --name openparty --no-daemon",
|
||||
"start:managed": "node core/scripts/run.js",
|
||||
"maintenance": "node core/scripts/update.js"
|
||||
},
|
||||
"author": "PartyService",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"body-parser": "^1.20.3",
|
||||
"chalk": "^5.4.1",
|
||||
"express": "^4.21.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"md5": "^2.3.0",
|
||||
"pm2": "^6.0.5",
|
||||
"request-country": "^0.1.2",
|
||||
"sqlite3": "^5.1.7",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
36
plugins/HelloWorldPlugin.js
Normal file
36
plugins/HelloWorldPlugin.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Example HelloWorld Plugin for OpenParty
|
||||
* Demonstrates how to create a plugin using the new class-based architecture
|
||||
*/
|
||||
const Plugin = require('../core/classes/Plugin');
|
||||
|
||||
class HelloWorldPlugin extends Plugin {
|
||||
/**
|
||||
* Create a new HelloWorld plugin
|
||||
*/
|
||||
constructor() {
|
||||
super('HelloWorldPlugin', 'A simple example plugin that demonstrates the plugin system');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin's routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
console.log(`[${this.name}] Initializing routes...`);
|
||||
|
||||
// Add a simple route that returns a greeting
|
||||
app.get('/hello-world', (req, res) => {
|
||||
res.json({
|
||||
message: 'Hello from OpenParty Plugin System!',
|
||||
plugin: this.name,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`[${this.name}] Routes initialized`);
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance of the plugin
|
||||
module.exports = new HelloWorldPlugin();
|
||||
65
plugins/README.md
Normal file
65
plugins/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# OpenParty Plugin System
|
||||
|
||||
This directory contains plugins for the OpenParty server. Plugins are a way to extend the functionality of the server without modifying the core codebase.
|
||||
|
||||
## Creating a Plugin
|
||||
|
||||
To create a plugin, you need to create a new JavaScript file that exports an instance of a class that extends the `Plugin` class. Here's a basic example:
|
||||
|
||||
```javascript
|
||||
const Plugin = require('../core/classes/Plugin');
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
constructor() {
|
||||
super('MyPlugin', 'Description of my plugin');
|
||||
}
|
||||
|
||||
initroute(app) {
|
||||
// Set up routes and functionality
|
||||
app.get('/my-plugin/endpoint', (req, res) => {
|
||||
res.send({ message: 'Hello from my plugin!' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MyPlugin();
|
||||
```
|
||||
|
||||
## Registering a Plugin
|
||||
|
||||
To register your plugin with the server, you need to add it to the `modules` array in `settings.json`:
|
||||
|
||||
```json
|
||||
"modules": [
|
||||
{
|
||||
"name": "MyPlugin",
|
||||
"description": "Description of my plugin",
|
||||
"path": "{dirname}/plugins/MyPlugin.js",
|
||||
"execution": "init"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The `execution` property can be either `"pre-load"` or `"init"`. Plugins with `"pre-load"` execution are initialized before the core routes, while plugins with `"init"` execution are initialized after the core routes.
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
Plugins have the following lifecycle methods:
|
||||
|
||||
- `constructor(name, description)`: Called when the plugin is created
|
||||
- `initroute(app)`: Called when the plugin is initialized
|
||||
- `enable()`: Called to enable the plugin
|
||||
- `disable()`: Called to disable the plugin
|
||||
- `isEnabled()`: Returns whether the plugin is enabled
|
||||
|
||||
## Example Plugins
|
||||
|
||||
- `HelloWorldPlugin.js`: A simple example plugin that demonstrates the plugin system
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep plugins focused**: Each plugin should have a single responsibility
|
||||
2. **Use descriptive names**: Plugin names should be descriptive and unique
|
||||
3. **Document your plugin**: Include a description of what your plugin does
|
||||
4. **Handle errors gracefully**: Catch and handle errors in your plugin
|
||||
5. **Clean up resources**: If your plugin uses resources like file handles or database connections, make sure to clean them up when the plugin is disabled
|
||||
27
server.js
27
server.js
@@ -1,19 +1,14 @@
|
||||
// Ibratabian17's jdu
|
||||
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
console.log(`[MAIN] Starting daemon`);
|
||||
process.title = "OpenParty | Custom Just Dance Unlimited Server";
|
||||
/**
|
||||
* OpenParty - Class-based entry point
|
||||
* This file serves as the entry point for the class-based version of OpenParty
|
||||
*/
|
||||
|
||||
// Import dependencies
|
||||
const settings = require('./settings.json');
|
||||
const core = require('./core/core');
|
||||
const port = settings.server.forcePort ? settings.server.port : process.env.PORT || settings.server.port;
|
||||
const isPublic = settings.server.isPublic ? "0.0.0.0" : "127.0.0.1";
|
||||
const Server = require('./core/classes/Server');
|
||||
|
||||
// Initialize Express.js
|
||||
const server = app.listen(port, isPublic, () => {
|
||||
core.init(app, express, server);
|
||||
console.log(`[MAIN] listening on ${isPublic}:${port}`);
|
||||
console.log(`[MAIN] Open panel to see more log`);
|
||||
console.log(`[MAIN] Running on ${process.env.NODE_ENV} session`);
|
||||
});
|
||||
console.log(`[MAIN] Starting OpenParty with class-based architecture`);
|
||||
|
||||
// Create and start the server
|
||||
const server = new Server(settings);
|
||||
server.start();
|
||||
@@ -17,9 +17,15 @@
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"name": "FAKEWDF",
|
||||
"name": "WDFPlugin",
|
||||
"description": "Create a fake response for WDF so that the mod can run on older version",
|
||||
"path": "{dirname}/core/wdf/fakewdf.js",
|
||||
"path": "{dirname}/core/wdf/FakeWdfPlugin.js",
|
||||
"execution": "init"
|
||||
},
|
||||
{
|
||||
"name": "HelloWorldPlugin",
|
||||
"description": "A simple example plugin that demonstrates the plugin system",
|
||||
"path": "{dirname}/plugins/HelloWorldPlugin.js",
|
||||
"execution": "init"
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user