mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-10-19 08:07:44 -05:00
Add command/menu item to export log files
This commit is contained in:
@@ -69,7 +69,7 @@ export const menus = {
|
||||
friend: {
|
||||
presence_online: 'Online',
|
||||
game_first_played: 'Zuerst gespielt: {{date, datetime}}',
|
||||
|
||||
|
||||
game_play_time_h: 'Spielzeit: $t(friend.hours, {"count": {{hours}}})',
|
||||
game_play_time_hm: 'Spielzeit: $t(friend.hours, {"count": {{hours}}}), $t(friend.minutes, {"count": {{minutes}}})',
|
||||
game_play_time_m: 'Spielzeit: $t(friend.minutes, {"count": {{minutes}}})',
|
||||
@@ -109,7 +109,7 @@ export const na_auth = {
|
||||
|
||||
text: `Um Zugriff auf die API der Nintendo Switch Online App zu erhalten, muss nxapi einige Daten an Drittanbieter-APIs senden. Dieser Schritt wird benötigt, um Daten zu generieren, damit Nintendo denkt, dass du die echte Nintendo Switch Online App verwendest.
|
||||
Standardmäßig wird nxapi-znca-api.fancy.org.uk oder api.imink.app benutzt. Ein anderer Service kann ebenfalls benutzt werden, indem eine Umgebungsvariable gesetzt wird. Die standardmäßige API könnte sich jederzeit ohne Hinweis ändern, wenn du keinen spezifischen Service erzwingst.
|
||||
|
||||
|
||||
Die gesendeten Daten beinhalten:
|
||||
|
||||
- Deine Nintendo Account ID
|
||||
|
||||
@@ -14,6 +14,7 @@ export const app_menu = {
|
||||
learn_more: 'Learn More',
|
||||
learn_more_github: 'Learn More (GitHub)',
|
||||
search_issues: 'Search Issues',
|
||||
export_logs: 'Export Logs',
|
||||
|
||||
refresh: 'Refresh',
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { i18n } from 'i18next';
|
||||
import { GITHUB_MIRROR_URL, GITLAB_URL, ISSUES_URL } from '../../common/constants.js';
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron';
|
||||
import { App } from './index.js';
|
||||
import { createLogArchive } from './support.js';
|
||||
|
||||
let appinstance: App | null;
|
||||
|
||||
@@ -59,6 +60,12 @@ function createAppMenuItems(i18n?: i18n) {
|
||||
await shell.openExternal(ISSUES_URL);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n?.t('app_menu:export_logs') ?? 'Export Logs',
|
||||
click: () => {
|
||||
createLogArchive();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ function buildUserMenu(app: App, user: NintendoAccountUser, nso?: CurrentUser, m
|
||||
new MenuItem({label: t('discord_disable')!,
|
||||
click: () => app.menu?.setActiveDiscordPresenceUser(null)}),
|
||||
] : monitor?.presence_user ? [
|
||||
new MenuItem({label: t('discord_enabled_for', {name:
|
||||
new MenuItem({label: t('discord_enabled_for', {name:
|
||||
monitor.user?.friends.result.friends.find(f => f.nsaId === monitor.presence_user)?.name ??
|
||||
monitor.presence_user})!,
|
||||
enabled: false}),
|
||||
|
||||
88
src/app/main/support.ts
Normal file
88
src/app/main/support.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createWriteStream, WriteStream } from 'node:fs';
|
||||
import { app, dialog, Notification, shell } from 'electron';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { generateEncryptedLogArchive } from '../../util/support.js';
|
||||
import { join } from 'node:path';
|
||||
import { showErrorDialog } from './util.js';
|
||||
|
||||
const debug = createDebug('app:main:support');
|
||||
|
||||
export async function createLogArchive() {
|
||||
let start_notification: Notification | null = null;
|
||||
|
||||
try {
|
||||
const { default: config } = await import('../../common/remote-config.js');
|
||||
|
||||
if (!config.log_encryption_key) {
|
||||
throw new Error('No log encryption key in remote configuration');
|
||||
}
|
||||
|
||||
const default_name = 'nxapi-logs-' +
|
||||
new Date().toISOString().replace(/[-:Z]/g, '').replace(/\.\d+/, '').replace(/T/, '-') +
|
||||
'.tar.gz';
|
||||
|
||||
const result = await dialog.showSaveDialog({
|
||||
defaultPath: join(app.getPath('downloads'), default_name),
|
||||
filters: [{name: 'Tape archive (encrypted)', extensions: ['tgz', 'tar.gz']}],
|
||||
});
|
||||
|
||||
if (result.canceled) return;
|
||||
|
||||
const out = await createOutputStream(result.filePath);
|
||||
|
||||
debug('creating log archive');
|
||||
|
||||
start_notification = new Notification({
|
||||
title: 'Creating log archive',
|
||||
});
|
||||
start_notification.show();
|
||||
|
||||
const key = Buffer.from(config.log_encryption_key, 'base64url');
|
||||
const [encrypt] = await generateEncryptedLogArchive(key);
|
||||
|
||||
encrypt.pipe(out);
|
||||
|
||||
await new Promise((rs, rj) => {
|
||||
encrypt.on('end', rs);
|
||||
encrypt.on('error', rj);
|
||||
});
|
||||
|
||||
debug('done');
|
||||
|
||||
start_notification.close();
|
||||
|
||||
new Notification({
|
||||
title: 'Created log archive',
|
||||
}).show();
|
||||
|
||||
shell.showItemInFolder(result.filePath);
|
||||
} catch (err) {
|
||||
start_notification?.close();
|
||||
|
||||
showErrorDialog({
|
||||
message: 'Error creating log archive',
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createOutputStream(path: string) {
|
||||
return new Promise<WriteStream>((rs, rj) => {
|
||||
const out = createWriteStream(path);
|
||||
|
||||
const onready = () => {
|
||||
out.removeListener('ready', onready);
|
||||
out.removeListener('error', onerror);
|
||||
rs(out);
|
||||
};
|
||||
const onerror = () => {
|
||||
out.removeListener('ready', onready);
|
||||
out.removeListener('error', onerror);
|
||||
rs(out);
|
||||
};
|
||||
|
||||
out.on('ready', onready);
|
||||
out.on('error', onerror);
|
||||
});
|
||||
}
|
||||
@@ -7,3 +7,5 @@ export * as remoteConfig from './remote-config.js';
|
||||
export * as storage from './storage.js';
|
||||
export * as presenceEmbedRender from './presence-embed-render.js';
|
||||
export * as presenceEmbedServer from './presence-embed-server.js';
|
||||
export * as logArchive from './log-archive.js';
|
||||
export * as decryptLogArchive from './decrypt-log-archive.js';
|
||||
|
||||
34
src/cli/util/decrypt-log-archive.ts
Normal file
34
src/cli/util/decrypt-log-archive.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { DecryptStream } from '@samuelthomas2774/saltpack';
|
||||
import tweetnacl from 'tweetnacl';
|
||||
import type { Arguments as ParentArguments } from './index.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
|
||||
const debug = createDebug('cli:util:decrypt-log-archive');
|
||||
|
||||
export const command = 'decrypt-log-archive';
|
||||
export const desc = null;
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs;
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
if (!process.env.NXAPI_SUPPORT_SECRET_KEY) {
|
||||
throw new Error('Missing NXAPI_SUPPORT_SECRET_KEY environment variable');
|
||||
}
|
||||
|
||||
const key = Buffer.from(process.env.NXAPI_SUPPORT_SECRET_KEY, 'base64url');
|
||||
const keypair = tweetnacl.box.keyPair.fromSecretKey(key);
|
||||
|
||||
const decrypt = new DecryptStream(keypair);
|
||||
|
||||
decrypt.pipe(process.stdout);
|
||||
|
||||
debug('decrypting tar.gz to stdout');
|
||||
|
||||
process.stdin.pipe(decrypt);
|
||||
}
|
||||
70
src/cli/util/log-archive.ts
Normal file
70
src/cli/util/log-archive.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createWriteStream, WriteStream } from 'node:fs';
|
||||
import type { Arguments as ParentArguments } from './index.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { generateEncryptedLogArchive } from '../../util/support.js';
|
||||
|
||||
const debug = createDebug('cli:util:log-archive');
|
||||
|
||||
export const command = 'log-archive [output]';
|
||||
export const desc = 'Create an encrypted log archive for support';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('output', {
|
||||
describe: 'Output path',
|
||||
type: 'string',
|
||||
});
|
||||
}
|
||||
|
||||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const { default: config } = await import('../../common/remote-config.js');
|
||||
|
||||
if (!config.log_encryption_key) {
|
||||
throw new Error('No log encryption key in remote configuration');
|
||||
}
|
||||
|
||||
const out = await createOutputStream(argv.output);
|
||||
|
||||
debug('creating log archive');
|
||||
|
||||
const key = Buffer.from(config.log_encryption_key, 'base64url');
|
||||
const [encrypt] = await generateEncryptedLogArchive(key);
|
||||
|
||||
encrypt.pipe(out);
|
||||
|
||||
encrypt.on('end', () => {
|
||||
debug('done');
|
||||
});
|
||||
}
|
||||
|
||||
async function createOutputStream(path?: string) {
|
||||
if (!path && process.stdout.isTTY) {
|
||||
console.error('No output path set but stdout is a TTY. Run `nxapi util log-archive -` to force output to a terminal.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!path || path === '-') {
|
||||
return process.stdout;
|
||||
}
|
||||
|
||||
return new Promise<WriteStream>((rs, rj) => {
|
||||
const out = createWriteStream(path);
|
||||
|
||||
const onready = () => {
|
||||
out.removeListener('ready', onready);
|
||||
out.removeListener('error', onerror);
|
||||
rs(out);
|
||||
};
|
||||
const onerror = () => {
|
||||
out.removeListener('ready', onready);
|
||||
out.removeListener('error', onerror);
|
||||
rs(out);
|
||||
};
|
||||
|
||||
out.on('ready', onready);
|
||||
out.on('error', onerror);
|
||||
});
|
||||
}
|
||||
@@ -251,6 +251,8 @@ export interface NxapiRemoteConfig {
|
||||
*/
|
||||
require_version: string[];
|
||||
|
||||
log_encryption_key?: string;
|
||||
|
||||
// If null the API should not be used
|
||||
coral: CoralRemoteConfig | null;
|
||||
coral_auth: {
|
||||
|
||||
119
src/util/support.ts
Normal file
119
src/util/support.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { resolve } from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { EncryptStream } from '@samuelthomas2774/saltpack';
|
||||
import { Header, list, Pack, ReadEntry } from 'tar';
|
||||
import createDebug from './debug.js';
|
||||
import { dev, docker, git, paths, product, release, version } from './product.js';
|
||||
import { getUserAgent } from './useragent.js';
|
||||
|
||||
const debug = createDebug('nxapi:util:support');
|
||||
|
||||
export async function createLogArchive(log_path = paths.log) {
|
||||
const tar = new Pack({
|
||||
gzip: true,
|
||||
cwd: log_path,
|
||||
preservePaths: true,
|
||||
onWriteEntry: e => {
|
||||
if (e.path === 'info.json') return;
|
||||
if (e.path.startsWith(log_path)) {
|
||||
e.path = e.path.substring(log_path.length + 1);
|
||||
}
|
||||
e.path = 'log/' + e.path;
|
||||
},
|
||||
});
|
||||
|
||||
tar.on('error', err => {
|
||||
debug('archive error', err);
|
||||
});
|
||||
|
||||
const data = getSystemInfo();
|
||||
tar.add(createJsonFileEntry(data, 'info.json'));
|
||||
|
||||
await addFiles(tar, log_path);
|
||||
|
||||
tar.end();
|
||||
|
||||
return tar;
|
||||
}
|
||||
|
||||
async function addFiles(tar: Pack, file: string) {
|
||||
if (file.charAt(0) === '@') {
|
||||
await list({
|
||||
file: resolve(tar.cwd, file.slice(1)),
|
||||
noResume: true,
|
||||
onReadEntry: entry => {
|
||||
tar.add(entry);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
tar.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemInfo() {
|
||||
return {
|
||||
version,
|
||||
created_at: new Date(),
|
||||
product: {
|
||||
release,
|
||||
docker,
|
||||
git,
|
||||
dev,
|
||||
product,
|
||||
user_agent: getUserAgent(),
|
||||
},
|
||||
environment: {
|
||||
execPath: process.execPath,
|
||||
execArgv: process.execArgv,
|
||||
argv: process.argv,
|
||||
env: process.env,
|
||||
paths,
|
||||
},
|
||||
node: {
|
||||
versions: process.versions,
|
||||
features: process.features,
|
||||
},
|
||||
system: {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
uname: os.version(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createJsonFileEntry(data: unknown, name: string, date = new Date()) {
|
||||
debug('adding file', name, data);
|
||||
|
||||
const buffer = Buffer.from(JSON.stringify(data, null, 4) + '\n', 'utf-8');
|
||||
|
||||
const header = new Header({
|
||||
path: name,
|
||||
mode: 0o600,
|
||||
uid: process.getuid?.() ?? 0,
|
||||
gid: process.getgid?.() ?? 0,
|
||||
ctime: date,
|
||||
mtime: date,
|
||||
size: buffer.length,
|
||||
type: 'File',
|
||||
});
|
||||
|
||||
const entry = new ReadEntry(header);
|
||||
|
||||
entry.end(buffer);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
export async function generateEncryptedLogArchive(key: Uint8Array, log_path = paths.log) {
|
||||
const encrypt = new EncryptStream(null, [key]);
|
||||
|
||||
encrypt.on('error', err => {
|
||||
debug('encrypt error', err);
|
||||
});
|
||||
|
||||
const tar = await createLogArchive();
|
||||
|
||||
tar.pipe(encrypt);
|
||||
|
||||
return [encrypt, tar] as const;
|
||||
}
|
||||
Reference in New Issue
Block a user