Move monitoring classes, use nxapi debug namespace for non-app/cli modules

This commit is contained in:
Samuel Elliott
2022-05-14 16:57:44 +01:00
parent 1efdfc9deb
commit b531fcced4
23 changed files with 1261 additions and 1205 deletions

View File

@@ -1,14 +1,11 @@
import * as path from 'path';
import * as fs from 'fs/promises';
import createDebug from 'debug';
import mkdirp from 'mkdirp';
import * as fs from 'fs/promises';
import * as path from 'path';
import fetch from 'node-fetch';
import * as crypto from 'crypto';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
import SplatNet2Api, { ShareColour } from '../../api/splatnet2.js';
import { Challenge, NicknameAndIcon, Records, Stages } from '../../api/splatnet2-types.js';
import { dumpChallenges, dumpHeroRecords, dumpProfileImage, dumpRecords } from '../../common/splatnet2/dump-records.js';
const debug = createDebug('cli:splatnet2:dump-records');
@@ -112,152 +109,3 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
await fs.writeFile(file, JSON.stringify(timeline, null, 4) + '\n', 'utf-8');
}
}
export async function dumpRecords(
directory: string, user_id: string,
records: Records, nickname_and_icon: NicknameAndIcon,
updated?: Date
) {
const latest_filename = 'splatnet2-records-' + user_id + '-latest.json';
const latest_file = path.join(directory, latest_filename);
if (updated) {
try {
const {sn2_updated_timestamp} = JSON.parse(await fs.readFile(latest_file, 'utf-8'));
if ((sn2_updated_timestamp * 1000) >= updated.getTime()) {
debug('Skipping user records, not updated');
return;
}
} catch (err) {}
}
const timestamp = Date.now();
const sn2_updated_timestamp = records.records.update_time;
const filename = 'splatnet2-records-' + user_id + '-' + timestamp + '.json';
const file = path.join(directory, filename);
const ni_filename = 'splatnet2-ni-' + user_id + '-' + timestamp + '.json';
const ni_file = path.join(directory, ni_filename);
debug('Writing records %s', filename);
await fs.writeFile(file, JSON.stringify(records, null, 4) + '\n', 'utf-8');
debug('Writing records %s', ni_filename);
await fs.writeFile(ni_file, JSON.stringify(nickname_and_icon, null, 4) + '\n', 'utf-8');
await fs.writeFile(latest_file, JSON.stringify({timestamp, sn2_updated_timestamp}, null, 4) + '\n', 'utf-8');
}
export async function dumpProfileImage(
splatnet: SplatNet2Api, directory: string, user_id: string,
stages: Stages, nickname_and_icon: NicknameAndIcon,
favourite_stage?: Arguments['favourite-stage'], favourite_colour?: Arguments['favourite-colour'],
updated?: Date
) {
const stage = favourite_stage ?
stages.stages.find(s => s.id === favourite_stage ||
s.name.toLowerCase() === favourite_stage?.toLowerCase()) :
stages.stages[0];
if (!stage) {
debug('Invalid favourite stage "%s"', favourite_stage);
}
// @ts-expect-error
if (!Object.values(ShareColour).includes(favourite_colour)) {
favourite_colour = ShareColour.PINK;
}
const latest_filename = 'splatnet2-profile-' + user_id + '-latest.json';
const latest_file = path.join(directory, latest_filename);
const etag_data = {
stage: (stage ?? stages.stages[0]).id,
stage_image: (stage ?? stages.stages[0]).image,
colour: favourite_colour,
name: nickname_and_icon.nickname,
image_url: nickname_and_icon.thumbnail_url,
};
const etag = crypto.createHash('sha256').update(JSON.stringify(etag_data)).digest('base64');
if (updated) {
try {
const {timestamp, etag: prev_etag} = JSON.parse(await fs.readFile(latest_file, 'utf-8'));
if (timestamp > updated.getTime() && etag === prev_etag) {
debug('Skipping profile image, not updated');
return;
}
} catch (err) {}
}
const timestamp = Date.now();
const filename = 'splatnet2-profile-' + user_id + '-' + timestamp + '.json';
const file = path.join(directory, filename);
const image_filename = 'splatnet2-profile-' + user_id + '-' + timestamp + '.png';
const image_file = path.join(directory, image_filename);
debug('Fetching profile image URL');
const share = await splatnet.shareProfile(stage?.id ?? stages.stages[0].id, favourite_colour as ShareColour);
debug('Fetching profile image');
const image_response = await fetch(share.url);
const image = await image_response.buffer();
debug('Writing profile image data %s', filename);
await fs.writeFile(file, JSON.stringify({
share,
stage: stage ?? stages.stages[0],
colour: favourite_colour,
}, null, 4) + '\n', 'utf-8');
debug('Writing profile image %s', image_filename);
await fs.writeFile(image_file, image);
await fs.writeFile(latest_file, JSON.stringify({timestamp, etag, etag_data}, null, 4) + '\n', 'utf-8');
}
async function dumpChallenges(
splatnet: SplatNet2Api, directory: string, user_id: string,
challenges: Challenge[], season: 1 | 2
) {
for (const challenge of challenges) {
const filename = 'splatnet2-challenge-' + user_id + '-' + challenge.key + '.json';
const file = path.join(directory, filename);
const image_filename = 'splatnet2-challenge-' + user_id + '-' + challenge.key + '.png';
const image_file = path.join(directory, image_filename);
try {
await fs.stat(file);
await fs.stat(image_file);
debug('Skipping challenge image %s, file already exists', challenge.key);
continue;
} catch (err) {}
debug('Fetching challenge image URL for %s', challenge.key);
const share = await splatnet.shareChallenge(challenge.key, season);
debug('Fetching challenge image for %s', challenge.key);
const image_response = await fetch(share.url);
const image = await image_response.buffer();
debug('Writing challenge image data %s', filename);
await fs.writeFile(file, JSON.stringify({share}, null, 4) + '\n', 'utf-8');
debug('Writing challenge image %s', filename);
await fs.writeFile(image_file, image);
}
}
async function dumpHeroRecords(splatnet: SplatNet2Api, directory: string, user_id: string) {
debug('Fetching hero records');
const hero = await splatnet.getHeroRecords();
const filename = 'splatnet2-hero-' + user_id + '-' + Date.now() + '.json';
const file = path.join(directory, filename);
debug('Writing hero records %s', filename);
await fs.writeFile(file, JSON.stringify(hero, null, 4) + '\n', 'utf-8');
}

