chore: remove update-rotations.mjs and replace it with documentation on how to use the CLI
Some checks failed
Build and Publish Docker Image / Build and Publish Docker Image (amd64) (push) Has been cancelled
Build and Publish Docker Image / Build and Publish Docker Image (arm64) (push) Has been cancelled

This commit is contained in:
mrjvs 2025-09-17 12:08:32 +02:00
parent 5518973557
commit 6b03e1c82f
4 changed files with 23 additions and 161 deletions

View File

@ -39,7 +39,6 @@ RUN chown node:node ${app_dir}
ENV NODE_ENV=production
USER node
COPY --chown=node:node update-rotation.mjs ${app_dir}
COPY --chown=node:node ./boss ${app_dir}
COPY --chown=node:node seeding ${app_dir}
COPY --chown=node:node package.json .

View File

@ -68,4 +68,20 @@ Configurations are loaded through environment variables. `.env` files are suppor
| `PN_BOSS_CLI_WIIU_HMAC_KEY` | The BOSS WiiU HMAC key, needs to be dumped from a console | Optional |
| `PN_BOSS_CLI_NPDI_URL` | The URL of the NPDI part the BOSS HTTP server, only needed when downloading | Optional |
| `PN_BOSS_CLI_NPDI_HOST` | The Host header for the NPDI requests. Use when you don't have NPDI exposed to the internet | Optional |
## Common CLI operations
```sh
# Download taskfile and decrypt
./boss file ls <BOSS_APP_ID> <TASK_ID> # View list of files and their IDs
./boss file view --decrypt <BOSS_APP_ID> <TASK_ID> <DATA_ID> > output.txt # Download file and decrypt
```
```sh
# Update splatoon rotations
# Run the following for all of these BOSS app ids:
# - bb6tOEckvgZ50ciH
# - rjVlM7hUXPxmYQJh
# - zvGSM4kOrXpkKnpT
./boss file create <BOSS_APP_ID> schdat2 --name VSSetting.byaml --type AppData --notify-new app --file <FILE_PATH_FOR_VSSETTING>
```

View File

