mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-08-24 03:25:21 -05:00
Add Discord Rich Presence and user/friend presence notifications to the Electron app
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import { app, BrowserWindow, ipcMain } from '../electron.js';
|
||||
import { app, BrowserWindow, ipcMain, nativeImage, Notification } from '../electron.js';
|
||||
import * as path from 'path';
|
||||
import { EventEmitter } from 'events';
|
||||
import createDebug from 'debug';
|
||||
import * as persist from 'node-persist';
|
||||
import { initStorage, paths } from '../../util.js';
|
||||
import MenuApp from './menu.js';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import dotenv from 'dotenv';
|
||||
import dotenvExpand from 'dotenv-expand';
|
||||
import { getToken, initStorage, paths } from '../../util.js';
|
||||
import MenuApp from './menu.js';
|
||||
import { ZncDiscordPresence } from '../../cli/nso/presence.js';
|
||||
import { WebServiceIpc } from './webservices.js';
|
||||
import { CurrentUser, Friend, Game } from '../../api/znc-types.js';
|
||||
import { NotificationManager } from '../../cli/nso/notify.js';
|
||||
|
||||
const debug = createDebug('app:main');
|
||||
|
||||
@@ -60,7 +63,8 @@ app.whenReady().then(async () => {
|
||||
BrowserWindow.getAllWindows().forEach(w => w.webContents.send(channel, ...args));
|
||||
store.on('update-nintendo-accounts', () => sendToAllWindows('nxapi:accounts:shouldrefresh'));
|
||||
|
||||
const menu = new MenuApp(store);
|
||||
const monitors = new PresenceMonitorManager(store);
|
||||
const menu = new MenuApp(store, monitors);
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
@@ -84,3 +88,151 @@ export class Store extends EventEmitter {
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
export class PresenceMonitorManager {
|
||||
monitors: EmbeddedPresenceMonitor[] = [];
|
||||
notifications = new ElectronNotificationManager();
|
||||
|
||||
constructor(
|
||||
public store: Store
|
||||
) {}
|
||||
|
||||
async start(id: string, callback?: (monitor: EmbeddedPresenceMonitor, firstRun: boolean) => void) {
|
||||
debug('Starting monitor', id);
|
||||
|
||||
const token = id.length === 16 ? await this.store.storage.getItem('NintendoAccountToken.' + id) : id;
|
||||
if (!token) throw new Error('No token for this user');
|
||||
|
||||
const {nso, data} = await getToken(this.store.storage, token, process.env.ZNC_PROXY_URL);
|
||||
|
||||
const existing = this.monitors.find(m => m.data.user.id === data.user.id);
|
||||
if (existing) {
|
||||
callback?.call(null, existing, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const i = new EmbeddedPresenceMonitor({
|
||||
userNotifications: false,
|
||||
friendNotifications: false,
|
||||
updateInterval: 60,
|
||||
friendCode: undefined,
|
||||
showInactivePresence: false,
|
||||
showEvent: false,
|
||||
friendNsaid: undefined,
|
||||
}, this.store.storage, token, nso, data);
|
||||
|
||||
i.notifications = this.notifications;
|
||||
i.presence_user = null;
|
||||
|
||||
this.monitors.push(i);
|
||||
|
||||
callback?.call(null, i, true);
|
||||
|
||||
i.enable();
|
||||
}
|
||||
|
||||
async stop(id: string) {
|
||||
let index;
|
||||
while ((index = this.monitors.findIndex(m => m.data.user.id === id)) >= 0) {
|
||||
const i = this.monitors[index];
|
||||
|
||||
this.monitors.splice(index, 1);
|
||||
|
||||
i.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EmbeddedPresenceMonitor extends ZncDiscordPresence {
|
||||
notifications = new ElectronNotificationManager();
|
||||
|
||||
enable() {
|
||||
if (this._running !== 0) return;
|
||||
this._run();
|
||||
}
|
||||
|
||||
disable() {
|
||||
this._running = 0;
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this._running !== 0;
|
||||
}
|
||||
|
||||
private _running = 0;
|
||||
|
||||
private async _run() {
|
||||
this._running++;
|
||||
const i = this._running;
|
||||
|
||||
try {
|
||||
await this.loop(true);
|
||||
|
||||
while (i === this._running) {
|
||||
await this.loop();
|
||||
}
|
||||
|
||||
if (this._running === 0) {
|
||||
// Run one more time after the loop ends
|
||||
const result = await this.loopRun();
|
||||
}
|
||||
|
||||
debug('Monitor for user %s finished', this.data.nsoAccount.user.name);
|
||||
} finally {
|
||||
this._running = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ElectronNotificationManager extends NotificationManager {
|
||||
private async getNativeImageFromUrl(url: string) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const image = await response.buffer();
|
||||
return nativeImage.createFromBuffer(image);
|
||||
} catch (err) {}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, ir?: boolean) {
|
||||
const currenttitle = friend.presence.game as Game;
|
||||
|
||||
new Notification({
|
||||
title: friend.name,
|
||||
body: 'Playing ' + currenttitle.name +
|
||||
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
|
||||
icon: await this.getNativeImageFromUrl(friend.imageUri),
|
||||
}).show();
|
||||
}
|
||||
|
||||
async onFriendOffline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, ir?: boolean) {
|
||||
new Notification({
|
||||
title: friend.name,
|
||||
body: 'Offline',
|
||||
icon: await this.getNativeImageFromUrl(friend.imageUri),
|
||||
}).show();
|
||||
}
|
||||
|
||||
async onFriendPlayingChangeTitle(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, ir?: boolean) {
|
||||
const currenttitle = friend.presence.game as Game;
|
||||
|
||||
new Notification({
|
||||
title: friend.name,
|
||||
body: 'Playing ' + currenttitle.name +
|
||||
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
|
||||
icon: await this.getNativeImageFromUrl(friend.imageUri),
|
||||
}).show();
|
||||
}
|
||||
|
||||
async onFriendTitleStateChange(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, ir?: boolean) {
|
||||
const currenttitle = friend.presence.game as Game;
|
||||
|
||||
new Notification({
|
||||
title: friend.name,
|
||||
body: 'Playing ' + currenttitle.name +
|
||||
(currenttitle.sysDescription ? '\n' + currenttitle.sysDescription : ''),
|
||||
icon: await this.getNativeImageFromUrl(friend.imageUri),
|
||||
}).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { app, dialog, Menu, Tray, nativeImage, MenuItem } from '../electron.js';
|
||||
import { addNsoAccount, addPctlAccount } from './na-auth.js';
|
||||
import { Store } from './index.js';
|
||||
import { PresenceMonitorManager, Store } from './index.js';
|
||||
import { getToken, SavedMoonToken, SavedToken } from '../../util.js';
|
||||
import { WebService } from '../../api/znc-types.js';
|
||||
import openWebService from './webservices.js';
|
||||
@@ -8,7 +8,7 @@ import openWebService from './webservices.js';
|
||||
export default class MenuApp {
|
||||
tray: Tray;
|
||||
|
||||
constructor(readonly store: Store) {
|
||||
constructor(readonly store: Store, readonly monitors: PresenceMonitorManager) {
|
||||
const icon = nativeImage.createEmpty();
|
||||
|
||||
this.tray = new Tray(icon);
|
||||
@@ -22,21 +22,37 @@ export default class MenuApp {
|
||||
|
||||
async updateMenu() {
|
||||
const menu = new Menu();
|
||||
|
||||
const ids = await this.store.storage.getItem('NintendoAccountIds') as string[] | undefined;
|
||||
menu.append(new MenuItem({label: 'Nintendo Switch Online', enabled: false}));
|
||||
|
||||
const discordPresenceMonitor = this.getActiveDiscordPresenceMonitor();
|
||||
|
||||
for (const id of ids ?? []) {
|
||||
const token = await this.store.storage.getItem('NintendoAccountToken.' + id) as string | undefined;
|
||||
if (!token) continue;
|
||||
const data = await this.store.storage.getItem('NsoToken.' + token) as SavedToken | undefined;
|
||||
if (!data) continue;
|
||||
|
||||
const monitor = this.monitors.monitors.find(m => m.data.user.id === data.user.id);
|
||||
const discordPresenceActive = discordPresenceMonitor?.data.user.id === data.user.id;
|
||||
|
||||
const item = new MenuItem({
|
||||
label: data.nsoAccount.user.name,
|
||||
submenu: [
|
||||
{label: 'Nintendo Account ID: ' + data.user.id, enabled: false},
|
||||
{label: 'NSA ID: ' + data.nsoAccount.user.nsaId, enabled: false},
|
||||
{type: 'separator'},
|
||||
{label: 'Enable Discord Presence', type: 'checkbox', checked: discordPresenceActive,
|
||||
click: () => this.setActiveDiscordPresenceUser(discordPresenceActive ? null : data.user.id)},
|
||||
{label: 'Enable notifications for this user\'s presence', type: 'checkbox',
|
||||
checked: monitor?.user_notifications,
|
||||
click: () => this.setUserNotificationsActive(data.user.id, !monitor?.user_notifications)},
|
||||
{label: 'Enable notifications for this friends of this user\'s presence', type: 'checkbox',
|
||||
checked: monitor?.friend_notifications,
|
||||
click: () => this.setFriendNotificationsActive(data.user.id, !monitor?.friend_notifications)},
|
||||
{label: 'Update now', enabled: !!monitor, click: () => monitor?.skipIntervalInCurrentLoop()},
|
||||
{type: 'separator'},
|
||||
{label: 'Web services', enabled: false},
|
||||
...await this.getWebServiceItems(token) as any,
|
||||
],
|
||||
@@ -159,4 +175,79 @@ export default class MenuApp {
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
getActiveDiscordPresenceMonitor() {
|
||||
for (const monitor of this.monitors.monitors) {
|
||||
if (!monitor.presence_user) continue;
|
||||
|
||||
return monitor;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async setActiveDiscordPresenceUser(id: string | null) {
|
||||
const monitor = this.getActiveDiscordPresenceMonitor();
|
||||
|
||||
if (monitor) {
|
||||
if (monitor.data.user.id === id) return;
|
||||
|
||||
monitor.presence_user = null;
|
||||
// monitor.skipIntervalInCurrentLoop();
|
||||
monitor.updatePresenceForDiscord(null);
|
||||
|
||||
if (!monitor.user_notifications && !monitor.friend_notifications) {
|
||||
this.monitors.stop(monitor.data.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (id) await this.monitors.start(id, monitor => {
|
||||
monitor.presence_user = monitor.data.nsoAccount.user.nsaId;
|
||||
monitor.skipIntervalInCurrentLoop();
|
||||
});
|
||||
|
||||
if (monitor || id) this.updateMenu();
|
||||
}
|
||||
|
||||
async setUserNotificationsActive(id: string, active: boolean) {
|
||||
const monitor = this.monitors.monitors.find(m => m.data.user.id === id);
|
||||
|
||||
if (monitor?.user_notifications && !active) {
|
||||
monitor.user_notifications = false;
|
||||
|
||||
if (!monitor.presence_user && !monitor.friend_notifications) {
|
||||
this.monitors.stop(monitor.data.user.id);
|
||||
}
|
||||
|
||||
monitor.skipIntervalInCurrentLoop();
|
||||
this.updateMenu();
|
||||
}
|
||||
|
||||
if (!monitor?.user_notifications && active) await this.monitors.start(id, monitor => {
|
||||
monitor.user_notifications = true;
|
||||
monitor.skipIntervalInCurrentLoop();
|
||||
this.updateMenu();
|
||||
});
|
||||
}
|
||||
|
||||
async setFriendNotificationsActive(id: string, active: boolean) {
|
||||
const monitor = this.monitors.monitors.find(m => m.data.user.id === id);
|
||||
|
||||
if (monitor?.friend_notifications && !active) {
|
||||
monitor.friend_notifications = false;
|
||||
|
||||
if (!monitor.presence_user && !monitor.user_notifications) {
|
||||
this.monitors.stop(monitor.data.user.id);
|
||||
}
|
||||
|
||||
monitor.skipIntervalInCurrentLoop();
|
||||
this.updateMenu();
|
||||
}
|
||||
|
||||
if (!monitor?.friend_notifications && active) await this.monitors.start(id, monitor => {
|
||||
monitor.friend_notifications = true;
|
||||
monitor.skipIntervalInCurrentLoop();
|
||||
this.updateMenu();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ActiveEvent, Announcement, CurrentUser, Friend, GetActiveEventResult, P
|
||||
import ZncApi from '../../api/znc.js';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import { ArgumentsCamelCase, Argv, getToken, initStorage, SavedToken, YargsArguments } from '../../util.js';
|
||||
import { ZncNotifications } from './notify.js';
|
||||
import { NotificationManager, ZncNotifications } from './notify.js';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
@@ -642,16 +642,26 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
|
||||
const nintendoAccountSessionToken = req.headers['authorization']!.substr(3);
|
||||
const i = new ZncPresenceEventStream(
|
||||
argv as any, storage, nintendoAccountSessionToken, req.znc!, req.zncAuth!,
|
||||
req, res,
|
||||
true, true,
|
||||
);
|
||||
const i = new ZncNotifications({
|
||||
...argv,
|
||||
userNotifications: true,
|
||||
friendNotifications: true,
|
||||
}, storage, nintendoAccountSessionToken, req.znc!, req.zncAuth!);
|
||||
|
||||
await i.init();
|
||||
const es = i.notifications = new EventStreamNotificationManager(req, res);
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
try {
|
||||
await i.loop(true);
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
}
|
||||
} catch (err) {
|
||||
es.sendEvent('error', {
|
||||
error: (err as Error).name,
|
||||
error_message: (err as Error).message,
|
||||
});
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -674,23 +684,12 @@ export enum ZncPresenceEventStreamEvent {
|
||||
FRIEND_TITLE_STATECHANGE = '3',
|
||||
}
|
||||
|
||||
class ZncPresenceEventStream extends ZncNotifications {
|
||||
class EventStreamNotificationManager extends NotificationManager {
|
||||
constructor(
|
||||
argv: ArgumentsCamelCase<Arguments>,
|
||||
storage: persist.LocalStorage,
|
||||
token: string,
|
||||
nso: ZncApi,
|
||||
data: Omit<SavedToken, 'expires_at'>,
|
||||
public req: express.Request,
|
||||
public res: express.Response,
|
||||
user: boolean,
|
||||
friend: boolean,
|
||||
public res: express.Response
|
||||
) {
|
||||
super({
|
||||
...argv,
|
||||
userNotifications: user,
|
||||
friendNotifications: friend,
|
||||
}, storage, token, nso, data);
|
||||
super();
|
||||
}
|
||||
|
||||
sendEvent(event: string | null, ...data: unknown[]) {
|
||||
|
||||
@@ -118,7 +118,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
i.splatnet2_monitors.set(data.nsoAccount.user.nsaId, handleEnableSplatNet2Monitoring(argv, storage, token));
|
||||
}
|
||||
|
||||
await i.init();
|
||||
await i.loop(true);
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
@@ -126,6 +126,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
}
|
||||
|
||||
export class ZncNotifications extends Loop {
|
||||
notifications = new NotificationManager();
|
||||
splatnet2_monitors = new Map<string, EmbeddedSplatNet2Monitor | (() => Promise<EmbeddedSplatNet2Monitor>)>();
|
||||
|
||||
user_notifications = true;
|
||||
@@ -217,11 +218,99 @@ export class ZncNotifications extends Loop {
|
||||
]);
|
||||
|
||||
await this.updatePresenceForNotifications(user, friends, true);
|
||||
await this.updatePresenceForSplatNet2Monitors([user!]);
|
||||
if (user) await this.updatePresenceForSplatNet2Monitors([user]);
|
||||
|
||||
await new Promise(rs => setTimeout(rs, this.update_interval * 1000));
|
||||
return LoopResult.OK;
|
||||
}
|
||||
|
||||
async updateFriendsStatusForNotifications(friends: (CurrentUser | Friend)[], initialRun?: boolean) {
|
||||
this.notifications.updateFriendsStatusForNotifications(friends, initialRun);
|
||||
}
|
||||
|
||||
async updatePresenceForNotifications(
|
||||
user: CurrentUser | undefined, friends: Friend[] | undefined, initialRun?: boolean
|
||||
) {
|
||||
await this.updateFriendsStatusForNotifications(([] as (CurrentUser | Friend)[])
|
||||
.concat(this.user_notifications && user ? [user] : [])
|
||||
.concat(this.friend_notifications && friends ? friends : []), initialRun);
|
||||
}
|
||||
|
||||
async updatePresenceForSplatNet2Monitors(friends: (CurrentUser | Friend)[]) {
|
||||
for (const friend of friends) {
|
||||
await this.updatePresenceForSplatNet2Monitor(friend.presence, friend.nsaId, friend.name);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePresenceForSplatNet2Monitor(presence: Presence, nsa_id: string, name?: string) {
|
||||
const playing = presence.state === PresenceState.PLAYING;
|
||||
const monitor = this.splatnet2_monitors.get(nsa_id);
|
||||
|
||||
if (playing && monitor) {
|
||||
const currenttitle = presence.game as Game;
|
||||
const titleid = getTitleIdFromEcUrl(currenttitle.shopUri);
|
||||
|
||||
if (titleid && EmbeddedSplatNet2Monitor.title_ids.includes(titleid)) {
|
||||
if ('enable' in monitor) {
|
||||
monitor.enable();
|
||||
if (!monitor.enabled) debugSplatnet2('Started monitor for user %s', name ?? nsa_id);
|
||||
} else {
|
||||
const m = await monitor.call(null);
|
||||
this.splatnet2_monitors.set(nsa_id, m);
|
||||
m.enable();
|
||||
debugSplatnet2('Started monitor for user %s', name ?? nsa_id);
|
||||
}
|
||||
} else if ('disable' in monitor) {
|
||||
if (monitor.enabled) debugSplatnet2('Stopping monitor for user %s', name ?? nsa_id);
|
||||
monitor.disable();
|
||||
}
|
||||
} else if (monitor && 'disable' in monitor) {
|
||||
if (monitor.enabled) debugSplatnet2('Stopping monitor for user %s', name ?? nsa_id);
|
||||
monitor.disable();
|
||||
}
|
||||
}
|
||||
|
||||
async update() {
|
||||
const {friends, user} = await this.fetch([
|
||||
this.user_notifications ? 'user' : null,
|
||||
this.friend_notifications ? 'friends' : null,
|
||||
this.splatnet2_monitors.size ? 'user' : null,
|
||||
]);
|
||||
|
||||
await this.updatePresenceForNotifications(user, friends);
|
||||
if (user) await this.updatePresenceForSplatNet2Monitors([user]);
|
||||
}
|
||||
|
||||
async handleError(err: ErrorResponse<ZncErrorResponse> | NodeJS.ErrnoException): Promise<LoopResult> {
|
||||
if (err && 'response' in err && err.data?.status === 9404) {
|
||||
// Token expired
|
||||
debug('Renewing token');
|
||||
|
||||
const data = await this.nso.renewToken(this.token, this.data.user);
|
||||
|
||||
const existingToken: SavedToken = {
|
||||
user: this.data.user,
|
||||
...data,
|
||||
expires_at: Date.now() + (data.credential.expiresIn * 1000),
|
||||
};
|
||||
|
||||
await this.storage.setItem('NsoToken.' + this.token, existingToken);
|
||||
|
||||
return LoopResult.OK_SKIP_INTERVAL;
|
||||
} else if ('code' in err && (err as any).type === 'system' && err.code === 'ETIMEDOUT') {
|
||||
debug('Request timed out, waiting %ds before retrying', this.update_interval, err);
|
||||
|
||||
return LoopResult.OK;
|
||||
} else if ('code' in err && (err as any).type === 'system' && err.code === 'ENOTFOUND') {
|
||||
debug('Request error, waiting %ds before retrying', this.update_interval, err);
|
||||
|
||||
return LoopResult.OK;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationManager {
|
||||
onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, ir?: boolean) {
|
||||
const currenttitle = friend.presence.game as Game;
|
||||
|
||||
@@ -373,88 +462,6 @@ export class ZncNotifications extends Loop {
|
||||
|
||||
this.onlinefriends = newonlinefriends;
|
||||
}
|
||||
|
||||
async updatePresenceForNotifications(
|
||||
user: CurrentUser | undefined, friends: Friend[] | undefined, initialRun?: boolean
|
||||
) {
|
||||
await this.updateFriendsStatusForNotifications(([] as (CurrentUser | Friend)[])
|
||||
.concat(this.user_notifications && user ? [user] : [])
|
||||
.concat(this.friend_notifications && friends ? friends : []), initialRun);
|
||||
}
|
||||
|
||||
async updatePresenceForSplatNet2Monitors(friends: (CurrentUser | Friend)[]) {
|
||||
for (const friend of friends) {
|
||||
await this.updatePresenceForSplatNet2Monitor(friend.presence, friend.nsaId, friend.name);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePresenceForSplatNet2Monitor(presence: Presence, nsa_id: string, name?: string) {
|
||||
const playing = presence.state === PresenceState.PLAYING;
|
||||
const monitor = this.splatnet2_monitors.get(nsa_id);
|
||||
|
||||
if (playing && monitor) {
|
||||
const currenttitle = presence.game as Game;
|
||||
const titleid = getTitleIdFromEcUrl(currenttitle.shopUri);
|
||||
|
||||
if (titleid && EmbeddedSplatNet2Monitor.title_ids.includes(titleid)) {
|
||||
if ('enable' in monitor) {
|
||||
monitor.enable();
|
||||
if (!monitor.enabled) debugSplatnet2('Started monitor for user %s', name ?? nsa_id);
|
||||
} else {
|
||||
const m = await monitor.call(null);
|
||||
this.splatnet2_monitors.set(nsa_id, m);
|
||||
m.enable();
|
||||
debugSplatnet2('Started monitor for user %s', name ?? nsa_id);
|
||||
}
|
||||
} else if ('disable' in monitor) {
|
||||
if (monitor.enabled) debugSplatnet2('Stopping monitor for user %s', name ?? nsa_id);
|
||||
monitor.disable();
|
||||
}
|
||||
} else if (monitor && 'disable' in monitor) {
|
||||
if (monitor.enabled) debugSplatnet2('Stopping monitor for user %s', name ?? nsa_id);
|
||||
monitor.disable();
|
||||
}
|
||||
}
|
||||
|
||||
async update() {
|
||||
const {friends, user} = await this.fetch([
|
||||
this.user_notifications ? 'user' : null,
|
||||
this.friend_notifications ? 'friends' : null,
|
||||
this.splatnet2_monitors.size ? 'user' : null,
|
||||
]);
|
||||
|
||||
await this.updatePresenceForNotifications(user, friends);
|
||||
await this.updatePresenceForSplatNet2Monitors([user!]);
|
||||
}
|
||||
|
||||
async handleError(err: ErrorResponse<ZncErrorResponse> | NodeJS.ErrnoException): Promise<LoopResult> {
|
||||
if (err && 'response' in err && err.data?.status === 9404) {
|
||||
// Token expired
|
||||
debug('Renewing token');
|
||||
|
||||
const data = await this.nso.renewToken(this.token, this.data.user);
|
||||
|
||||
const existingToken: SavedToken = {
|
||||
user: this.data.user,
|
||||
...data,
|
||||
expires_at: Date.now() + (data.credential.expiresIn * 1000),
|
||||
};
|
||||
|
||||
await this.storage.setItem('NsoToken.' + this.token, existingToken);
|
||||
|
||||
return LoopResult.OK_SKIP_INTERVAL;
|
||||
} else if ('code' in err && (err as any).type === 'system' && err.code === 'ETIMEDOUT') {
|
||||
debug('Request timed out, waiting %ds before retrying', this.update_interval, err);
|
||||
|
||||
return LoopResult.OK;
|
||||
} else if ('code' in err && (err as any).type === 'system' && err.code === 'ENOTFOUND') {
|
||||
debug('Request error, waiting %ds before retrying', this.update_interval, err);
|
||||
|
||||
return LoopResult.OK;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EmbeddedSplatNet2Monitor extends SplatNet2RecordsMonitor {
|
||||
@@ -484,7 +491,7 @@ export class EmbeddedSplatNet2Monitor extends SplatNet2RecordsMonitor {
|
||||
const i = this._running;
|
||||
|
||||
try {
|
||||
await this.init();
|
||||
await this.loop(true);
|
||||
|
||||
while (i === this._running) {
|
||||
await this.loop();
|
||||
|
||||
@@ -144,7 +144,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
console.warn('Not authenticated; using znc proxy');
|
||||
}
|
||||
|
||||
await i.init();
|
||||
await i.loop(true);
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
@@ -175,7 +175,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
i.splatnet2_monitors.set(data.nsoAccount.user.nsaId, handleEnableSplatNet2Monitoring(argv, storage, token));
|
||||
}
|
||||
|
||||
await i.init();
|
||||
await i.loop(true);
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
@@ -239,9 +239,9 @@ export class ZncDiscordPresence extends ZncNotifications {
|
||||
}
|
||||
|
||||
await this.updatePresenceForNotifications(user, friends);
|
||||
await this.updatePresenceForSplatNet2Monitors([user!]);
|
||||
if (user) await this.updatePresenceForSplatNet2Monitors([user]);
|
||||
|
||||
await new Promise(rs => setTimeout(rs, this.update_interval * 1000));
|
||||
return LoopResult.OK;
|
||||
}
|
||||
|
||||
rpc: {client: DiscordRPC.Client, id: string} | null = null;
|
||||
@@ -390,7 +390,7 @@ export class ZncDiscordPresence extends ZncNotifications {
|
||||
}
|
||||
|
||||
await this.updatePresenceForNotifications(user, friends);
|
||||
await this.updatePresenceForSplatNet2Monitors([user!]);
|
||||
if (user) await this.updatePresenceForSplatNet2Monitors([user]);
|
||||
}
|
||||
|
||||
update_presence_errors = 0;
|
||||
@@ -425,7 +425,7 @@ export class ZncProxyDiscordPresence extends ZncDiscordPresence {
|
||||
async init() {
|
||||
await this.update();
|
||||
|
||||
await new Promise(rs => setTimeout(rs, this.argv.updateInterval * 1000));
|
||||
return LoopResult.OK;
|
||||
}
|
||||
|
||||
async update() {
|
||||
|
||||
@@ -14,14 +14,13 @@ import { ErrorResponse } from '../../api/util.js';
|
||||
|
||||
const debug = createDebug('cli:splatnet2:monitor');
|
||||
|
||||
export const command = 'monitor <directory>';
|
||||
export const command = 'monitor [directory]';
|
||||
export const desc = 'Monitor SplatNet 2 for new user records/battles/Salmon Run results';
|
||||
|
||||
export function builder(yargs: Argv<ParentArguments>) {
|
||||
return yargs.positional('directory', {
|
||||
describe: 'Directory to write record data to',
|
||||
type: 'string',
|
||||
demandOption: true,
|
||||
}).option('user', {
|
||||
describe: 'Nintendo Account ID',
|
||||
type: 'string',
|
||||
@@ -77,7 +76,8 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const records = await splatnet.getRecords();
|
||||
const stages = await splatnet.getStages();
|
||||
|
||||
const i = new SplatNet2RecordsMonitor(storage, token, splatnet, stages, argv.directory, argv.zncProxyUrl);
|
||||
const directory = argv.directory ?? path.join(argv.dataPath, 'splatnet2');
|
||||
const i = new SplatNet2RecordsMonitor(storage, token, splatnet, stages, directory, argv.zncProxyUrl);
|
||||
|
||||
i.update_interval = argv.updateInterval;
|
||||
|
||||
@@ -101,7 +101,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
records.records.player.player_rank,
|
||||
records.records.player.player_type);
|
||||
|
||||
await i.init();
|
||||
await i.loop(true);
|
||||
|
||||
while (true) {
|
||||
await i.loop();
|
||||
|
||||
43
src/util.ts
43
src/util.ts
@@ -199,15 +199,15 @@ export function hrduration(duration: number, short = false) {
|
||||
export abstract class Loop {
|
||||
update_interval = 60;
|
||||
|
||||
init(): void | Promise<void> {}
|
||||
init(): void | Promise<LoopResult | void> {}
|
||||
|
||||
abstract update(): void | Promise<void>;
|
||||
abstract update(): void | Promise<LoopResult | void>;
|
||||
|
||||
async loopRun(): Promise<LoopResult> {
|
||||
protected async loopRun(init = false): Promise<LoopResult> {
|
||||
try {
|
||||
await this.update();
|
||||
const result = init ? await this.init() : await this.update();
|
||||
|
||||
return LoopResult.OK;
|
||||
return result ?? (init ? LoopResult.OK_SKIP_INTERVAL : LoopResult.OK);
|
||||
} catch (err) {
|
||||
return this.handleError(err as any);
|
||||
}
|
||||
@@ -217,13 +217,38 @@ export abstract class Loop {
|
||||
throw err;
|
||||
}
|
||||
|
||||
async loop() {
|
||||
const result = await this.loopRun();
|
||||
private is_loop_active = 0;
|
||||
|
||||
if (result === LoopResult.OK) {
|
||||
await new Promise(rs => setTimeout(rs, this.update_interval * 1000));
|
||||
async loop(init = false) {
|
||||
try {
|
||||
this.is_loop_active++;
|
||||
|
||||
const result = await this.loopRun(init);
|
||||
|
||||
if (result === LoopResult.OK) {
|
||||
if (this.skip_interval_once) {
|
||||
this.skip_interval_once = false;
|
||||
} else {
|
||||
await new Promise(rs => setTimeout(this.timeout_resolve = rs, this.update_interval * 1000));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.is_loop_active--;
|
||||
this.skip_interval_once = false;
|
||||
this.timeout_resolve = null;
|
||||
}
|
||||
}
|
||||
|
||||
private skip_interval_once = false;
|
||||
private timeout_resolve: ((value: void) => void) | null = null;
|
||||
|
||||
skipIntervalInCurrentLoop() {
|
||||
debug('Skip update interval', this.is_loop_active);
|
||||
if (!this.is_loop_active) return;
|
||||
|
||||
this.skip_interval_once = true;
|
||||
this.timeout_resolve?.call(null);
|
||||
}
|
||||
}
|
||||
|
||||
const LoopRunOk = Symbol('LoopRunOk');
|
||||
|
||||
Reference in New Issue
Block a user