Merge pull request #15 from PretendoNetwork/memory-fixes

This commit is contained in:
Jonathan Barrow 2025-01-08 14:25:41 -05:00 committed by GitHub
commit 644081e615
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
50 changed files with 7023 additions and 3487 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
[*]
indent_style = tab
end_of_line = lf
insert_final_newline = true
[*.json]
indent_size = 2
[*.yml]
indent_style = space
indent_size = 2

View File

@ -1,2 +0,0 @@
dist
*.js

23
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: Lint
on:
pull_request: {}
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint

113
eslint.config.mjs Normal file
View File

@ -0,0 +1,113 @@
// @ts-check
/* eslint-disable import/no-named-as-default-member -- We want to be able to use the full package name for the imports here for clarity */
import eslint from '@eslint/js';
import eslintCommentPlugin from '@eslint-community/eslint-plugin-eslint-comments/configs';
import stylisticPlugin from '@stylistic/eslint-plugin';
// @ts-expect-error importPlugin is not typed
import importPlugin from 'eslint-plugin-import';
import tseslint from 'typescript-eslint';
/**
* Typed to any as the type is incompatible for some reason - it works!
* @type {any}
*/
const stylisticConfig = stylisticPlugin.configs.customize({
indent: 'tab',
quotes: 'single',
semi: true,
commaDangle: 'never',
braceStyle: '1tbs'
});
/**
* Typed to any as the type is incompatible for some reason - it works!
* @type {any}
*/
const eslintCommentPluginConfig = eslintCommentPlugin.recommended;
export default tseslint.config(
{
// https://eslint.org/docs/rules/
extends: [eslint.configs.recommended],
rules: {
'require-atomic-updates': 'off', // This rule is widely controversial and causes false positives
'no-console': 'off',
'prefer-const': 'error',
'no-var': 'error',
'no-unused-vars': 'off',
'one-var': ['error', 'never']
}
},
{
// https://typescript-eslint.io/rules/
extends: [tseslint.configs.recommended],
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' }
],
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-object-type': ['off'],
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/consistent-type-imports': ['error', {
fixStyle: 'separate-type-imports'
}]
}
},
{
// https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/
extends: [eslintCommentPluginConfig],
rules: {
'@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
'@eslint-community/eslint-comments/require-description': 'error'
}
},
{
// https://eslint.style/rules
extends: [stylisticConfig],
rules: {
'@stylistic/no-extra-semi': 'error',
'@stylistic/yield-star-spacing': ['error', 'after'],
'@stylistic/operator-linebreak': ['error', 'after', { overrides: { '?': 'before', ':': 'before' } }],
'@stylistic/curly-newline': ['error', {
multiline: true,
consistent: true
}],
'@stylistic/object-curly-newline': ['error', {
multiline: true,
consistent: true
}]
}
},
{
// https://www.npmjs.com/package/eslint-plugin-import
extends: [importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.warnings, importPlugin.flatConfigs.typescript],
settings: {
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json'
},
node: true
}
},
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'newlines-between': 'never'
}],
'import/first': 'error',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level']
}
},
{
ignores: [
'scripts/*',
'dist/*'
]
}
);

View File

@ -1,52 +0,0 @@
const fs = require('fs-extra');
require('colors');
const root = __dirname;
fs.ensureDirSync(`${root}/logs`);
const streams = {
latest: fs.createWriteStream(`${root}/logs/latest.log`),
success: fs.createWriteStream(`${root}/logs/success.log`),
error: fs.createWriteStream(`${root}/logs/error.log`),
warn: fs.createWriteStream(`${root}/logs/warn.log`),
info: fs.createWriteStream(`${root}/logs/info.log`)
};
function success(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
console.log(`${input}`.green.bold);
}
function error(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
console.log(`${input}`.red.bold);
}
function warn(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
console.log(`${input}`.yellow.bold);
}
function info(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}
module.exports = {
success,
error,
warn,
info
};

9784
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
"main": "dist/server.js",
"scripts": {
"lint": "npx eslint .",
"lint:fix": "npx eslint . --fix",
"build": "npm run lint && npm run clean && npx tsc && npx tsc-alias",
"clean": "rimraf ./dist",
"start": "node --enable-source-maps ."
@ -13,36 +14,41 @@
"author": "",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-s3": "^3.395.0",
"@aws-sdk/client-s3": "^3.723.0",
"@pretendonetwork/boss-crypto": "^1.0.0",
"@pretendonetwork/grpc": "^1.0.6",
"@typegoose/auto-increment": "^3.6.1",
"boss-js": "github:PretendoNetwork/boss-js",
"cacache": "^18.0.0",
"cacache": "^19.0.1",
"colors": "^1.4.0",
"dicer": "^0.3.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-subdomain": "^1.0.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-subdomain": "^1.0.6",
"fs-extra": "^11.2.0",
"moment": "^2.29.4",
"mongoose": "^7.4.3",
"moment": "^2.30.1",
"mongoose": "~7.6.1",
"morgan": "^1.10.0",
"nice-grpc": "^2.1.5",
"nice-grpc": "^2.1.10",
"xmlbuilder": "^15.1.1"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
"@smithy/types": "^4.0.0",
"@stylistic/eslint-plugin": "^2.12.1",
"@types/dicer": "^0.2.4",
"@types/express": "^4.17.17",
"@types/fs-extra": "^11.0.1",
"@types/morgan": "^1.9.4",
"@types/node": "^20.5.0",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"axios": "^1.6.2",
"eslint": "^8.47.0",
"tsc-alias": "^1.8.7",
"typescript": "^5.1.6",
"xmlbuilder2": "^3.0.2"
"@types/express": "^4.17.21",
"@types/fs-extra": "^11.0.4",
"@types/morgan": "^1.9.9",
"@types/node": "^22.10.5",
"@typescript-eslint/parser": "^8.19.1",
"axios": "^1.7.9",
"eslint": "^9.17.0",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-plugin-import": "^2.31.0",
"tsc-alias": "^1.8.10",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"xmlbuilder2": "^3.1.1"
}
}

View File

@ -1,10 +1,10 @@
import crypto from 'node:crypto';
import path from 'node:path';
import fs from 'fs-extra';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
import { LOG_INFO, LOG_WARN, LOG_ERROR } from '@/logger';
import { DisabledFeatures, Config } from '@/types/common/config';
import type mongoose from 'mongoose';
import type { DisabledFeatures, Config } from '@/types/common/config';
dotenv.config();
@ -78,7 +78,7 @@ export const config: Config = {
secret: process.env.PN_BOSS_CONFIG_S3_ACCESS_SECRET?.trim() || ''
},
disk_path: process.env.PN_BOSS_CONFIG_CDN_DISK_PATH?.trim() || ''
},
}
};
LOG_INFO('Config loaded, checking integrity');