@ -113,7 +113,9 @@ const createCmd = new Command('create')
.option('--country <country...>', 'Countries for this task file')
.option('--lang <language...>', 'Languages for this task file')
.option('--name-as-id', 'Force the name as the data ID')
.action(async (appId: string, taskId: string, opts: { name: string; country: string[]; lang: string[]; nameAsId?: boolean; type: string; file: string }) => {
.option('--notify-new <type...>', 'Add entry to NotifyNew')
.option('--notify-led', 'Enable NotifyLED')
.action(async (appId: string, taskId: string, opts: { name: string; country: string[]; notifyNew: string[]; notifyLed: boolean; lang: string[]; nameAsId?: boolean; type: string; file: string }) => {
const fileBuf = await fs.readFile(opts.file);
const ctx = getCliContext();
const { file } = await ctx.grpc.uploadFile({
@ -124,7 +126,9 @@ const createCmd = new Command('create')
supportedLanguages: opts.lang,
type: opts.type,
nameEqualsDataId: opts.nameAsId ?? false,
data: fileBuf
data: fileBuf,
notifyOnNew: opts.notifyNew,
notifyLed: opts.notifyLed
});
if (!file) {
console.log(`Failed to create file!`);

View File

@ -1,157 +0,0 @@
/* eslint-disable no-undef -- Tis a script */
import path from 'path';
import crypto from 'crypto';
import readline from 'readline';
import fs from 'fs-extra';
import dotenv from 'dotenv';
import xml from 'xml-js';
import { encryptWiiU } from '@pretendonetwork/boss-crypto';
dotenv.config();
function md5(input) {
return crypto.createHash('md5').update(input).digest('hex');
}
const BOSS_WIIU_AES_KEY_MD5_HASH = '5202ce5099232c3d365e28379790a919';
const BOSS_WIIU_HMAC_KEY_MD5_HASH = 'b4482fef177b0100090ce0dbeb8ce977';
const { PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY, PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY } = process.env;
if (!PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY || md5(PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY) !== BOSS_WIIU_AES_KEY_MD5_HASH) {
console.error('PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY is not set or does not match the expected value');
process.exit(1);
}
if (!PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY || md5(PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY) !== BOSS_WIIU_HMAC_KEY_MD5_HASH) {
console.error('PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY is not set or does not match the expected value');
process.exit(1);
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const askQuestion = (question) => {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer);
});
});
};
const rootDir = import.meta.dirname;
const sourceFile = path.join(rootDir, 'VSSetting.byaml');
const checksumFile = path.join(rootDir, 'cdn/VSSetting.byaml.checksum');
const exists = await fs.exists(sourceFile);
if (!exists) {
console.error('Source VSSetting.byaml file does not exist');
process.exit(1);
}
const sourceFileContents = await fs.readFile(sourceFile);
const stat = await fs.stat(sourceFile);
if (stat.mtime.toDateString() !== new Date().toDateString()) {
const answer = await askQuestion(`The source file was not updated today (Updated ${stat.mtime.toDateString()}). Do you want to continue? (y/n) `);
if (answer.toLowerCase() !== 'y') {
process.exit(0);
}
}
const checksumExists = await fs.exists(checksumFile);
const newChecksum = crypto.createHash('sha256').update(sourceFileContents).digest('hex');
console.log(`Checksum for source file is ${newChecksum}`);
if (checksumExists) {
const checksum = await fs.readFile(checksumFile, 'utf8');
if (checksum === newChecksum) {
const answer = await askQuestion('The source file has not changed since the last run. Do you want to continue? (y/n) ');
if (answer.toLowerCase() !== 'y') {
process.exit(0);
}
}
}
const titles = [
'bb6tOEckvgZ50ciH',
'rjVlM7hUXPxmYQJh',
'zvGSM4kOrXpkKnpT'
];
async function backupFile(filePath) {
const copyFilePath = path.join(path.dirname(filePath), `${path.basename(filePath)}.bak`);
const exists = await fs.exists(filePath);
if (!exists) {
console.log(`File ${filePath} does not exist, skipping backup...`);
return;
}
await fs.copyFile(filePath, copyFilePath);
console.log(`Backup created of ${filePath} at ${copyFilePath}`);
}
for (const title of titles) {
console.log(`\n --- Processing ${title} ---`);
const decryptedDir = path.join(rootDir, `cdn/content/decrypted/${title}`);
const encryptedDir = path.join(rootDir, `cdn/content/encrypted/${title}`);
const taskSheetDir = path.join(rootDir, `cdn/tasksheet/1/${title}`);
await fs.ensureDir(decryptedDir);
await fs.ensureDir(encryptedDir);
await fs.ensureDir(taskSheetDir);
const decryptedFilePath = path.join(decryptedDir, 'VSSetting.byaml');
await backupFile(decryptedFilePath);
await fs.copyFile(sourceFile, decryptedFilePath);
const encryptedContents = encryptWiiU(decryptedFilePath, PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY, PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY);
const hash = crypto.createHash('md5').update(encryptedContents).digest('hex');
console.log(`Hash for title ${title} is ${hash}`);
const encryptedFilePath = path.join(encryptedDir, hash);
await fs.writeFile(encryptedFilePath, encryptedContents);
console.log(`Encrypted file created at ${encryptedFilePath}`);
const taskSheetFilePath = path.join(taskSheetDir, 'schdat2');
await backupFile(taskSheetFilePath);
const tasksheetContents = await fs.readFile(taskSheetFilePath, 'utf8');
const xmlContents = xml.xml2js(tasksheetContents, { compact: true });
const dataId = parseInt(xmlContents.TaskSheet.Files.File.DataId._text);
if (isNaN(dataId)) {
console.error(`DataId for title ${title} is not a number, skipping...`);
continue;
}
console.log(`DataId for title ${title} is ${dataId}`);
const newDataId = dataId + 1;
console.log(`New DataId for title ${title} is ${newDataId}`);
xmlContents.TaskSheet.Files.File.DataId._text = newDataId.toString();
const size = encryptedContents.length;
const oldSize = parseInt(xmlContents.TaskSheet.Files.File.Size._text);
if (size === oldSize) {
console.log(`Size for title ${title} is already updated, skipping update...`);
} else {
console.log(`Old size for title ${title} is ${oldSize}`);
console.log(`New size for title ${title} is ${size}`);
xmlContents.TaskSheet.Files.File.Size._text = size.toString();
}
const oldUrl = xmlContents.TaskSheet.Files.File.Url._text;
const newUrl = `https://npdi.cdn.pretendo.cc/p01/data/1/${title}/${newDataId}/${hash}`;
console.log(`Old URL for title ${title} is ${oldUrl}`);
console.log(`New URL for title ${title} is ${newUrl}`);
xmlContents.TaskSheet.Files.File.Url._text = newUrl;
const newXmlContents = xml.js2xml(xmlContents, { spaces: 2, compact: true });
await fs.writeFile(taskSheetFilePath, newXmlContents);
console.log(`Tasksheet file updated at ${taskSheetFilePath}`);
}
rl.close();
console.log('All tasks completed successfully!');
await fs.writeFile(checksumFile, newChecksum);