View File

@@ -1,13 +1,10 @@
import * as path from 'path';
import createDebug from 'debug';
import mkdirp from 'mkdirp';
import * as fs from 'fs/promises';
import * as path from 'path';
import type { Arguments as ParentArguments } from '../splatnet2.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { getIksmToken } from './util.js';
import SplatNet2Api from '../../api/splatnet2.js';
import { NicknameAndIcon } from '../../api/splatnet2-types.js';
import fetch from 'node-fetch';
import { dumpCoopResults, dumpResults } from '../../common/splatnet2/dump-results.js';
const debug = createDebug('cli:splatnet2:dump-results');
@@ -73,187 +70,3 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
await dumpCoopResults(splatnet, directory, records.records.unique_id, updated);
}
}
export async function dumpResults(
splatnet: SplatNet2Api, directory: string, user_id: string,
images = false, summary_image = images, updated?: Date
) {
const latest_filename = 'splatnet2-results-summary-' + user_id + '-latest.json';
const latest_file = path.join(directory, latest_filename);
if (updated) {
try {
const {timestamp} = JSON.parse(await fs.readFile(latest_file, 'utf-8'));
if (timestamp > updated.getTime()) {
debug('Skipping battle results, user records not updated');
return;
}
} catch (err) {}
}
debug('Fetching battle results');
const results = await splatnet.getResults();
const timestamp = Date.now();
const summary_filename = 'splatnet2-results-summary-' + results.unique_id + '-' + timestamp + '.json';
const summary_file = path.join(directory, summary_filename);
debug('Writing summary %s', summary_filename);
await fs.writeFile(summary_file, JSON.stringify(results, null, 4) + '\n', 'utf-8');
if (summary_image) {
const filename = 'splatnet2-results-summary-image-' + results.unique_id + '-' + timestamp + '.json';
const file = path.join(directory, filename);
const image_filename = 'splatnet2-results-summary-image-' + results.unique_id + '-' + timestamp + '.png';
const image_file = path.join(directory, image_filename);
debug('Fetching battle results summary image URL');
const share = await splatnet.shareResultsSummary();
debug('Fetching battle results summary image');
const image_response = await fetch(share.url);
const image = await image_response.buffer();
debug('Writing battle results summary image data %s', filename);
await fs.writeFile(file, JSON.stringify({
share,
}, null, 4) + '\n', 'utf-8');
debug('Writing battle results summary image %s', filename);
await fs.writeFile(image_file, image);
}
const skipped = [];
const skipped_images = [];
for (const item of results.results) {
const filename = 'splatnet2-result-' + results.unique_id + '-' + item.battle_number + '-' + item.type + '.json';
const file = path.join(directory, filename);
try {
await fs.stat(file);
skipped.push(item.battle_number);
} catch (err) {
debug('Fetching battle result %d', item.battle_number);
const result = await splatnet.getResult(item.battle_number);
const nickname_and_icons: NicknameAndIcon[] = [];
for (const playerresult of [result.player_result, ...result.my_team_members, ...result.other_team_members]) {
const nickname_and_icon = await splatnet.getUserNicknameAndIcon([playerresult.player.principal_id]);
nickname_and_icons.push(...nickname_and_icon.nickname_and_icons);
}
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result,
nickname_and_icons,
}, null, 4) + '\n', 'utf-8');
}
if (images) {
const filename = 'splatnet2-result-image-' + results.unique_id + '-' +
item.battle_number + '-' + item.type + '.json';
const file = path.join(directory, filename);
const image_filename = 'splatnet2-result-image-' + results.unique_id + '-' +
item.battle_number + '-' + item.type + '.png';
const image_file = path.join(directory, image_filename);
try {
await fs.stat(file);
await fs.stat(image_file);
skipped_images.push(item.battle_number);
} catch (err) {
debug('Fetching battle results image URL');
const share = await splatnet.shareResult(item.battle_number);
debug('Fetching battle results image');
const image_response = await fetch(share.url);
const image = await image_response.buffer();
debug('Writing battle results image data %s', filename);
await fs.writeFile(file, JSON.stringify({
share,
}, null, 4) + '\n', 'utf-8');
debug('Writing battle results image %s', filename);
await fs.writeFile(image_file, image);
}
}
}
if (skipped.length) {
if (skipped.length === 1) debug('Skipped battle result %d, file already exists', skipped[0]);
else debug('Skipped battle results %s, files already exist', skipped.join(', '));
}
if (skipped_images.length) {
if (skipped_images.length === 1) debug('Skipped battle result image %d, file already exists',
skipped_images[0]);
else debug('Skipped battle result images %s, files already exist', skipped_images.join(', '));
}
await fs.writeFile(latest_file, JSON.stringify({timestamp}, null, 4) + '\n', 'utf-8');
}
export async function dumpCoopResults(splatnet: SplatNet2Api, directory: string, user_id: string, updated?: Date) {
const latest_filename = 'splatnet2-coop-summary-' + user_id + '-latest.json';
const latest_file = path.join(directory, latest_filename);
if (updated) {
try {
const {timestamp} = JSON.parse(await fs.readFile(latest_file, 'utf-8'));
if (timestamp > updated.getTime()) {
debug('Skipping coop results, user records not updated');
return;
}
} catch (err) {}
}
debug('Fetching coop results');
const results = await splatnet.getCoopResults();
const timestamp = Date.now();
const summary_filename = 'splatnet2-coop-summary-' + user_id + '-' + Date.now() + '.json';
const summary_file = path.join(directory, summary_filename);
debug('Writing summary %s', summary_filename);
await fs.writeFile(summary_file, JSON.stringify(results, null, 4) + '\n', 'utf-8');
const skipped = [];
for (const item of results.results) {
const filename = 'splatnet2-coop-result-' + user_id + '-' + item.job_id + '.json';
const file = path.join(directory, filename);
try {
await fs.stat(file);
skipped.push(item.job_id);
continue;
} catch (err) {}
debug('Fetching coop result %d', item.job_id);
const result = await splatnet.getCoopResult(item.job_id);
const nickname_and_icons: NicknameAndIcon[] = [];
for (const playerresult of [result.my_result, ...result.other_results]) {
const nickname_and_icon = await splatnet.getUserNicknameAndIcon([playerresult.pid]);
nickname_and_icons.push(...nickname_and_icon.nickname_and_icons);
}
debug('Writing %s', filename);
await fs.writeFile(file, JSON.stringify({
result,
nickname_and_icons,
}, null, 4) + '\n', 'utf-8');
}
if (skipped.length) {
if (skipped.length === 1) debug('Skipped coop result %d, file already exists', skipped[0]);
else debug('Skipped coop results %s, files already exist', skipped.join(', '));
}
await fs.writeFile(latest_file, JSON.stringify({timestamp}, null, 4) + '\n', 'utf-8');
}