View File

@ -4,10 +4,10 @@ import { CECSlot } from '@/models/cec-slot';
import { Task } from '@/models/task';
import { File } from '@/models/file';
import { config } from '@/config-manager';
import { HydratedCECDataDocument } from '@/types/mongoose/cec-data';
import { HydratedCECSlotDocument, ICECSlot } from '@/types/mongoose/cec-slot';
import { HydratedTaskDocument, ITask } from '@/types/mongoose/task';
import { HydratedFileDocument, IFile } from '@/types/mongoose/file';
import type { HydratedCECDataDocument } from '@/types/mongoose/cec-data';
import type { HydratedCECSlotDocument, ICECSlot } from '@/types/mongoose/cec-slot';
import type { HydratedTaskDocument, ITask } from '@/types/mongoose/task';
import type { HydratedFileDocument, IFile } from '@/types/mongoose/file';
const connection_string: string = config.mongoose.connection_string;
const options: mongoose.ConnectOptions = config.mongoose.options;
@ -168,7 +168,7 @@ export async function getRandomCECData(pids: number[], gameID: number): Promise<
// * We search through the CECSlot so that everyone has the same chance of getting their data picked up
const filter: mongoose.FilterQuery<ICECSlot> = {
creator_pid: {
$in: pids,
$in: pids
},
game_id: gameID
};

View File

@ -14,34 +14,42 @@ const streams = {
info: fs.createWriteStream(`${root}/logs/info.log`)
} as const;
function getCurrentTimestamp(): string {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // * Months are 0-indexed
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
export function LOG_SUCCESS(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
input = `[${getCurrentTimestamp()}] [SUCCESS]: ${input}`;
streams.success.write(`${input}\n`);
console.log(`${input}`.green.bold);
}
export function LOG_ERROR(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
input = `[${getCurrentTimestamp()}] [ERROR]: ${input}`;
streams.error.write(`${input}\n`);
console.log(`${input}`.red.bold);
console.error(`${input}`.red.bold);
}
export function LOG_WARN(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
input = `[${getCurrentTimestamp()}] [WARN]: ${input}`;
streams.warn.write(`${input}\n`);
console.log(`${input}`.yellow.bold);
}
export function LOG_INFO(input: string): void {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
input = `[${getCurrentTimestamp()}] [INFO]: ${input}`;
streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}
}

View File

