Handle simultaneous znc proxy requests properly

This commit is contained in:
Samuel Elliott 2022-05-30 13:12:06 +01:00
parent b8f9c2c110
commit 9f63fc8359
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
10 changed files with 207 additions and 79 deletions

View File

@ -169,6 +169,7 @@ export enum ZncPresenceEventStreamEvent {
FRIEND_OFFLINE = '1',
FRIEND_TITLE_CHANGE = '2',
FRIEND_TITLE_STATECHANGE = '3',
PRESENCE_UPDATED = '4',
}
export type PresenceUrlResponse =

View File

@ -242,7 +242,13 @@ export class EmbeddedPresenceMonitor extends ZncDiscordPresence {
try {
return await super.handleError(err);
} catch (err) {
if (err instanceof Error) {
if (err instanceof ErrorResponse) {
dialog.showErrorBox('Request error',
err.response.status + ' ' + err.response.statusText + ' ' +
err.response.url + '\n' +
err.body + '\n\n' +
(err.stack ?? err.message));
} else if (err instanceof Error) {
dialog.showErrorBox(err.name, err.stack ?? err.message);
} else {
dialog.showErrorBox('Error', err as any);
@ -251,6 +257,11 @@ export class EmbeddedPresenceMonitor extends ZncDiscordPresence {
return LoopResult.OK;
}
}
skipIntervalInCurrentLoop(start = false) {
super.skipIntervalInCurrentLoop();
if (!this._running && start) this.enable();
}
}
export class ElectronNotificationManager extends NotificationManager {

View File

@ -59,7 +59,7 @@ export default class MenuApp {
{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()},
{label: 'Update now', enabled: !!monitor, click: () => monitor?.skipIntervalInCurrentLoop(true)},
{type: 'separator'},
{label: 'Web services', enabled: false},
...await this.getWebServiceItems(token) as any,

View File

@ -14,6 +14,8 @@ import { ZNCA_CLIENT_ID, ZncJwtPayload } from '../api/znc.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js';
import { initStorage, paths } from '../util/storage.js';
import { getJwks, Jwt } from '../util/jwt.js';
import { product } from '../util/product.js';
import { parseListenAddress } from '../util/net.js';
const debug = createDebug('cli:android-znca-api-server-frida');
const debugApi = createDebug('cli:android-znca-api-server-frida:api');
@ -100,13 +102,20 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const app = express();
app.use('/api/znca', (req, res, next) => {
console.log('[%s] %s %s HTTP/%s from %s, port %d%s, %s',
new Date(), req.method, req.path, req.httpVersion,
req.socket.remoteAddress, req.socket.remotePort,
req.headers['x-forwarded-for'] ? ' (' + req.headers['x-forwarded-for'] + ')' : '',
req.headers['user-agent']);
res.setHeader('Server', product + ' android-znca-api-frida');
next();
});
app.post('/api/znca/f', bodyParser.json(), async (req, res) => {
try {
console.log('[%s] %s %s HTTP/%s from %s, port %d%s, %s',
new Date(), req.method, req.url, req.httpVersion,
req.socket.remoteAddress, req.socket.remotePort,
req.headers['x-forwarded-for'] ? ', ' + req.headers['x-forwarded-for'] : '',
req.headers['user-agent']);
await ready;
const data: {
@ -216,10 +225,8 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
});
for (const address of argv.listen) {
const match = address.match(/^((?:((?:\d+\.){3}\d+)|\[(.*)\]):)(\d+)$/);
if (!match || (match[1] && !net.isIP(match[2] || match[3]))) throw new Error('Not a valid address/port');
const server = app.listen(parseInt(match[4]), match[2] || match[3] || '::');
const [host, port] = parseListenAddress(address);
const server = app.listen(port, host ?? '::');
server.on('listening', () => {
const address = server.address() as net.AddressInfo;
console.log('Listening on %s, port %d', address.address, address.port);

View File

@ -1,5 +1,6 @@
import * as net from 'node:net';
import createDebug from 'debug';
import * as persist from 'node-persist';
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
import { v4 as uuidgen } from 'uuid';
@ -9,7 +10,9 @@ import type { Arguments as ParentArguments } from '../nso.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getToken, SavedToken } from '../../common/auth/nso.js';
import { NotificationManager, ZncNotifications } from '../../common/notify.js';
import { NotificationManager, PresenceEvent, ZncNotifications } from '../../common/notify.js';
import { product } from '../../util/product.js';
import { parseListenAddress } from '../../util/net.js';
import { AuthPolicy, AuthToken, ZncPresenceEventStreamEvent } from '../../api/znc-proxy.js';
declare global {
@ -49,10 +52,26 @@ export function builder(yargs: Argv<ParentArguments>) {
type Arguments = YargsArguments<ReturnType<typeof builder>>;
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const updateInterval = argv.updateInterval * 1000;
const storage = await initStorage(argv.dataPath);
const app = createApp(storage, argv.zncProxyUrl, argv.requireToken, argv.updateInterval * 1000);
for (const address of argv.listen) {
const [host, port] = parseListenAddress(address);
const server = app.listen(port, host ?? '::');
server.on('listening', () => {
const address = server.address() as net.AddressInfo;
console.log('Listening on %s, port %d', address.address, address.port);
});
}
}
function createApp(
storage: persist.LocalStorage,
znc_proxy_url?: string,
require_token = true,
update_interval = 30 * 1000
) {
const app = express();
app.use('/api/znc', (req, res, next) => {
@ -62,11 +81,13 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
req.headers['x-forwarded-for'] ? ' (' + req.headers['x-forwarded-for'] + ')' : '',
req.headers['user-agent']);
res.setHeader('Server', product + ' znc-proxy');
next();
});
const localAuth: express.RequestHandler = async (req, res, next) => {
if (argv.requireToken || !req.query.user) return next();
if (require_token || !req.query.user) return next();
const token = await storage.getItem('NintendoAccountToken.' + req.query.user);
if (!token) return next();
@ -105,6 +126,9 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}));
}
const znc_auth_promise = new Map</** session token */ string, Promise<{nso: ZncApi, data: SavedToken;}>>();
const znc_auth_timeout = new Map</** session token */ string, NodeJS.Timeout>();
const nsoAuth: express.RequestHandler = async (req, res, next) => {
try {
let nintendoAccountSessionToken: string;
@ -118,15 +142,33 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
nintendoAccountSessionToken = auth.substr(3);
}
const {nso, data} = await getToken(storage, nintendoAccountSessionToken, argv.zncProxyUrl);
const promise = znc_auth_promise.get(nintendoAccountSessionToken) ?? (async () => {
const auth = await getToken(storage, nintendoAccountSessionToken, znc_proxy_url);
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
users.add(auth.data.user.id);
await storage.setItem('NintendoAccountIds', [...users]);
return auth;
})().catch(err => {
znc_auth_promise.delete(nintendoAccountSessionToken);
clearTimeout(znc_auth_timeout.get(nintendoAccountSessionToken));
znc_auth_timeout.delete(nintendoAccountSessionToken);
throw err;
});
znc_auth_promise.set(nintendoAccountSessionToken, promise);
// Remove the authenticated ZncApi 30 minutes after last use
clearTimeout(znc_auth_timeout.get(nintendoAccountSessionToken));
znc_auth_timeout.set(nintendoAccountSessionToken, setTimeout(() => {
znc_auth_promise.delete(nintendoAccountSessionToken);
znc_auth_timeout.delete(nintendoAccountSessionToken);
}, 30 * 60 * 1000).unref());
const {nso, data} = await promise;
req.znc = nso;
req.zncAuth = data;
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
users.add(data.user.id);
await storage.setItem('NintendoAccountIds', [...users]);
next();
} catch (err) {
res.statusCode = 500;
@ -249,20 +291,26 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
// Nintendo Switch user data
//
const cached_userdata = new Map<string, [CurrentUser, number]>();
const user_data_promise = new Map</** NA ID */ string, Promise<void>>();
const cached_userdata = new Map</** NA ID */ string, [CurrentUser, number]>();
const getUserData: express.RequestHandler = async (req, res, next) => {
const cache = cached_userdata.get(req.zncAuth!.user.id);
if (cache && ((cache[1] + updateInterval) > Date.now())) {
if (cache && ((cache[1] + update_interval) > Date.now())) {
debug('Using cached user data for %s', req.zncAuth!.user.id);
next();
return;
}
try {
const user = await req.znc!.getCurrentUser();
cached_userdata.set(req.zncAuth!.user.id, [user.result, Date.now()]);
const promise = user_data_promise.get(req.zncAuth!.user.id) ?? req.znc!.getCurrentUser().then(user => {
cached_userdata.set(req.zncAuth!.user.id, [user.result, Date.now()]);
}).finally(() => {
user_data_promise.delete(req.zncAuth!.user.id);
});
user_data_promise.set(req.zncAuth!.user.id, promise);
await promise;
next();
} catch (err) {
@ -282,6 +330,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}, localAuth, nsoAuth, getUserData, async (req, res) => {
const [user, updated] = cached_userdata.get(req.zncAuth!.user.id)!;
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({user, updated}));
});
@ -302,24 +351,28 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
// Nintendo Switch friends, NSO app web services, events
//
const cached_friendsdata = new Map<string, [Friend[], number]>();
const cached_appdata = new Map<string, [WebService[], GetActiveEventResult, number]>();
const friends_data_promise = new Map</** NA ID */ string, Promise<void>>();
const cached_friendsdata = new Map</** NA ID */ string, [Friend[], number]>();
const app_data_promise = new Map</** NA ID */ string, Promise<void>>();
const cached_appdata = new Map</** NA ID */ string, [WebService[], GetActiveEventResult, number]>();
const getFriendsData: express.RequestHandler = async (req, res, next) => {
const cache = cached_friendsdata.get(req.zncAuth!.user.id);
if (cache && ((cache[1] + updateInterval) > Date.now())) {
if (cache && ((cache[1] + update_interval) > Date.now())) {
debug('Using cached friends data for %s', req.zncAuth!.user.id);
next();
return;
}
try {
const friends = await req.znc!.getFriendList();
cached_friendsdata.set(req.zncAuth!.user.id, [
friends.result.friends, Date.now(),
]);
const promise = friends_data_promise.get(req.zncAuth!.user.id) ?? req.znc!.getFriendList().then(friends => {
cached_friendsdata.set(req.zncAuth!.user.id, [friends.result.friends, Date.now()]);
}).finally(() => {
friends_data_promise.delete(req.zncAuth!.user.id);
});
friends_data_promise.set(req.zncAuth!.user.id, promise);
await promise;
next();
} catch (err) {
@ -334,23 +387,32 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
const getAppData: express.RequestHandler = async (req, res, next) => {
const cache = cached_appdata.get(req.zncAuth!.user.id);
if (cache && ((cache[2] + updateInterval) > Date.now())) {
if (cache && ((cache[2] + update_interval) > Date.now())) {
debug('Using cached app data for %s', req.zncAuth!.user.id);
next();
return;
}
try {
const friends = await req.znc!.getFriendList();
const webservices = await req.znc!.getWebServices();
const activeevent = await req.znc!.getActiveEvent();
const friends_promise = friends_data_promise.get(req.zncAuth!.user.id) ?? req.znc!.getFriendList().then(friends => {
cached_friendsdata.set(req.zncAuth!.user.id, [friends.result.friends, Date.now()]);
}).finally(() => {
friends_data_promise.delete(req.zncAuth!.user.id);
});
friends_data_promise.set(req.zncAuth!.user.id, friends_promise);
cached_friendsdata.set(req.zncAuth!.user.id, [
friends.result.friends, Date.now(),
]);
cached_appdata.set(req.zncAuth!.user.id, [
webservices.result, activeevent.result, Date.now(),
]);
const promise = app_data_promise.get(req.zncAuth!.user.id) ?? Promise.all([
friends_promise,
req.znc!.getWebServices(),
req.znc!.getActiveEvent(),
]).then(([friends, webservices, activeevent]) => {
// Friends list was already added to cache
cached_appdata.set(req.zncAuth!.user.id, [webservices.result, activeevent.result, Date.now()]);
}).finally(() => {
app_data_promise.delete(req.zncAuth!.user.id);
});
app_data_promise.set(req.zncAuth!.user.id, promise);
await promise;
next();
} catch (err) {
@ -370,6 +432,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}, localAuth, nsoAuth, getFriendsData, async (req, res) => {
const [friends, updated] = cached_friendsdata.get(req.zncAuth!.user.id)!;
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
friends: req.zncAuthPolicy?.friends ?
@ -385,6 +448,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}, localAuth, nsoAuth, getFriendsData, async (req, res) => {
const [friends, updated] = cached_friendsdata.get(req.zncAuth!.user.id)!;
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
friends: friends.filter(f => {
@ -414,6 +478,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
presence[friend.nsaId] = friend.presence;
}
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(presence));
});
@ -438,6 +503,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
presence[friend.nsaId] = friend.presence;
}
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(presence));
});
@ -461,6 +527,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
return;
}
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({friend, updated}));
});
@ -535,6 +602,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
return;
}
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(friend.presence));
});
@ -546,6 +614,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}, localAuth, nsoAuth, getAppData, async (req, res) => {
const [webservices, activeevent, updated] = cached_appdata.get(req.zncAuth!.user.id)!;
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({webservices, updated}));
});
@ -575,6 +644,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}, localAuth, nsoAuth, getAppData, async (req, res) => {
const [webservices, activeevent, updated] = cached_appdata.get(req.zncAuth!.user.id)!;
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, update_interval));
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({activeevent, updated}));
});
@ -632,7 +702,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
i.user_notifications = false;
i.friend_notifications = true;
i.update_interval = argv.updateInterval;
i.update_interval = update_interval / 1000;
const es = i.notifications = new EventStreamNotificationManager(req, res);
@ -651,16 +721,11 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
}
});
for (const address of argv.listen) {
const match = address.match(/^((?:((?:\d+\.){3}\d+)|\[(.*)\]):)(\d+)$/);
if (!match || (match[1] && !net.isIP(match[2] || match[3]))) throw new Error('Not a valid address/port');
return app;
}
const server = app.listen(parseInt(match[4]), match[2] || match[3] || '::');
server.on('listening', () => {
const address = server.address() as net.AddressInfo;
console.log('Listening on %s, port %d', address.address, address.port);
});
}
function cacheMaxAge(updated_timestamp_ms: number, update_interval_ms: number) {
return Math.floor(((updated_timestamp_ms + update_interval_ms) - Date.now()) / 1000);
}
class EventStreamNotificationManager extends NotificationManager {
@ -677,6 +742,15 @@ class EventStreamNotificationManager extends NotificationManager {
this.res.write('\n');
}
onPresenceUpdated(
friend: CurrentUser | Friend, prev?: CurrentUser | Friend, type?: PresenceEvent,
naid?: string, ir?: boolean
) {
this.sendEvent(ZncPresenceEventStreamEvent.PRESENCE_UPDATED, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,
});
}
onFriendOnline(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean) {
this.sendEvent(ZncPresenceEventStreamEvent.FRIEND_ONLINE, {
id: friend.nsaId, presence: friend.presence, prev: prev?.presence,

View File

@ -184,6 +184,8 @@ export class ZncNotifications extends Loop {
}
export class NotificationManager {
onPresenceUpdated?(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, type?: PresenceEvent, naid?: string, ir?: boolean): void;
onFriendOnline?(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean): void;
onFriendOffline?(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean): void;
onFriendPlayingChangeTitle?(friend: CurrentUser | Friend, prev?: CurrentUser | Friend, naid?: string, ir?: boolean): void;
@ -216,8 +218,19 @@ export class NotificationManager {
// Another account is monitoring this user's presence
if (this.accounts.get(friend.nsaId) !== naid) continue;
if (lastpresence?.updatedAt !== friend.presence.updatedAt && !initialRun) {
debug('%s\'s presence updated', friend.name, new Date(friend.presence.updatedAt * 1000).toString());
}
let type: PresenceEvent | undefined = undefined;
let callback: 'onFriendOnline' | 'onFriendOffline' | 'onFriendPlayingChangeTitle' |
'onFriendTitleStateChange' | undefined = undefined;
if (!wasonline && online) {
// Friend has come online
type = PresenceEvent.STATE_CHANGE;
callback = 'onFriendOnline';
const currenttitle = friend.presence.game as Game;
debugFriends('%s is now online%s%s, title %s %s - played for %s since %s', friend.name,
@ -227,20 +240,20 @@ export class NotificationManager {
hrduration(currenttitle.totalPlayTime),
currenttitle.firstPlayedAt ? new Date(currenttitle.firstPlayedAt * 1000).toString() : 'now');
this.onFriendOnline?.(friend, prev, naid, initialRun);
if (consolewasonline) {
// Friend's console was already online
}
} else if (wasonline && !online) {
// Friend has gone offline
type = PresenceEvent.STATE_CHANGE;
callback = 'onFriendOffline';
const lasttitle = lastpresence.game as Game;
debugFriends('%s is now offline%s, was playing title %s %s', friend.name,
friend.presence.state !== PresenceState.OFFLINE ? ' (console still online)' : '',
lasttitle.name, JSON.stringify(lasttitle.sysDescription));
this.onFriendOffline?.(friend, prev, naid, initialRun);
lasttitle.name, JSON.stringify(lasttitle.sysDescription),
new Date(friend.presence.logoutAt * 1000).toString());
if (friend.presence.state !== PresenceState.OFFLINE) {
// Friend's console is still online
@ -252,6 +265,8 @@ export class NotificationManager {
if (getTitleIdFromEcUrl(lasttitle.shopUri) !== getTitleIdFromEcUrl(currenttitle.shopUri)) {
// Friend is playing a different title
type = PresenceEvent.TITLE_CHANGE;
callback = 'onFriendPlayingChangeTitle';
debugFriends('%s title is now %s %s%s, was playing %s %s%s - played for %s since %s',
friend.name,
@ -261,41 +276,44 @@ export class NotificationManager {
lastpresence.state === PresenceState.ONLINE ? '' : ' (' + lastpresence.state + ')',
hrduration(currenttitle.totalPlayTime),
currenttitle.firstPlayedAt ? new Date(currenttitle.firstPlayedAt * 1000).toString() : 'now');
this.onFriendPlayingChangeTitle?.(friend, prev, naid, initialRun);
} else if (
lastpresence.state !== friend.presence.state ||
lasttitle.sysDescription !== currenttitle.sysDescription
) {
// Title state changed
} else if (lasttitle.sysDescription !== currenttitle.sysDescription) {
// Title state changed (presence state may have also changed between online/playing)
type = PresenceEvent.TITLE_STATE_CHANGE;
callback = 'onFriendTitleStateChange';
debugFriends('%s title %s state changed, now %s %s, was %s %s',
friend.name, currenttitle.name,
friend.presence.state, JSON.stringify(currenttitle.sysDescription),
lastpresence.state, JSON.stringify(lasttitle.sysDescription));
this.onFriendTitleStateChange?.(friend, prev, naid, initialRun);
} else if (
lastpresence.state !== friend.presence.state ||
lasttitle.sysDescription !== currenttitle.sysDescription
) {
} else if (lastpresence.state !== friend.presence.state) {
// Presence state changed (between online/playing)
type = PresenceEvent.TITLE_STATE_CHANGE;
callback = 'onFriendTitleStateChange';
debugFriends('%s title %s state changed%s, now %s %s, was %s %s',
friend.name, currenttitle.name,
friend.presence.state, JSON.stringify(currenttitle.sysDescription),
friend.name, currenttitle.name, friend.presence.state,
lastpresence.state, JSON.stringify(lasttitle.sysDescription));
}
} else if (!consolewasonline && friend.presence.state !== PresenceState.OFFLINE) {
// Friend's console is now online, but the user is not playing
type = PresenceEvent.STATE_CHANGE;
debugFriends('%s\'s console is now online', friend.name);
} else if (consolewasonline && friend.presence.state !== PresenceState.OFFLINE) {
// Friend's console is still online, the user is still not playing
} else if (consolewasonline && friend.presence.state === PresenceState.OFFLINE) {
// Friend's console is now offline
type = PresenceEvent.STATE_CHANGE;
debugFriends('%s\'s console is now offline', friend.name);
debugFriends('%s\'s console is now offline', friend.name,
new Date(friend.presence.logoutAt * 1000).toString());
}
if (lastpresence?.updatedAt !== friend.presence.updatedAt && !initialRun) {
this.onPresenceUpdated?.(friend, prev, type, naid, initialRun);
}
if (callback) {
this[callback]?.(friend, prev, naid, initialRun);
}
}
@ -345,6 +363,12 @@ export class NotificationManager {
}
}
export enum PresenceEvent {
STATE_CHANGE,
TITLE_CHANGE,
TITLE_STATE_CHANGE,
}
export class EmbeddedSplatNet2Monitor extends SplatNet2RecordsMonitor {
static title_ids = [
'0100f8f0000a2000', // Europe

View File

@ -46,7 +46,7 @@ export class ZncDiscordPresence extends ZncNotifications {
}
}
await this.updatePresenceForNotifications(user, friends);
await this.updatePresenceForNotifications(user, friends, this.data.user.id, true);
if (user) await this.updatePresenceForSplatNet2Monitors([user]);
return LoopResult.OK;
@ -248,7 +248,7 @@ export class ZncDiscordPresence extends ZncNotifications {
}
}
await this.updatePresenceForNotifications(user, friends);
await this.updatePresenceForNotifications(user, friends, this.data.user.id, true);
if (user) await this.updatePresenceForSplatNet2Monitors([user]);
}

View File

@ -1,13 +1,10 @@
import DiscordRPC from 'discord-rpc';
import { ActiveEvent, CurrentUser, Friend, Game, PresenceState } from '../api/znc-types.js';
import { defaultTitle, titles } from './titles.js';
import { dev, git, version } from '../util/product.js';
import { product } from '../util/product.js';
import { getTitleIdFromEcUrl, hrduration } from '../util/misc.js';
import { ZncDiscordPresence } from '../common/presence.js';
const product = 'nxapi ' + version +
(git ? '-' + git.revision.substr(0, 7) + (git.branch ? ' (' + git.branch + ')' : dev ? '-dev' : '') : '');
export function getDiscordPresence(
state: PresenceState, game: Game, context?: DiscordPresenceContext
): DiscordPresence {

11
src/util/net.ts Normal file
View File

@ -0,0 +1,11 @@
import * as net from 'node:net';
export function parseListenAddress(address: string | number) {
const match = ('' + address).match(/^((?:((?:\d+\.){3}\d+)|\[(.*)\]):)?(\d+)$/);
if (!match || (match[1] && !net.isIP(match[2] || match[3]))) throw new Error('Invalid address/port');
const host = match[2] || match[3] || null;
const port = parseInt(match[4]);
return [host, port] as const;
}

View File

@ -33,3 +33,6 @@ export const git = (() => {
};
})();
export const dev = !!git || process.env.NODE_ENV === 'development';
export const product = 'nxapi ' + version +
(git ? '-' + git.revision.substr(0, 7) + (git.branch ? ' (' + git.branch + ')' : dev ? '-dev' : '') : '');