Use cached web services from the API for the menu

Fixes https://github.com/samuelthomas2774/nxapi/issues/21
This commit is contained in:
Samuel Elliott 2022-09-12 16:41:53 +01:00
parent 7576a16af7
commit 07a30e89be
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
3 changed files with 79 additions and 57 deletions

View File

@ -257,7 +257,7 @@ export class Store extends EventEmitter {
super();
// ratelimit = false, as most users.get calls are triggered by user interaction (or at startup)
this.users = Users.coral(storage, process.env.ZNC_PROXY_URL, false);
this.users = Users.coral(this, process.env.ZNC_PROXY_URL, false);
}
async saveMonitorState(monitors: PresenceMonitorManager) {

View File

@ -13,6 +13,7 @@ import { EmbeddedPresenceMonitor, EmbeddedProxyPresenceMonitor } from './monitor
import { createWindow } from './windows.js';
import { WindowType } from '../common/types.js';
import CoralApi from '../../api/coral.js';
import { CachedWebServicesList } from '../../common/users.js';
const debug = createDebug('app:main:menu');
@ -30,6 +31,10 @@ export default class MenuApp {
this.tray.setToolTip('nxapi');
app.store.on('update-nintendo-accounts', () => this.updateMenu());
app.store.on('update-cached-web-services', (language: string, cache: CachedWebServicesList) => {
this.webservices.set(language, cache.webservices);
this.updateMenu();
});
this.updateMenu();
}
@ -52,6 +57,8 @@ export default class MenuApp {
const discord_presence_active = discord_presence_monitor instanceof EmbeddedPresenceMonitor &&
discord_presence_monitor?.data?.user.id === data.user.id;
const webservices = await this.getWebServiceItems(data.user.language, token);
const item = new MenuItem({
label: data.nsoAccount.user.name,
submenu: [
@ -72,9 +79,11 @@ export default class MenuApp {
{label: 'Update now', enabled: !!monitor, click: () => monitor?.skipIntervalInCurrentLoop(true)},
{type: 'separator'},
{label: 'Add friend', click: () => this.showAddFriendWindow(data.user.id)},
{type: 'separator'},
{label: 'Web services', enabled: false},
...await this.getWebServiceItems(token) as any,
...(webservices.length ? [
{type: 'separator'},
{label: 'Web services', enabled: false},
...webservices as any,
] : []),
],
});
@ -120,57 +129,21 @@ export default class MenuApp {
addPctlAccount = (item: MenuItem, window: BrowserWindow | undefined, event: KeyboardEvent) =>
askAddPctlAccount(this.app.store.storage, !event.shiftKey);
// Hardcode these temporarily until they are cached
webservices: WebService[] | null = [
{
id: 4953919198265344,
uri: 'https://web.sd.lp1.acbaa.srv.nintendo.net',
customAttributes: [
{attrKey: 'verifyMembership', attrValue: 'true'},
{attrKey: 'deepLinkingEnabled', attrValue: 'true'},
{attrKey: 'appNavigationBarBgColor', attrValue: '82D7AA'},
{attrKey: 'appStatusBarBgColor', attrValue: '82D7AA'},
],
whiteList: ['*.acbaa.srv.nintendo.net'],
name: 'Animal Crossing: New Horizons',
imageUri: 'https://cdn.znc.srv.nintendo.net/gameWebServices/n5b4648f/n5b4648f/images/euEn/banner.png',
},
{
id: 5598642853249024,
uri: 'https://app.smashbros.nintendo.net',
customAttributes: [
{attrKey: 'verifyMembership', attrValue: 'true'},
{attrKey: 'appNavigationBarBgColor', attrValue: 'A50514'},
{attrKey: 'appStatusBarBgColor', attrValue: 'A50514'},
],
whiteList: ['app.smashbros.nintendo.net'],
name: 'Super Smash Bros. Ultimate',
imageUri: 'https://cdn.znc.srv.nintendo.net/gameWebServices/n3f32691/n3f32691/images/euEn/banner.png',
},
{
id: 5741031244955648,
uri: 'https://app.splatoon2.nintendo.net/',
customAttributes: [
{attrKey: 'appNavigationBarBgColor', attrValue: 'E60012'},
{attrKey: 'appStatusBarBgColor', attrValue: 'E60012'},
],
whiteList: ['app.splatoon2.nintendo.net'],
name: 'Splatoon 2',
imageUri: 'https://cdn.znc.srv.nintendo.net/gameWebServices/splatoon2/images/euEn/banner.png',
},
];
protected webservices = new Map</** language */ string, WebService[]>();
async getWebServices(token: string) {
if (this.webservices) return this.webservices;
async getWebServices(language: string) {
const cache = this.webservices.get(language);
if (cache) return cache;
const {nso, data} = await this.app.store.users.get(token);
const webservices: CachedWebServicesList | undefined =
await this.app.store.storage.getItem('CachedWebServicesList.' + language);
const webservices = await nso.getWebServices();
return this.webservices = webservices;
if (webservices) this.webservices.set(language, webservices.webservices);
return webservices?.webservices ?? [];
}
async getWebServiceItems(token: string) {
const webservices = await this.getWebServices(token);
async getWebServiceItems(language: string, token: string) {
const webservices = await this.getWebServices(language);
const items = [];
for (const webservice of webservices) {

View File

@ -1,9 +1,12 @@
import * as crypto from 'node:crypto';
import createDebug from 'debug';
import * as persist from 'node-persist';
import CoralApi from '../api/coral.js';
import CoralApi, { Result } from '../api/coral.js';
import ZncProxyApi from '../api/znc-proxy.js';
import { Announcements, Friends, GetActiveEventResult, WebServices, CoralSuccessResponse, Friend } from '../api/coral-types.js';
import { Announcements, Friends, Friend, GetActiveEventResult, CoralSuccessResponse, WebService, WebServices } from '../api/coral-types.js';
import { getToken, SavedToken } from './auth/coral.js';
import type { Store } from '../app/main/index.js';
import { NintendoAccountUser } from '../api/na.js';
const debug = createDebug('nxapi:users');
@ -40,9 +43,14 @@ export default class Users<T extends UserData> {
return promise;
}
static coral(storage: persist.LocalStorage, znc_proxy_url: string, ratelimit?: boolean): Users<CoralUser<ZncProxyApi>>
static coral(storage: persist.LocalStorage, znc_proxy_url?: string, ratelimit?: boolean): Users<CoralUser>
static coral(storage: persist.LocalStorage, znc_proxy_url?: string, ratelimit?: boolean) {
static coral(store: Store | persist.LocalStorage, znc_proxy_url: string, ratelimit?: boolean): Users<CoralUser<ZncProxyApi>>
static coral(store: Store | persist.LocalStorage, znc_proxy_url?: string, ratelimit?: boolean): Users<CoralUser>
static coral(_store: Store | persist.LocalStorage, znc_proxy_url?: string, ratelimit?: boolean) {
const store = 'storage' in _store ? _store : null;
const storage = 'storage' in _store ? _store.storage : _store;
const cached_webservices = new Map</** language */ string, string>();
return new Users(async token => {
const {nso, data} = await getToken(storage, token, znc_proxy_url, ratelimit);
@ -53,7 +61,16 @@ export default class Users<T extends UserData> {
nso.getActiveEvent(),
]);
return new CoralUser(nso, data, announcements, friends, webservices, active_event);
const user = new CoralUser(nso, data, announcements, friends, webservices, active_event);
if (store) {
await maybeUpdateWebServicesListCache(cached_webservices, store, data.user, webservices);
user.onUpdatedWebServices = webservices => {
maybeUpdateWebServicesListCache(cached_webservices, store, data.user, webservices);
};
}
return user;
});
}
}
@ -80,6 +97,8 @@ export class CoralUser<T extends CoralApi = CoralApi> implements CoralUserData<T
active_event: Date.now(),
};
onUpdatedWebServices: ((webservices: Result<WebServices>) => void) | null = null;
constructor(
public nso: T,
public data: SavedToken,
@ -125,7 +144,9 @@ export class CoralUser<T extends CoralApi = CoralApi> implements CoralUserData<T
async getWebServices() {
await this.update('webservices', async () => {
this.webservices = await this.nso.getWebServices();
const webservices = this.webservices = await this.nso.getWebServices();
this.onUpdatedWebServices?.call(null, webservices);
}, 10 * 1000);
return this.webservices.result;
@ -167,3 +188,31 @@ export class CoralUser<T extends CoralApi = CoralApi> implements CoralUserData<T
return {result, friend};
}
}
export interface CachedWebServicesList {
webservices: WebService[];
updated_at: number;
language: string;
user: string;
}
async function maybeUpdateWebServicesListCache(
cached_webservices: Map<string, string>, store: Store, // storage: persist.LocalStorage,
user: NintendoAccountUser, webservices: WebService[]
) {
const webservices_hash = crypto.createHash('sha256').update(JSON.stringify(webservices)).digest('hex');
if (cached_webservices.get(user.language) === webservices_hash) return;
debug('Updating web services list', user.language);
const cache: CachedWebServicesList = {
webservices,
updated_at: Date.now(),
language: user.language,
user: user.id,
};
await store.storage.setItem('CachedWebServicesList.' + user.language, cache);
cached_webservices.set(user.language, webservices_hash);
store?.emit('update-cached-web-services', user.language, cache);
}