Export NookLink client, remove duplicate ErrorResponse class

This commit is contained in:
Samuel Elliott
2022-04-26 16:50:19 +01:00
parent ff6706386d
commit 81cec767df
11 changed files with 136 additions and 122 deletions

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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: ''};

View File

@@ -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,