mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-07-08 21:33:56 -05:00
Export NookLink client, remove duplicate ErrorResponse class
This commit is contained in:
parent
ff6706386d
commit
81cec767df
|
|
@ -42,10 +42,7 @@ export default class NooklinkApi {
|
|||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
const data = response.headers.get('Content-Type')?.match(/\bapplication\/json\b/i) ?
|
||||
await response.json() : await response.text();
|
||||
|
||||
throw new ErrorResponse('[nooklink] Unknown error', response, data);
|
||||
throw new ErrorResponse('[nooklink] Unknown error', response, await response.text());
|
||||
}
|
||||
|
||||
const data = await response.json() as T | WebServiceError;
|
||||
|
|
@ -113,15 +110,17 @@ export default class NooklinkApi {
|
|||
|
||||
debug('fetch %s %s, response %s', 'GET', url, response.status);
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new ErrorResponse('Unknown error', response);
|
||||
throw new ErrorResponse('[nooklink] Unknown error', response, body);
|
||||
}
|
||||
|
||||
const cookies = response.headers.get('Set-Cookie');
|
||||
const match = cookies?.match(/\b_gtoken=([^;]*)(;(\s*((?!expires)[a-z]+=([^;]*));?)*(\s*(expires=([^;]*));?)?|$)/i);
|
||||
|
||||
if (!match) {
|
||||
throw new ErrorResponse('Response didn\'t include _gtoken cookie', response);
|
||||
throw new ErrorResponse('[nooklink] Response didn\'t include _gtoken cookie', response, body);
|
||||
}
|
||||
|
||||
const gtoken = decodeURIComponent(match[1]);
|
||||
|
|
@ -132,8 +131,6 @@ export default class NooklinkApi {
|
|||
|
||||
const expires_at = expires ? Date.parse(expires) : Date.now() + webserviceToken.expiresIn * 1000;
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
return {
|
||||
webserviceToken,
|
||||
url: url.toString(),
|
||||
|
|
@ -178,10 +175,7 @@ export class NooklinkUserApi {
|
|||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
const data = response.headers.get('Content-Type')?.match(/\bapplication\/json\b/i) ?
|
||||
await response.json() : await response.text();
|
||||
|
||||
throw new ErrorResponse('[nooklink] Unknown error', response, data);
|
||||
throw new ErrorResponse('[nooklink] Unknown error', response, await response.text());
|
||||
}
|
||||
|
||||
const data = await response.json() as T | WebServiceError;
|
||||
|
|
|
|||
|
|
@ -43,10 +43,7 @@ export default class SplatNet2Api {
|
|||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status !== 200) {
|
||||
const data = response.headers.get('Content-Type')?.match(/\bapplication\/json\b/i) ?
|
||||
await response.json() : await response.text();
|
||||
|
||||
throw new ErrorResponse('[splatnet2] Unknown error', response, data);
|
||||
throw new ErrorResponse('[splatnet2] Unknown error', response, await response.text());
|
||||
}
|
||||
|
||||
updateIksmSessionLastUsed(this.iksm_session);
|
||||
|
|
@ -244,15 +241,17 @@ ${colour}
|
|||
|
||||
debug('fetch %s %s, response %s', 'GET', url, response.status);
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new ErrorResponse('Unknown error', response);
|
||||
throw new ErrorResponse('[splatnet2] Unknown error', response, body);
|
||||
}
|
||||
|
||||
const cookies = response.headers.get('Set-Cookie');
|
||||
const match = cookies?.match(/\biksm_session=([^;]*)(;(\s*((?!expires)[a-z]+=([^;]*));?)*(\s*(expires=([^;]*));?)?|$)/i);
|
||||
|
||||
if (!match) {
|
||||
throw new ErrorResponse('Response didn\'t include iksm_session cookie', response);
|
||||
throw new ErrorResponse('[splatnet2] Response didn\'t include iksm_session cookie', response, body);
|
||||
}
|
||||
|
||||
const iksm_session = decodeURIComponent(match[1]);
|
||||
|
|
@ -264,8 +263,6 @@ ${colour}
|
|||
|
||||
const expires_at = expires ? Date.parse(expires) : Date.now() + 24 * 60 * 60 * 1000;
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
const ml = body.match(/<html(?:\s+[a-z0-9-]+(?:=(?:"[^"]*"|[^\s>]*))?)*\s+lang=(?:"([^"]*)"|([^\s>]*))/i);
|
||||
const mr = body.match(/<html(?:\s+[a-z0-9-]+(?:=(?:"[^"]*"|[^\s>]*))?)*\s+data-region=(?:"([^"]*)"|([^\s>]*))/i);
|
||||
const mu = body.match(/<html(?:\s+[a-z0-9-]+(?:=(?:"[^"]*"|[^\s>]*))?)*\s+data-unique-id=(?:"([^"]*)"|([^\s>]*))/i);
|
||||
|
|
|
|||
|
|
@ -2,15 +2,38 @@ import * as crypto from 'crypto';
|
|||
import { Response } from 'node-fetch';
|
||||
|
||||
export class ErrorResponse<T = unknown> extends Error {
|
||||
readonly body: string | undefined;
|
||||
readonly data: T | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
readonly response: Response,
|
||||
readonly data: T = undefined as any
|
||||
body?: string | T
|
||||
) {
|
||||
super(message);
|
||||
|
||||
if (typeof body === 'string') {
|
||||
this.body = body;
|
||||
try {
|
||||
this.data = body ? JSON.parse(body) : undefined;
|
||||
} catch (err) {}
|
||||
} else if (typeof body !== 'undefined') {
|
||||
this.data = body;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ErrorResponse, Symbol.hasInstance, {
|
||||
configurable: true,
|
||||
value: (instance: ErrorResponse) => {
|
||||
return instance instanceof Error &&
|
||||
'response' in instance &&
|
||||
instance.response instanceof Response &&
|
||||
'body' in instance &&
|
||||
'data' in instance;
|
||||
},
|
||||
});
|
||||
|
||||
export interface JwtHeader {
|
||||
typ?: 'JWT';
|
||||
alg: JwtAlgorithm;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { AccountToken, ActiveEvent, Announcements, CurrentUser, Event, Friend, PresencePermissions, User, WebService, WebServiceToken, ZncStatus, ZncSuccessResponse } from './znc-types.js';
|
||||
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, PresencePermissions, User, WebService, WebServiceToken, ZncStatus, ZncSuccessResponse } from './znc-types.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
import ZncApi from './znc.js';
|
||||
import { SavedToken, version } from '../util.js';
|
||||
|
|
@ -34,7 +34,7 @@ export default class ZncProxyApi implements ZncApi {
|
|||
if (response.status === 204) return null!;
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new ErrorResponse('[zncproxy] Unknown error', response);
|
||||
throw new ErrorResponse('[zncproxy] Unknown error', response, await response.text());
|
||||
}
|
||||
|
||||
const data = await response.json() as T;
|
||||
|
|
@ -42,6 +42,10 @@ export default class ZncProxyApi implements ZncApi {
|
|||
return data;
|
||||
}
|
||||
|
||||
async call<T = unknown>(url: string, parameter = {}): Promise<ZncSuccessResponse<T>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
const response = await this.fetch<{announcements: Announcements}>('/announcements');
|
||||
return {status: ZncStatus.OK as const, result: response.announcements, correlationId: ''};
|
||||
|
|
|
|||
|
|
@ -51,78 +51,73 @@ export default class ZncApi {
|
|||
return data;
|
||||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
return this.fetch<Announcements>('/v1/Announcement/List', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async getFriendList() {
|
||||
return this.fetch<Friends>('/v3/Friend/List', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async addFavouriteFriend(nsaid: string) {
|
||||
return this.fetch<{}>('/v3/Friend/Favorite/Create', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
nsaId: nsaid,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async removeFavouriteFriend(nsaid: string) {
|
||||
return this.fetch<{}>('/v3/Friend/Favorite/Delete', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
nsaId: nsaid,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async getWebServices() {
|
||||
async call<T = unknown>(url: string, parameter = {}) {
|
||||
const uuid = uuidgen();
|
||||
|
||||
return this.fetch<WebServices>('/v1/Game/ListWebServices', 'POST', JSON.stringify({
|
||||
return this.fetch<T>(url, 'POST', JSON.stringify({
|
||||
parameter,
|
||||
requestId: uuid,
|
||||
}));
|
||||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
return this.call<Announcements>('/v1/Announcement/List');
|
||||
}
|
||||
|
||||
async getFriendList() {
|
||||
return this.call<Friends>('/v3/Friend/List');
|
||||
}
|
||||
|
||||
async addFavouriteFriend(nsaid: string) {
|
||||
return this.call<{}>('/v3/Friend/Favorite/Create', {
|
||||
nsaId: nsaid,
|
||||
});
|
||||
}
|
||||
|
||||
async removeFavouriteFriend(nsaid: string) {
|
||||
return this.call<{}>('/v3/Friend/Favorite/Delete', {
|
||||
nsaId: nsaid,
|
||||
});
|
||||
}
|
||||
|
||||
async getWebServices() {
|
||||
return this.call<WebServices>('/v1/Game/ListWebServices');
|
||||
}
|
||||
|
||||
async getActiveEvent() {
|
||||
return this.fetch<GetActiveEventResult>('/v1/Event/GetActiveEvent', 'POST', '{"parameter":{}}');
|
||||
return this.call<GetActiveEventResult>('/v1/Event/GetActiveEvent');
|
||||
}
|
||||
|
||||
async getEvent(id: number) {
|
||||
return this.fetch<Event>('/v1/Event/Show', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
id,
|
||||
},
|
||||
}));
|
||||
return this.call<Event>('/v1/Event/Show', {
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
async getUser(id: number) {
|
||||
return this.fetch<User>('/v3/User/Show', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
id,
|
||||
},
|
||||
}));
|
||||
return this.call<User>('/v3/User/Show', {
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
return this.fetch<CurrentUser>('/v3/User/ShowSelf', 'POST', '{"parameter":{}}');
|
||||
return this.call<CurrentUser>('/v3/User/ShowSelf');
|
||||
}
|
||||
|
||||
async getCurrentUserPermissions() {
|
||||
return this.fetch<CurrentUserPermissions>('/v3/User/Permissions/ShowSelf', 'POST', '{"parameter":{}}');
|
||||
return this.call<CurrentUserPermissions>('/v3/User/Permissions/ShowSelf');
|
||||
}
|
||||
|
||||
async updateCurrentUserPermissions(to: PresencePermissions, from: PresencePermissions, etag: string) {
|
||||
return this.fetch<{}>('/v3/User/Permissions/UpdateSelf', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
permissions: {
|
||||
presence: {
|
||||
toValue: to,
|
||||
fromValue: from,
|
||||
},
|
||||
return this.call<{}>('/v3/User/Permissions/UpdateSelf', {
|
||||
permissions: {
|
||||
presence: {
|
||||
toValue: to,
|
||||
fromValue: from,
|
||||
},
|
||||
etag,
|
||||
},
|
||||
}));
|
||||
etag,
|
||||
});
|
||||
}
|
||||
|
||||
async getWebServiceToken(id: string) {
|
||||
|
|
@ -142,9 +137,7 @@ export default class ZncApi {
|
|||
timestamp,
|
||||
};
|
||||
|
||||
return this.fetch<WebServiceToken>('/v2/Game/GetWebServiceToken', 'POST', JSON.stringify({
|
||||
parameter: req,
|
||||
}));
|
||||
return this.call<WebServiceToken>('/v2/Game/GetWebServiceToken', req);
|
||||
}
|
||||
|
||||
async getToken(token: string, user: NintendoAccountUser) {
|
||||
|
|
@ -168,9 +161,7 @@ export default class ZncApi {
|
|||
naIdToken: id_token,
|
||||
};
|
||||
|
||||
const data = await this.fetch<AccountToken>('/v3/Account/GetToken', 'POST', JSON.stringify({
|
||||
parameter: req,
|
||||
}));
|
||||
const data = await this.call<AccountToken>('/v3/Account/GetToken', req);
|
||||
|
||||
return {
|
||||
uuid,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import fetch, { RequestInfo, RequestInit, Response } from 'node-fetch';
|
||||
import { ErrorResponse } from '../../index.js';
|
||||
|
||||
export enum RequestState {
|
||||
NOT_LOADING,
|
||||
|
|
@ -40,28 +42,6 @@ export function useFetch<T>(requestInfo: RequestInfo | null, init?: RequestInit,
|
|||
return useAsync<T | Response>(requestInfo ? f : null);
|
||||
}
|
||||
|
||||
export class ErrorResponse extends Error {
|
||||
readonly data: any | undefined = undefined;
|
||||
|
||||
constructor(message: string, readonly response: Response, readonly body?: string) {
|
||||
super(message);
|
||||
|
||||
try {
|
||||
this.data = body ? JSON.parse(body) : undefined;
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(ErrorResponse, Symbol.hasInstance, {
|
||||
configurable: true,
|
||||
value: (instance: ErrorResponse) => {
|
||||
return instance instanceof Error &&
|
||||
'response' in instance &&
|
||||
'body' in instance &&
|
||||
'data' in instance;
|
||||
},
|
||||
});
|
||||
|
||||
export function useFetchJson<T>(requestInfo: RequestInfo | null, init?: RequestInit) {
|
||||
return useFetch(requestInfo, init, response => {
|
||||
if (response.status !== 200) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import createDebug from 'debug';
|
||||
|
||||
// Logs are written to the browser window developer tools, and are hidden by default (enable verbose logs)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import * as path from 'path';
|
||||
import createDebug from 'debug';
|
||||
import persist from 'node-persist';
|
||||
import DiscordRPC from 'discord-rpc';
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export class SplatNet2RecordsMonitor extends Loop {
|
|||
}
|
||||
|
||||
async handleError(err: Error | ErrorResponse<WebServiceError>): Promise<LoopResult> {
|
||||
if ('response' in err && err.data.code === 'AUTHENTICATION_ERROR') {
|
||||
if ('response' in err && err.data?.code === 'AUTHENTICATION_ERROR') {
|
||||
// Token expired
|
||||
debug('Renewing iksm_session cookie');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import createDebug from 'debug';
|
||||
import persist from 'node-persist';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { getToken } from '../../util.js';
|
||||
import SplatNet2Api from '../../api/splatnet2.js';
|
||||
import { WebServiceToken } from '../../api/znc-types.js';
|
||||
|
|
@ -47,7 +46,7 @@ export async function getIksmToken(storage: persist.LocalStorage, token: string,
|
|||
await storage.setItem('IksmToken.' + token, existingToken);
|
||||
|
||||
if (!iksm_sessions.has(existingToken.iksm_session)) {
|
||||
iksm_sessions.set(existingToken.iksm_session, [storage, token, null]);
|
||||
iksm_sessions.set(existingToken.iksm_session, [storage, token, null, null]);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -59,7 +58,7 @@ export async function getIksmToken(storage: persist.LocalStorage, token: string,
|
|||
debug('Using existing token');
|
||||
|
||||
if (!iksm_sessions.has(existingToken.iksm_session)) {
|
||||
iksm_sessions.set(existingToken.iksm_session, [storage, token, null]);
|
||||
iksm_sessions.set(existingToken.iksm_session, [storage, token, null, null]);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -79,7 +78,7 @@ export async function renewIksmToken(splatnet: SplatNet2Api, storage: persist.Lo
|
|||
await storage.setItem('IksmToken.' + token, existingToken);
|
||||
|
||||
if (!iksm_sessions.has(existingToken.iksm_session)) {
|
||||
iksm_sessions.set(existingToken.iksm_session, [storage, token, null]);
|
||||
iksm_sessions.set(existingToken.iksm_session, [storage, token, null, null]);
|
||||
}
|
||||
|
||||
iksm_sessions.delete(splatnet.iksm_session);
|
||||
|
|
@ -88,30 +87,51 @@ export async function renewIksmToken(splatnet: SplatNet2Api, storage: persist.Lo
|
|||
splatnet.useragent = existingToken.useragent;
|
||||
}
|
||||
|
||||
const iksm_sessions = new Map<string, [persist.LocalStorage, string, number | null]>();
|
||||
const iksm_sessions = new Map<string, [persist.LocalStorage, string, number | null, NodeJS.Timeout | null]>();
|
||||
|
||||
export function updateIksmSessionLastUsed(iksm_session: string, last_used: number = Date.now()) {
|
||||
const match = iksm_sessions.get(iksm_session);
|
||||
if (!match) return;
|
||||
|
||||
const [storage, token] = match;
|
||||
const [storage, token,, timeout] = match;
|
||||
|
||||
iksm_sessions.set(iksm_session, [storage, token, last_used]);
|
||||
const new_timeout = timeout ?? setTimeout(() => {
|
||||
const match = iksm_sessions.get(iksm_session);
|
||||
if (!match) return;
|
||||
|
||||
const [storage, token, last_used, timeout] = match;
|
||||
if (timeout === new_timeout) match[3] = null;
|
||||
|
||||
writeUpdatedIksmSessionLastUsed(storage, token, last_used!);
|
||||
match[2] = null;
|
||||
}, 1000);
|
||||
|
||||
iksm_sessions.set(iksm_session, [storage, token, last_used, new_timeout]);
|
||||
}
|
||||
|
||||
process.on('exit', () => {
|
||||
for (const [iksm_session, [storage, token, last_used]] of iksm_sessions) {
|
||||
function writeUpdatedIksmSessionLastUsed(storage: persist.LocalStorage, token: string, last_used: number) {
|
||||
const datum_str = fs.readFileSync(storage.getDatumPath('IksmToken.' + token), 'utf-8');
|
||||
const datum: persist.Datum = storage.parse(datum_str);
|
||||
const data: SavedIksmSessionToken = datum.value;
|
||||
|
||||
if (data.last_used && data.last_used >= last_used) return;
|
||||
|
||||
data.last_used = last_used;
|
||||
|
||||
const new_datum_str = storage.stringify(datum);
|
||||
fs.writeFileSync(storage.getDatumPath('IksmToken.' + token), new_datum_str, 'utf-8');
|
||||
}
|
||||
|
||||
function writeUpdatedIksmSessionsLastUsed() {
|
||||
for (const [iksm_session, data] of iksm_sessions) {
|
||||
const [storage, token, last_used, timeout] = data;
|
||||
if (timeout) clearTimeout(timeout), data[3] = null;
|
||||
if (!last_used) continue;
|
||||
|
||||
const datum_str = fs.readFileSync(storage.getDatumPath('IksmToken.' + token), 'utf-8');
|
||||
const datum: persist.Datum = storage.parse(datum_str);
|
||||
const data: SavedIksmSessionToken = datum.value;
|
||||
|
||||
if (data.last_used && data.last_used >= last_used) continue;
|
||||
|
||||
data.last_used = last_used;
|
||||
|
||||
const new_datum_str = storage.stringify(datum);
|
||||
fs.writeFileSync(storage.getDatumPath('IksmToken.' + token), new_datum_str, 'utf-8');
|
||||
writeUpdatedIksmSessionLastUsed(storage, token, last_used);
|
||||
data[2] = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
process.on('exit', () => writeUpdatedIksmSessionsLastUsed());
|
||||
process.on('uncaughtExceptionMonitor', () => writeUpdatedIksmSessionsLastUsed());
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ export {
|
|||
ShareColour as SplatNet2ProfileColour,
|
||||
} from './api/splatnet2.js';
|
||||
export * as splatnet2 from './api/splatnet2-types.js';
|
||||
export {
|
||||
default as NooklinkApi,
|
||||
NooklinkUserApi,
|
||||
MessageType as NooklinkMessageType,
|
||||
} from './api/nooklink.js';
|
||||
export * as nooklink from './api/nooklink-types.js';
|
||||
|
||||
export { getTitleIdFromEcUrl } from './util.js';
|
||||
export { ErrorResponse } from './api/util.js';
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user