View File

@@ -1,16 +1,9 @@
import createDebug from 'debug';
import persist from 'node-persist';
import mkdirp from 'mkdirp';
import * as fs from 'fs/promises';
import * as path from 'path';
import SplatNet2Api from '../../api/splatnet2.js';
import { getIksmToken, renewIksmToken } from './util.js';
import { ArgumentsCamelCase, Argv, initStorage, Loop, LoopResult, YargsArguments } from '../../util.js';
import { Records, Stages, WebServiceError } from '../../api/splatnet2-types.js';
import createDebug from 'debug';
import { getIksmToken } from './util.js';
import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js';
import { Arguments as ParentArguments } from '../splatnet2.js';
import { dumpCoopResults, dumpResults } from './dump-results.js';
import { dumpProfileImage, dumpRecords } from './dump-records.js';
import { ErrorResponse } from '../../api/util.js';
import { SplatNet2RecordsMonitor } from '../../common/splatnet2/monitor.js';
const debug = createDebug('cli:splatnet2:monitor');
@@ -107,98 +100,3 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
await i.loop();
}
}
export class SplatNet2RecordsMonitor extends Loop {
update_interval: number = 3 * 60; // 3 minutes in seconds
profile_image = true;
favourite_stage: string | undefined = undefined;
favourite_colour: string | undefined = undefined;
results = true;
results_summary_image = true;
result_images = true;
coop_results = true;
/** Prevents redownloading user records on the first loop run */
cached_records: Records | null = null;
auto_update_iksm_session = true;
constructor(
public storage: persist.LocalStorage,
public token: string,
public splatnet: SplatNet2Api,
public stages: Stages,
public directory: string,
public znc_proxy_url?: string
) {
super();
}
async init() {
await mkdirp(this.directory);
}
async hasChanged(records: Records) {
const latest_filename = 'splatnet2-records-' + records.records.unique_id + '-latest.json';
const latest_file = path.join(this.directory, latest_filename);
const updated = new Date(records.records.update_time * 1000);
try {
const {sn2_updated_timestamp} = JSON.parse(await fs.readFile(latest_file, 'utf-8'));
return (sn2_updated_timestamp * 1000) < updated.getTime();
} catch (err) {}
return true;
}
async update() {
const records = this.cached_records ?? await this.splatnet.getRecords();
this.cached_records = null;
if (!(await this.hasChanged(records))) {
debug('No data changed');
return;
}
const nickname_and_icons = await this.splatnet.getUserNicknameAndIcon([records.records.player.principal_id]);
// This will also update the splatnet2-records-...-latest.json file
await dumpRecords(this.directory, records.records.unique_id, records,
nickname_and_icons.nickname_and_icons[0]);
if (this.profile_image) {
await dumpProfileImage(this.splatnet, this.directory, records.records.unique_id, this.stages,
nickname_and_icons.nickname_and_icons[0],
this.favourite_stage, this.favourite_colour);
}
if (this.results) {
await dumpResults(this.splatnet, this.directory, records.records.unique_id,
this.result_images, this.results_summary_image);
}
if (this.coop_results) {
await dumpCoopResults(this.splatnet, this.directory, records.records.unique_id);
}
}
async handleError(err: Error | ErrorResponse<WebServiceError>): Promise<LoopResult> {
if ('response' in err && err.data?.code === 'AUTHENTICATION_ERROR') {
// Token expired
debug('Renewing iksm_session cookie');
if (!this.auto_update_iksm_session) {
throw new Error('iksm_session cookie expired');
}
await renewIksmToken(this.splatnet, this.storage, this.token, this.znc_proxy_url);
return LoopResult.OK_SKIP_INTERVAL;
} else {
throw err;
}
}
}