mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-07-04 17:11:01 -05:00
Handle simultaneous znc proxy requests properly
This commit is contained in:
parent
b8f9c2c110
commit
9f63fc8359
|
|
@ -169,6 +169,7 @@ export enum ZncPresenceEventStreamEvent {
|
|||
FRIEND_OFFLINE = '1',
|
||||
FRIEND_TITLE_CHANGE = '2',
|
||||
FRIEND_TITLE_STATECHANGE = '3',
|
||||
PRESENCE_UPDATED = '4',
|
||||
}
|
||||
|
||||
export type PresenceUrlResponse =
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
11
src/util/net.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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' : '') : '');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user