@ -1,5 +1,5 @@
import express from 'express';
import { getNEXDataByPID } from '@/util';
import type express from 'express';
export default async function authenticationMiddleware(request: express.Request, response: express.Response, next: express.NextFunction): Promise<void> {
if (request.pid) {

View File

@ -1,6 +1,7 @@
import express from 'express';
import { CTRSystemModel } from '@/types/common/user-agent-info';
import type { UserAgentInfo } from '@/types/common/user-agent-info';
import type express from 'express';
// import RequestException from '@/request-exception';
import { CTRSystemModel, UserAgentInfo } from '@/types/common/user-agent-info';
const FIRMWARE_PATCH_REGION_WIIU_REGEX = /(\d)([JEU])/;

View File

@ -1,5 +1,5 @@
import mongoose from 'mongoose';
import { ICECData, ICECDataMethods, CECDataModel } from '@/types/mongoose/cec-data';
import type { ICECData, ICECDataMethods, CECDataModel } from '@/types/mongoose/cec-data';
const CECDataSchema = new mongoose.Schema<ICECData, CECDataModel, ICECDataMethods>({
creator_pid: Number,

View File

@ -1,5 +1,5 @@
import mongoose from 'mongoose';
import { ICECSlot, ICECSlotMethods, CECSlotModel } from '@/types/mongoose/cec-slot';
import type { ICECSlot, ICECSlotMethods, CECSlotModel } from '@/types/mongoose/cec-slot';
const CECSlotSchema = new mongoose.Schema<ICECSlot, CECSlotModel, ICECSlotMethods>({
creator_pid: Number,

View File

@ -1,6 +1,6 @@
import mongoose from 'mongoose';
import { AutoIncrementID } from '@typegoose/auto-increment';
import { IFile, IFileMethods, FileModel } from '@/types/mongoose/file';
import { AutoIncrementID } from '@typegoose/auto-increment';
import type { IFile, IFileMethods, FileModel } from '@/types/mongoose/file';
const FileSchema = new mongoose.Schema<IFile, FileModel, IFileMethods>({
deleted: {

View File

@ -1,5 +1,5 @@
import mongoose from 'mongoose';
import { ITask, ITaskMethods, TaskModel } from '@/types/mongoose/task';
import type { ITask, ITaskMethods, TaskModel } from '@/types/mongoose/task';
const TaskSchema = new mongoose.Schema<ITask, TaskModel, ITaskMethods>({
deleted: {
@ -12,7 +12,7 @@ const TaskSchema = new mongoose.Schema<ITask, TaskModel, ITaskMethods>({
creator_pid: Number,
status: {
type: String,
enum : ['open'] // TODO - What else is there?
enum: ['open'] // TODO - What else is there?
},
title_id: String,
description: Number,

View File

@ -6,4 +6,4 @@ export default class RequestException extends Error {
this.status = status;
}
}
}

View File

@ -1,8 +1,3 @@
process.title = 'Pretendo - BOSS';
process.on('SIGTERM', () => {
process.exit(0);
});
import express from 'express';
import morgan from 'morgan';
import { connect as connectDatabase } from '@/database';
@ -10,10 +5,8 @@ import { startGRPCServer } from '@/services/grpc/server';
import RequestException from '@/request-exception';
import { LOG_INFO, LOG_SUCCESS } from '@/logger';
import { config } from '@/config-manager';
import parseUserAgentMiddleware from '@/middleware/parse-user-agent';
import authenticationMiddleware from '@/middleware/authentication';
import nppl from '@/services/nppl';
import npts from '@/services/npts';
import npdi from '@/services/npdi';
@ -21,6 +14,11 @@ import npfl from '@/services/npfl';
import npdl from '@/services/npdl';
import spr from '@/services/spr';
process.title = 'Pretendo - BOSS';
process.on('SIGTERM', () => {
process.exit(0);
});
const app = express();
LOG_INFO('Setting up Middleware');

View File

@ -1,9 +1,10 @@
import { CallContext, Status, ServerError } from 'nice-grpc';
import { DeleteFileRequest } from '@pretendonetwork/grpc/boss/delete_file';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { Status, ServerError } from 'nice-grpc';
import { getTaskFileByDataID } from '@/database';
import { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
import type { CallContext } from 'nice-grpc';
import type { DeleteFileRequest } from '@pretendonetwork/grpc/boss/delete_file';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
export async function deleteFile(request: DeleteFileRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty> {
// * This is asserted in authentication middleware, we know this is never null

View File

@ -1,9 +1,10 @@
import { CallContext, Status, ServerError } from 'nice-grpc';
import { DeleteTaskRequest } from '@pretendonetwork/grpc/boss/delete_task';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { Status, ServerError } from 'nice-grpc';
import { getTask } from '@/database';
import { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
import type { CallContext } from 'nice-grpc';
import type { DeleteTaskRequest } from '@pretendonetwork/grpc/boss/delete_task';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
export async function deleteTask(request: DeleteTaskRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty> {
// * This is asserted in authentication middleware, we know this is never null

View File

@ -1,4 +1,3 @@
import { BOSSServiceImplementation } from '@pretendonetwork/grpc/boss/boss_service';
import { listKnownBOSSApps } from '@/services/grpc/boss/list-known-boss-apps';
import { listTasks } from '@/services/grpc/boss/list-tasks';
import { registerTask } from '@/services/grpc/boss/register-task';
@ -8,6 +7,7 @@ import { listFiles } from '@/services/grpc/boss/list-files';
import { uploadFile } from '@/services/grpc/boss/upload-file';
import { updateFileMetadata } from '@/services/grpc/boss/update-file-metadata';
import { deleteFile } from '@/services/grpc/boss/delete-file';
import type { BOSSServiceImplementation } from '@pretendonetwork/grpc/boss/boss_service';
export const implementation: BOSSServiceImplementation = {
listKnownBOSSApps,

View File

@ -1,7 +1,7 @@
import { Status, ServerError } from 'nice-grpc';
import { ListFilesRequest, ListFilesResponse } from '@pretendonetwork/grpc/boss/list_files';
import { isValidCountryCode, isValidLanguage } from '@/util';
import { getTaskFiles } from '@/database';
import type { ListFilesRequest, ListFilesResponse } from '@pretendonetwork/grpc/boss/list_files';
const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/;

View File

@ -1,4 +1,4 @@
import { ListKnownBOSSAppsResponse } from '@pretendonetwork/grpc/boss/list_known_boss_apps';
import type { ListKnownBOSSAppsResponse } from '@pretendonetwork/grpc/boss/list_known_boss_apps';
export async function listKnownBOSSApps(): Promise<ListKnownBOSSAppsResponse> {
return {
@ -8,400 +8,400 @@ export async function listKnownBOSSApps(): Promise<ListKnownBOSSAppsResponse> {
titleId: '0005003010016000',
titleRegion: 'UNK',
name: 'Unknown',
tasks: [ 'olvinfo' ]
tasks: ['olvinfo']
},
{
bossAppId: 'VFoY6V7u7UUq1EG5',
titleId: '0005003010016100',
titleRegion: 'UNK',
name: 'Unknown',
tasks: [ 'olvinfo' ]
tasks: ['olvinfo']
},
{
bossAppId: '8MNOVprfNVAJjfCM',
titleId: '0005003010016200',
titleRegion: 'UNK',
name: 'Unknown',
tasks: [ 'olvinfo' ]
tasks: ['olvinfo']
},
{
bossAppId: 'v1cqzWykBKUg0rHQ',
titleId: '000500301001900A',
titleRegion: 'JPN',
name: 'Miiverse Post All',
tasks: [ 'solv' ]
tasks: ['solv']
},
{
bossAppId: 'bieC9ACJlisFg5xS',
titleId: '000500301001910A',
titleRegion: 'USA',
name: 'Miiverse Post All',
tasks: [ 'solv' ]
tasks: ['solv']
},
{
bossAppId: 'tOaQcoBLtPTgVN3Y',
titleId: '000500301001920A',
titleRegion: 'EUR',
name: 'Miiverse Post All',
tasks: [ 'solv' ]
tasks: ['solv']
},
{
bossAppId: 'HX8a16MMNn6i1z0Y',
titleId: '000500301001400A',
titleRegion: 'JPN',
name: 'Nintendo eShop',
tasks: [ 'wood1', 'woodBGM' ]
tasks: ['wood1', 'woodBGM']
},
{
bossAppId: '07E3nY6lAwlwrQRo',
titleId: '000500301001410A',
titleRegion: 'USA',
name: 'Nintendo eShop',
tasks: [ 'wood1', 'woodBGM' ]
tasks: ['wood1', 'woodBGM']
},
{
bossAppId: '8UsM86l8xgkjFk8z',
titleId: '000500301001420A',
titleRegion: 'EUR',
name: 'Nintendo eShop',
tasks: [ 'wood1', 'woodBGM' ]
tasks: ['wood1', 'woodBGM']
},
{
bossAppId: 'IXmFUqR2qenXfF61',
titleId: '0005001010066000',
titleRegion: 'ALL',
name: 'ECO Process',
tasks: [ 'promo1', 'promo2', 'promo3', 'push' ]
tasks: ['promo1', 'promo2', 'promo3', 'push']
},
{
bossAppId: 'BMQAm5iUVtPsJVsU',
titleId: '000500101004D000',
titleRegion: 'JPN',
name: 'Notifications',
tasks: [ 'sysmsg1', 'sysmsg2' ]
tasks: ['sysmsg1', 'sysmsg2']
},
{
bossAppId: 'LRmanFo4Tx3kEGDp',
titleId: '000500101004D100',
titleRegion: 'USA',
name: 'Notifications',
tasks: [ 'sysmsg1', 'sysmsg2' ]
tasks: ['sysmsg1', 'sysmsg2']
},
{
bossAppId: 'TZr27FE8wzKiEaTO',
titleId: '000500101004D200',
titleRegion: 'EUR',
name: 'Notifications',
tasks: [ 'sysmsg1', 'sysmsg2' ]
tasks: ['sysmsg1', 'sysmsg2']
},
{
bossAppId: 'JnIrm9c4E9JBmxBo',
titleId: '0005000010185200',
titleRegion: 'JPN',
name: 'NewスーパーマリオブラザーズU 無料お試し版 (New SUPER MARIO BROS. U (Trial))',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: 'dadlI27Ww8H2d56x',
titleId: '0005000010101C00',
titleRegion: 'JPN',
name: 'NewスーパーマリオブラザーズU (New SUPER MARIO BROS. U)',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: 'RaPn5saabzliYrpo',
titleId: '0005000010101D00',
titleRegion: 'USA',
name: 'New SUPER MARIO BROS. U',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: '14VFIK3rY2SP0WRE',
titleId: '0005000010101E00',
titleRegion: 'EUR',
name: 'New SUPER MARIO BROS. U',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: 'RbEQ44t2AocC4rvu',
titleId: '000500001014B700',
titleRegion: 'USA',
name: 'New SUPER MARIO BROS. U + New SUPER LUIGI U',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: '287gv3WZdxo1QRhl',
titleId: '000500001014B800',
titleRegion: 'EUR',
name: 'New SUPER MARIO BROS. U + New SUPER LUIGI U',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: 'bb6tOEckvgZ50ciH',
titleId: '0005000010162B00',
titleRegion: 'JPN',
name: 'スプラトゥーン (Splatoon)',
tasks: [ 'optdat2', 'schdat2', 'schdata' ]
tasks: ['optdat2', 'schdat2', 'schdata']
},
{
bossAppId: 'rjVlM7hUXPxmYQJh',
titleId: '0005000010176900',
titleRegion: 'USA',
name: 'Splatoon',
tasks: [ 'optdat2', 'schdat2', 'schdata', 'optdata2', 'schdata2' ]
tasks: ['optdat2', 'schdat2', 'schdata', 'optdata2', 'schdata2']
},
{
bossAppId: 'zvGSM4kOrXpkKnpT',
titleId: '0005000010176A00',
titleRegion: 'EUR',
name: 'Splatoon',
tasks: [ 'optdat2', 'schdat2', 'schdata', 'optdata' ]
tasks: ['optdat2', 'schdat2', 'schdata', 'optdata']
},
{
bossAppId: 'm8KJPtmPweiPuETE',
titleId: '000500001012F100',
titleRegion: 'JPN',
name: 'Wii Sports Club',
tasks: [ 'sp1_ans' ]
tasks: ['sp1_ans']
},
{
bossAppId: 'pO72Hi5uqf5yuNd8',
titleId: '0005000010144D00',
titleRegion: 'USA',
name: 'Wii Sports Club',
tasks: [ 'sp1_ans' ]
tasks: ['sp1_ans']
},
{
bossAppId: '4m8Xme1wKgzwslTJ',
titleId: '0005000010144E00',
titleRegion: 'EUR',
name: 'Wii Sports Club',
tasks: [ 'sp1_ans' ]
tasks: ['sp1_ans']
},
{
bossAppId: 'ESLqtAhxS8KQU4eu',
titleId: '000500001018DB00',
titleRegion: 'JPN',
name: 'Super Mario Maker (スーパーマリオメーカー)',
tasks: [ 'CHARA' ]
tasks: ['CHARA']
},
{
bossAppId: 'vGwChBW1ExOoHDsm',
titleId: '000500001018DC00',
titleRegion: 'USA',
name: 'Super Mario Maker',
tasks: [ 'CHARA' ]
tasks: ['CHARA']
},
{
bossAppId: 'IeUc4hQsKKe9rJHB',
titleId: '000500001018DD00',
titleRegion: 'EUA',
name: 'Super Mario Maker',
tasks: [ 'CHARA' ]
tasks: ['CHARA']
},
{
bossAppId: '4krJA4Gx3jF5nhQf',
titleId: '000500001012BC00',
titleRegion: 'JPN',
name: 'ピクミン3 (PIKMIN 3)',
tasks: [ 'histgrm' ]
tasks: ['histgrm']
},
{
bossAppId: '9jRZEoWYLc3OG9a8',
titleId: '000500001012BD00',
titleRegion: 'USA',
name: 'PIKMIN 3',
tasks: [ 'histgrm' ]
tasks: ['histgrm']
},
{
bossAppId: 'VWqUTspR5YtjDjxa',
titleId: '000500001012BE00',
titleRegion: 'EUR',
name: 'PIKMIN 3',
tasks: [ 'histgrm' ]
tasks: ['histgrm']
},
{
bossAppId: 'Ge1KtMu8tYlf4AUM',
titleId: '0005000010192000',
titleRegion: 'JPN',
name: '太鼓の達人 特盛り! (Taiko no Tatsujin Tokumori!)',
tasks: [ 'notice1' ]
tasks: ['notice1']
},
{
bossAppId: 'gycVtTzCouZmukZ6',
titleId: '0005000010110E00',
titleRegion: 'JPN',
name: '大乱闘スマッシュブラザーズ for Wii U (Super Smash Bros. for Wii U)',
tasks: [ 'NEWS', 'amiibo' ]
tasks: ['NEWS', 'amiibo']
},
{
bossAppId: 'o2Ug1pIp9Uhri6Nh',
titleId: '0005000010144F00',
titleRegion: 'USA',
name: 'Super Smash Bros. for Wii U',
tasks: [ 'amiibo', 'NEWS', 'friend', 'CONQ' ]
tasks: ['amiibo', 'NEWS', 'friend', 'CONQ']
},
{
bossAppId: 'n6rAJ1nnfC1Sgcpl',
titleId: '0005000010145000',
titleRegion: 'EUR',
name: 'Super Smash Bros. for Wii U',
tasks: [ 'amiibo', 'NEWS', 'friend', 'CONQ' ]
tasks: ['amiibo', 'NEWS', 'friend', 'CONQ']
},
{
bossAppId: 'CHUN6T1m7Xk4EBg4',
titleId: '00050000101DFF00',
titleRegion: 'JPN',
name: 'プチコンBIG (Petitcom BIG)',
tasks: [ 'ptcbnws' ]
tasks: ['ptcbnws']
},
{
bossAppId: 'zyXdCW9jGdi9rjaz',
titleId: '0005000010142200',
titleRegion: 'JPN',
name: 'NewスーパールイージU (New SUPER LUIGI U)',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: 'jPHLlJr2fJyTzffp',
titleId: '0005000010142300',
titleRegion: 'USA',
name: 'New SUPER LUIGI U',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: 'YsXB6IRGSI56tPxl',
titleId: '0005000010142400',
titleRegion: 'EUR',
name: 'New SUPER LUIGI U',
tasks: [ 'news' ]
tasks: ['news']
},
{
bossAppId: 'Lbqp9Sg1i0xUzFFa',
titleId: '0005000010113800',
titleRegion: 'EUR',
name: 'Zen Pinball 2',
tasks: [ 'PTS' ]
tasks: ['PTS']
},
{
bossAppId: 'DwU7n0FidGrLNiOo',
titleId: '000500001014D900',
titleRegion: 'JPN',
name: 'ぷよぷよテトリス (PUYOPUYOTETRIS)',
tasks: [ 'boss1', 'boss2', 'boss3' ]
tasks: ['boss1', 'boss2', 'boss3']
},
{
bossAppId: 'yIUkFmuGVkGP8pDb',
titleId: '0005000010132200',
titleRegion: 'JPN',
name: '太鼓の達人 Uば~じょん! (Taiko no Tatsujin Wii U version!)',
tasks: [ 'notice1' ]
tasks: ['notice1']
},
{
bossAppId: 'v4WRObSzD7VU3dcJ',
titleId: '00050000101D3000',
titleRegion: 'JPN',
name: '太鼓の達人 あつめて★ともだち大作戦! (Taiko no Tatsujin Atsumete★ TomodachiDaisakusen!)',
tasks: [ 'notice1' ]
tasks: ['notice1']
},
{
bossAppId: '3zDjXIA57bSceyaw',
titleId: '00050000101BEC00',
titleRegion: 'USA',
name: 'Star Fox Guard',
tasks: [ 'param' ]
tasks: ['param']
},
{
bossAppId: 'NL38jhExI2CQqhWd',
titleId: '00050000101CDB00',
titleRegion: 'JPN',
name: 'Splatoon Pre-Launch Review',
tasks: [ 'schdata' ]
tasks: ['schdata']
},
{
bossAppId: 'sE6KwEpQYyg6tdU7',
titleId: '00050000101CDC00',
titleRegion: 'USA',
name: 'Splatoon Pre-Launch Review',
tasks: [ 'schdata' ]
tasks: ['schdata']
},
{
bossAppId: 'pTKZ9q5KrCP3gBag',
titleId: '00050000101CDD00',
titleRegion: 'EUR',
name: 'Splatoon Pre-Launch Review',
tasks: [ 'schdata' ]
tasks: ['schdata']
},
{
bossAppId: 'CJT88RO008LAnD51',
titleId: '0005000010170600',
titleRegion: 'JPN',
name: '仮面ライダー バトライド・ウォーⅡ プレミアムTVMOVIEサウンドED. (KAMEN RIDER BATTRIDE WAR Ⅱ PREMIUM TV&MOVIE SOUND ED.)',
tasks: [ 'PE_GAK', 'PE_ZNG' ]
tasks: ['PE_GAK', 'PE_ZNG']
},
{
bossAppId: 'FyyMFzEByuQJc6sJ',
titleId: '0005000010135200',
titleRegion: 'USA',
name: 'Star Wars Pinball',
tasks: [ 'PTS' ]
tasks: ['PTS']
},
{
bossAppId: 'A4yyXWKZZUToFtrt',
titleId: '0005000010132A00',
titleRegion: 'EUR',
name: 'Star Wars Pinball',
tasks: [ 'PTS' ]
tasks: ['PTS']
},
{
bossAppId: 'HauaFQ1sPsnQ6rBj',
titleId: '0005000010171F00',
titleRegion: 'USA',
name: 'Pushmo World',
tasks: [ 'annouce' ]
tasks: ['annouce']
},
{
bossAppId: 'qDUeFmk0Az71nHyD',
titleId: '0005000010110900',
titleRegion: 'JPN',
name: 'NINJA GAIDEN 3: Razor\'s Edge',
tasks: [ 'DLCINFO' ]
tasks: ['DLCINFO']
},
{
bossAppId: 'yVsSPM2E0DEOxroT',
titleId: '0005000010110A00',
titleRegion: 'USA',
name: 'NINJA GAIDEN 3: Razor\'s Edge',
tasks: [ 'DLCINFO' ]
tasks: ['DLCINFO']
},
{
bossAppId: 'Xw6OvZkQofQ3O8Bi',
titleId: '0005000010110B00',
titleRegion: 'EUR',
name: 'Ninja Gaiden 3: Razor\'s Edge',
tasks: [ 'DLCINFO' ]
tasks: ['DLCINFO']
},
{
bossAppId: 'LUQX5swEjBUPQ8nR',
titleId: '0005000010110200',
titleRegion: 'USA',
name: 'WARRIORS OROCHI 3 Hyper(NA)',
tasks: [ 'OR2H000' ]
tasks: ['OR2H000']
},
{
bossAppId: 'y4pXrgLe0JGao3No',
titleId: '0005000010112B00',
titleRegion: 'EUR',
name: 'WARRIORS OROCHI 3 Hyper(EU)',
tasks: [ 'OR2H000' ]
tasks: ['OR2H000']
},
{
bossAppId: 'j01mRJ9sNe00MWPC',
titleId: '0005000010170700',
titleRegion: 'JPN',
name: '仮面ライダー バトライド・ウォーⅡ (KAMEN RIDER BATTRIDE WAR Ⅱ)',
tasks: [ 'CHR_GAK', 'CHR_ZNG' ]
tasks: ['CHR_GAK', 'CHR_ZNG']
}
]
};
}
}

View File

@ -1,5 +1,5 @@
import { ListTasksResponse } from '@pretendonetwork/grpc/boss/list_tasks';
import { getAllTasks } from '@/database';
import type { ListTasksResponse } from '@pretendonetwork/grpc/boss/list_tasks';
export async function listTasks(): Promise<ListTasksResponse> {
const tasks = await getAllTasks(false);

View File

@ -1,9 +1,10 @@
import { Status, ServerMiddlewareCall, CallContext, ServerError } from 'nice-grpc';
import { Status, ServerError } from 'nice-grpc';
import { config } from '@/config-manager';
import type { ServerMiddlewareCall, CallContext } from 'nice-grpc';
export async function* apiKeyMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response>,
context: CallContext,
context: CallContext
): AsyncGenerator<Response, Response | void, undefined> {
const apiKey: string | undefined = context.metadata.get('X-API-Key');
@ -12,4 +13,4 @@ export async function* apiKeyMiddleware<Request, Response>(
}
return yield* call.next(call.request, context);
}
}

View File

@ -1,13 +1,14 @@
import { Status, ServerMiddlewareCall, CallContext, ServerError } from 'nice-grpc';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { Status, ServerError } from 'nice-grpc';
import { getUserDataByToken } from '@/util';
import type { ServerMiddlewareCall, CallContext } from 'nice-grpc';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
const TOKEN_REQUIRED_PATHS = [
'/boss.BOSS/RegisterTask',
'/boss.BOSS/UpdateTask',
'/boss.BOSS/DeleteTask',
'/boss.BOSS/UploadFile',
'/boss.BOSS/DeleteFile',
'/boss.BOSS/DeleteFile'
];
export type AuthenticationCallContextExt = {
@ -16,7 +17,7 @@ export type AuthenticationCallContextExt = {
export async function* authenticationMiddleware<Request, Response>(
call: ServerMiddlewareCall<Request, Response, AuthenticationCallContextExt>,
context: CallContext,
context: CallContext
): AsyncGenerator<Response, Response | void, undefined> {
const token: string | undefined = context.metadata.get('X-Token')?.trim();
@ -50,4 +51,4 @@ export async function* authenticationMiddleware<Request, Response>(
throw new ServerError(Status.INVALID_ARGUMENT, message);
}
}
}

View File

@ -1,9 +1,10 @@
import { CallContext, Status, ServerError } from 'nice-grpc';
import { RegisterTaskRequest, RegisterTaskResponse } from '@pretendonetwork/grpc/boss/register_task';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { ServerError, Status } from 'nice-grpc';
import { getTask } from '@/database';
import { Task } from '@/models/task';
import { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import type { CallContext } from 'nice-grpc';
import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { RegisterTaskRequest, RegisterTaskResponse } from '@pretendonetwork/grpc/boss/register_task';
const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/;

View File

@ -1,10 +1,11 @@
import { CallContext, Status, ServerError } from 'nice-grpc';
import { UpdateFileMetadataRequest } from '@pretendonetwork/grpc/boss/update_file_metadata';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { Status, ServerError } from 'nice-grpc';
import { getTaskFileByDataID } from '@/database';
import { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
import { isValidFileNotifyCondition, isValidFileType } from '@/util';
import type { CallContext } from 'nice-grpc';
import type { UpdateFileMetadataRequest } from '@pretendonetwork/grpc/boss/update_file_metadata';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
export async function updateFileMetadata(request: UpdateFileMetadataRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty> {
// * This is asserted in authentication middleware, we know this is never null

View File

@ -1,9 +1,10 @@
import { CallContext, Status, ServerError } from 'nice-grpc';
import { UpdateTaskRequest } from '@pretendonetwork/grpc/boss/update_task';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { Status, ServerError } from 'nice-grpc';
import { getTask } from '@/database';
import { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
import type { CallContext } from 'nice-grpc';
import type { UpdateTaskRequest } from '@pretendonetwork/grpc/boss/update_task';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import type { Empty } from '@pretendonetwork/grpc/boss/google/protobuf/empty';
export async function updateTask(request: UpdateTaskRequest, context: CallContext & AuthenticationCallContextExt): Promise<Empty> {
// * This is asserted in authentication middleware, we know this is never null

View File

@ -1,12 +1,13 @@
import { CallContext, Status, ServerError } from 'nice-grpc';
import { UploadFileRequest, UploadFileResponse } from '@pretendonetwork/grpc/boss/upload_file';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { Status, ServerError } from 'nice-grpc';
import { encryptWiiU } from '@pretendonetwork/boss-crypto';
import { isValidCountryCode, isValidFileNotifyCondition, isValidFileType, isValidLanguage, md5, uploadCDNFile } from '@/util';
import { getTask, getTaskFile } from '@/database';
import { File } from '@/models/file';
import { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import { config } from '@/config-manager';
import type { CallContext } from 'nice-grpc';
import type { AuthenticationCallContextExt } from '@/services/grpc/boss/middleware/authentication-middleware';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { UploadFileRequest, UploadFileResponse } from '@pretendonetwork/grpc/boss/upload_file';
const BOSS_APP_ID_FILTER_REGEX = /^[A-Za-z0-9]*$/;
@ -172,7 +173,7 @@ export async function uploadFile(request: UploadFileRequest, context: CallContex
notifyOnNew: file.notify_on_new,
notifyLed: file.notify_led,
createdTimestamp: file.created,
updatedTimestamp: file.updated,
updatedTimestamp: file.updated
}
};
}

View File

@ -1,9 +1,10 @@
import { createServer, Server } from 'nice-grpc';
import { createServer } from 'nice-grpc';
import { BOSSDefinition } from '@pretendonetwork/grpc/boss/boss_service';
import { apiKeyMiddleware } from '@/services/grpc/boss/middleware/api-key-middleware';
import { authenticationMiddleware } from '@/services/grpc/boss/middleware/authentication-middleware';
import { implementation } from '@/services/grpc/boss/implementation';
import { config } from '@/config-manager';
import type { Server } from 'nice-grpc';
export async function startGRPCServer(): Promise<void> {
const server: Server = createServer();
@ -11,4 +12,4 @@ export async function startGRPCServer(): Promise<void> {
server.with(apiKeyMiddleware).with(authenticationMiddleware).add(BOSSDefinition, implementation);
await server.listen(`${config.grpc.boss.address}:${config.grpc.boss.port}`);
}
}

View File

@ -1,7 +1,7 @@
import path from 'node:path';
import fs from 'fs-extra';
import express from 'express';
import subdomain from 'express-subdomain';
import { fileErrCallback } from '@/util';
const npdi = express.Router();
@ -9,19 +9,18 @@ npdi.get('/p01/data/1/:titleHash/:dataID/:fileHash', (request, response) => {
const { titleHash, fileHash } = request.params;
const contentPath = path.normalize(`${__dirname}/../../cdn/content/encrypted/${titleHash}/${fileHash}`);
if (fs.existsSync(contentPath)) {
response.set('Content-Type', 'applicatoin/octet-stream');
response.set('Content-Disposition', 'attachment');
response.set('Content-Transfer-Encoding', 'binary');
response.set('Content-Type', 'applicatoin/octet-stream');
response.sendFile(contentPath);
} else {
response.sendStatus(404);
}
response.sendFile(contentPath, {
headers: {
// * The misspelling here is intentional, it's what the official server sets
'Content-Type': 'applicatoin/octet-stream',
'Content-Disposition': 'attachment',
'Content-Transfer-Encoding': 'binary'
}
}, fileErrCallback(response));
});
const router = express.Router();
router.use(subdomain('npdi.cdn', npdi));
export default router;
export default router;

View File

@ -1,7 +1,9 @@
import { Stream } from 'node:stream';
import express from 'express';
import subdomain from 'express-subdomain';
import { getTaskFile } from '@/database';
import { getCDNFileStream } from '@/util';
import { LOG_ERROR } from '@/logger';
const npdl = express.Router();
@ -16,9 +18,9 @@ npdl.get([
languageCode?: string;
fileName: string;
}, any, any, {
ap?: string;
tm?: string;
}>, response) => {
ap?: string;
tm?: string;
}>, response) => {
const { appID, taskID, countryCode, languageCode, fileName } = request.params;
const file = await getTaskFile(appID, taskID, fileName, countryCode, languageCode);
@ -37,7 +39,13 @@ npdl.get([
}
response.setHeader('Last-Modified', new Date(Number(file.updated)).toUTCString());
readStream.pipe(response);
Stream.pipeline(readStream, response, (err) => {
if (err) {
LOG_ERROR('Error with response stream: ' + err.message);
response.end();
}
});
});
const router = express.Router();

View File

@ -15,14 +15,14 @@ npfl.get('/p01/filelist/:appID/:taskID', async (request: express.Request<{
appID: string;
taskID: string;
}, any, any, {
c?: string;
l?: string;
a1?: string;
a2?: string;
a3?: string;
ap?: string;
tm?: string;
}>, response) => {
c?: string;
l?: string;
a1?: string;
a2?: string;
a3?: string;
ap?: string;
tm?: string;
}>, response) => {
for (const param in request.query) {
// * Beats me why the real server does this.
// * Just doing accurate emulation ¯\_(ツ)_/¯
@ -87,7 +87,7 @@ npfl.get('/p01/filelist/:appID/:taskID', async (request: express.Request<{
file.attribute2,
file.attribute3,
file.size,
file.updated/1000n // * Expects time as seconds, not milliseconds
file.updated / 1000n // * Expects time as seconds, not milliseconds
];
const line = `${params.join('\t')}\r\n`;
@ -111,4 +111,4 @@ const router = express.Router();
router.use(subdomain('npfl.c.app', npfl));
export default router;
export default router;

View File

@ -2,7 +2,7 @@ import xmlbuilder from 'xmlbuilder';
import moment from 'moment';
import express from 'express';
import subdomain from 'express-subdomain';
import { PolicyList } from '@/types/common/policylist';
import type { PolicyList } from '@/types/common/policylist';
const nppl = express.Router();
@ -186,4 +186,4 @@ const router = express.Router();
router.use(subdomain('nppl.c.app', nppl)); // * 3DS
router.use(subdomain('nppl.app', nppl)); // * WiiU
export default router;
export default router;

View File

@ -1,7 +1,7 @@
import path from 'node:path';
import fs from 'fs-extra';
import express from 'express';
import subdomain from 'express-subdomain';
import { fileErrCallback } from '@/util';
const npts = express.Router();
@ -9,28 +9,26 @@ npts.get('/p01/tasksheet/:id/:hash/:fileName', (request, response) => {
const { id, hash, fileName } = request.params;
const tasksheetPath = path.normalize(`${__dirname}/../../cdn/tasksheet/${id}/${hash}/${fileName}`);
if (fs.existsSync(tasksheetPath)) {
response.set('Content-Type', 'text/xml');
response.sendFile(tasksheetPath);
} else {
response.sendStatus(404);
}
response.sendFile(tasksheetPath, {
headers: {
'Content-Type': 'text/xml'
}
}, fileErrCallback(response));
});
npts.get('/p01/tasksheet/:id/:hash/:subfolder/:fileName', (request, response) => {
const { id, hash, subfolder, fileName } = request.params;
const tasksheetPath = path.normalize(`${__dirname}/../../cdn/tasksheet/${id}/${hash}/_subfolder/${subfolder}/${fileName}`);
if (fs.existsSync(tasksheetPath)) {
response.set('Content-Type', 'text/xml');
response.sendFile(tasksheetPath);
} else {
response.sendStatus(404);
}
response.sendFile(tasksheetPath, {
headers: {
'Content-Type': 'text/xml'
}
}, fileErrCallback(response));
});
const router = express.Router();
router.use(subdomain('npts.app', npts));
export default router;
export default router;

View File

@ -6,7 +6,8 @@ import { getDuplicateCECData, getRandomCECData } from '@/database';
import { getFriends } from '@/util';
import { CECData } from '@/models/cec-data';
import { CECSlot } from '@/models/cec-slot';
import { SendMode, SPRSlot } from '@/types/common/spr-slot';
import { SendMode } from '@/types/common/spr-slot';
import type { SPRSlot } from '@/types/common/spr-slot';
const spr = express.Router();
@ -33,7 +34,7 @@ function multipartParser(request: express.Request, response: express.Response, n
let fileBuffer = Buffer.alloc(0);
let fileName = '';
part.on('header', header => {
part.on('header', (header) => {
const contentDisposition = header['content-disposition' as keyof object];
const regexResult = RE_FILE_NAME.exec(contentDisposition);
@ -202,7 +203,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
sendMode,
gameID,
size,
data,
data
});
}
@ -234,7 +235,7 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
await CECSlot.findOneAndUpdate({
creator_pid: request.pid,
game_id: slotData.game_id
}, {latest_data_id: slotData.id}, {upsert: true});
}, { latest_data_id: slotData.id }, { upsert: true });
}
}
@ -264,7 +265,6 @@ spr.post('/relay/0', multipartParser, async (request, response) => {
return;
}
response.send(sprData);
});

View File

@ -1,7 +1,7 @@
import mongoose from 'mongoose';
import type mongoose from 'mongoose';
export interface DisabledFeatures {
s3: boolean
s3: boolean;
}
export interface Config {

View File

@ -14,4 +14,4 @@ export type PolicyList = {
Persistent?: boolean;
Revive?: boolean;
}[];
};
};

View File

@ -4,7 +4,7 @@ export enum CTRSystemModel {
KTR, // * New Nintendo 3DS
FTR, // * Nintendo 2DS
RED, // * New Nintendo 3DS XL
JAN // * New Nintendo 2DS XL
JAN // * New Nintendo 2DS XL
}
export type UserAgentInfo = {

View File

@ -13,4 +13,4 @@ declare module 'express-subdomain'{
subdomain: string,
fn: Router | ((req: Request, res: Response) => void | any)
): (req: Request, res: Response, next: () => void) => void | typeof fn;
}
}

View File

@ -1,4 +1,4 @@
import { GetNEXDataResponse } from '@pretendonetwork/grpc/account/get_nex_data_rpc';
import type { GetNEXDataResponse } from '@pretendonetwork/grpc/account/get_nex_data_rpc';
declare global {
namespace Express {

View File

@ -1,4 +1,4 @@
import { Model, HydratedDocument } from 'mongoose';
import type { Model, HydratedDocument } from 'mongoose';
export interface ICECData {
creator_pid: number;
@ -13,6 +13,6 @@ export interface ICECDataMethods {}
interface ICECDataQueryHelpers {}
export interface CECDataModel extends Model<ICECData, ICECDataQueryHelpers, ICECDataMethods> {}
export type CECDataModel = Model<ICECData, ICECDataQueryHelpers, ICECDataMethods>;
export type HydratedCECDataDocument = HydratedDocument<ICECData, ICECDataMethods>
export type HydratedCECDataDocument = HydratedDocument<ICECData, ICECDataMethods>;

View File

@ -1,4 +1,4 @@
import { Model, HydratedDocument } from 'mongoose';
import type { Model, HydratedDocument } from 'mongoose';
export interface ICECSlot {
creator_pid: number;
@ -10,6 +10,6 @@ export interface ICECSlotMethods {}
interface ICECSlotQueryHelpers {}
export interface CECSlotModel extends Model<ICECSlot, ICECSlotQueryHelpers, ICECSlotMethods> {}
export type CECSlotModel = Model<ICECSlot, ICECSlotQueryHelpers, ICECSlotMethods>;
export type HydratedCECSlotDocument = HydratedDocument<ICECSlot, ICECSlotMethods>
export type HydratedCECSlotDocument = HydratedDocument<ICECSlot, ICECSlotMethods>;

View File

@ -1,4 +1,4 @@
import { Model, HydratedDocument } from 'mongoose';
import type { Model, HydratedDocument } from 'mongoose';
export interface IFile {
deleted: boolean;
@ -26,6 +26,6 @@ export interface IFileMethods {}
interface IFileQueryHelpers {}
export interface FileModel extends Model<IFile, IFileQueryHelpers, IFileMethods> {}
export type FileModel = Model<IFile, IFileQueryHelpers, IFileMethods>;
export type HydratedFileDocument = HydratedDocument<IFile, IFileMethods>
export type HydratedFileDocument = HydratedDocument<IFile, IFileMethods>;

View File

@ -1,4 +1,4 @@
import { Model, HydratedDocument } from 'mongoose';
import type { Model, HydratedDocument } from 'mongoose';
export interface ITask {
deleted: boolean;
@ -6,17 +6,17 @@ export interface ITask {
in_game_id: string;
boss_app_id: string;
creator_pid: number;
status: 'open'; // TODO - Make this a union. What else is there?
status: 'open'; // TODO - Make this a union. What else is there?
title_id: string;
description: string;
created: bigint;
updated: bigint;
}
export interface ITaskMethods {}
export interface ITaskMethods { }
interface ITaskQueryHelpers {}
interface ITaskQueryHelpers { }
export interface TaskModel extends Model<ITask, ITaskQueryHelpers, ITaskMethods> {}
export type TaskModel = Model<ITask, ITaskQueryHelpers, ITaskMethods>;
export type HydratedTaskDocument = HydratedDocument<ITask, ITaskMethods>
export type HydratedTaskDocument = HydratedDocument<ITask, ITaskMethods>;

View File

@ -1,17 +1,23 @@
import crypto from 'node:crypto';
import path from 'node:path';
import { Readable } from 'node:stream';
import fs from 'fs-extra';
import { createChannel, createClient, Metadata } from 'nice-grpc';
import { GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
import { AccountClient, AccountDefinition } from '@pretendonetwork/grpc/account/account_service';
import { FriendsClient, FriendsDefinition } from '@pretendonetwork/grpc/friends/friends_service';
import { GetNEXDataResponse } from '@pretendonetwork/grpc/account/get_nex_data_rpc';
import { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import { GetUserFriendPIDsResponse } from '@pretendonetwork/grpc/friends/get_user_friend_pids_rpc';
import { AccountDefinition } from '@pretendonetwork/grpc/account/account_service';
import { FriendsDefinition } from '@pretendonetwork/grpc/friends/friends_service';
import { config, disabledFeatures } from '@/config-manager';
import { LOG_ERROR } from '@/logger';
import type { FriendsClient } from '@pretendonetwork/grpc/friends/friends_service';
import type { AccountClient } from '@pretendonetwork/grpc/account/account_service';
import type { S3Client } from '@aws-sdk/client-s3';
import type { GetNEXDataResponse } from '@pretendonetwork/grpc/account/get_nex_data_rpc';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { GetUserFriendPIDsResponse } from '@pretendonetwork/grpc/friends/get_user_friend_pids_rpc';
import type { NodeJsClient } from '@smithy/types';
import type { Response } from 'express';
import type { Readable } from 'node:stream';
let s3: S3;
let s3: NodeJsClient<S3Client>;
if (!disabledFeatures.s3) {
s3 = new S3({
@ -57,6 +63,21 @@ const VALID_FILE_NOTIFY_CONDITIONS = [
'app', 'account'
];
export function fileErrCallback(response: Response) {
return (err: NodeJS.ErrnoException): void => {
if (err) {
if (err.code === 'ENOENT') {
response.sendStatus(404);
} else {
if (!response.headersSent) {
response.status(500).send('Server Error');
}
LOG_ERROR('Error in sending file: ' + err.message);
}
}
};
}
export function md5(input: crypto.BinaryLike): string {
return crypto.createHash('md5').update(input).digest('hex');
}
@ -138,7 +159,7 @@ export async function getFriends(pid: number): Promise<GetUserFriendPIDsResponse
}
}
export async function getCDNFileStream(key: string): Promise<Readable | fs.ReadStream | null> {
export async function getCDNFileStream(key: string): Promise<Readable | null> {
try {
if (disabledFeatures.s3) {
return await getLocalCDNFile(key);
@ -152,9 +173,9 @@ export async function getCDNFileStream(key: string): Promise<Readable | fs.ReadS
return null;
}
return response.Body as Readable;
return response.Body;
}
} catch (error) {
} catch {
return null;
}
}

View File

@ -3,7 +3,7 @@
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"module": "commonjs",
"module": "ES2022",
"esModuleInterop": true,
"moduleResolution": "node",
"baseUrl": "src",
@ -11,9 +11,16 @@
"allowJs": true,
"target": "es2022",
"noEmitOnError": true,
"noImplicitAny": true,
"strictPropertyInitialization": true,
"verbatimModuleSyntax": true,
"paths": {
"@/*": ["./*"]
"@/*": [
"./*"
]
}
},
"include": ["src"]
"include": [
"src"
]
}