Return response objects and return coral result object directly

This commit is contained in:
Samuel Elliott
2022-09-07 17:13:53 +01:00
parent 4b5edd73df
commit 9112a5a02b
9 changed files with 138 additions and 76 deletions

View File

@@ -4,7 +4,7 @@ import createDebug from 'debug';
import { f, FResult, HashMethod } from './f.js';
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl, AccountTokenParameter, AccountLoginParameter } from './coral-types.js';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import { ErrorResponse, ResponseSymbol } from './util.js';
import { JwtPayload } from '../util/jwt.js';
import { getAdditionalUserAgents } from '../util/useragent.js';
import { timeoutSignal } from '../util/misc.js';
@@ -22,6 +22,24 @@ export const ZNCA_CLIENT_ID = '71b963c1b7b6d119';
const FRIEND_CODE = /^\d{4}-\d{4}-\d{4}$/;
const FRIEND_CODE_HASH = /^[A-Za-z0-9]{10}$/;
export const ResponseDataSymbol = Symbol('ResponseData');
export const CorrelationIdSymbol = Symbol('CorrelationId');
export type Result<T> = T & ResultData<T>;
export interface ResultData<T> {
[ResponseSymbol]: Response;
[ResponseDataSymbol]: CoralSuccessResponse<T>;
[CorrelationIdSymbol]: string;
/** @deprecated */
status: CoralStatus.OK;
/** @deprecated */
result: T;
/** @deprecated */
correlationId: string;
}
export default class CoralApi {
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<void>) | null = null;
/** @internal */
@@ -38,7 +56,7 @@ export default class CoralApi {
url: string, method = 'GET', body?: string, headers?: object,
/** @internal */ _autoRenewToken = true,
/** @internal */ _attempt = 0
): Promise<CoralSuccessResponse<T>> {
): Promise<Result<T>> {
if (this._renewToken && _autoRenewToken) {
await this._renewToken;
}
@@ -80,7 +98,17 @@ export default class CoralApi {
throw new ErrorResponse('[znc] Unknown error', response, data);
}
return data;
const result = data.result;
Object.defineProperty(result, ResponseSymbol, {enumerable: false, value: response});
Object.defineProperty(result, ResponseDataSymbol, {enumerable: false, value: data});
Object.defineProperty(result, CorrelationIdSymbol, {enumerable: false, value: data.correlationId});
Object.defineProperty(result, 'status', {enumerable: false, value: CoralStatus.OK});
Object.defineProperty(result, 'result', {enumerable: false, value: data.result});
Object.defineProperty(result, 'correlationId', {enumerable: false, value: data.correlationId});
return result as Result<T>;
}
async call<T = unknown>(

View File

@@ -2,7 +2,7 @@ import process from 'node:process';
import fetch from 'node-fetch';
import createDebug from 'debug';
import { v4 as uuidgen } from 'uuid';
import { ErrorResponse } from './util.js';
import { defineResponse, ErrorResponse } from './util.js';
import { timeoutSignal } from '../util/misc.js';
import { getUserAgent } from '../util/useragent.js';
@@ -109,7 +109,7 @@ export async function flapg(
debugFlapg('Got f parameter', data);
return data;
return defineResponse(data, response);
}
/** @deprecated */
@@ -197,7 +197,7 @@ export async function iminkf(
debugImink('Got f parameter "%s"', data.f);
return data;
return defineResponse(data, response);
}
export interface IminkFRequest {
@@ -276,7 +276,7 @@ export async function genf(
debugZncaApi('Got f parameter', data, response.headers);
return data;
return defineResponse(data, response);
}
export interface AndroidZncaFRequest {

View File

@@ -1,7 +1,7 @@
import fetch from 'node-fetch';
import createDebug from 'debug';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import { defineResponse, ErrorResponse } from './util.js';
import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, ParentalControlSettingState, SmartDevices, User } from './moon-types.js';
import { timeoutSignal } from '../util/misc.js';
@@ -59,7 +59,7 @@ export default class MoonApi {
throw new ErrorResponse('[moon] ' + data.title, response, data);
}
return data;
return defineResponse(data, response);
}
async getUser() {

View File

@@ -1,6 +1,6 @@
import fetch from 'node-fetch';
import createDebug from 'debug';
import { ErrorResponse } from './util.js';
import { defineResponse, ErrorResponse } from './util.js';
import { JwtPayload } from '../util/jwt.js';
import { timeoutSignal } from '../util/misc.js';
@@ -26,18 +26,22 @@ export async function getNintendoAccountSessionToken(code: string, verifier: str
signal,
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse('[na] Non-200 status code', response, await response.text());
}
const token = await response.json() as NintendoAccountSessionToken | NintendoAccountAuthError | NintendoAccountError;
if ('errorCode' in token) {
throw new ErrorResponse('[na] ' + token.detail, response, token);
throw new ErrorResponse<NintendoAccountError>('[na] ' + token.detail, response, token);
}
if ('error' in token) {
throw new ErrorResponse('[na] ' + token.error_description ?? token.error, response, token);
throw new ErrorResponse<NintendoAccountAuthError>('[na] ' + token.error_description ?? token.error, response, token);
}
debug('Got Nintendo Account session token', token);
return token;
return defineResponse(token, response);
}
export async function getNintendoAccountToken(token: string, client_id: string) {
@@ -59,18 +63,22 @@ export async function getNintendoAccountToken(token: string, client_id: string)
signal,
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse('[na] Non-200 status code', response, await response.text());
}
const nintendoAccountToken = await response.json() as NintendoAccountToken | NintendoAccountAuthError | NintendoAccountError;
if ('errorCode' in nintendoAccountToken) {
throw new ErrorResponse('[na] ' + nintendoAccountToken.detail, response, nintendoAccountToken);
throw new ErrorResponse<NintendoAccountError>('[na] ' + nintendoAccountToken.detail, response, nintendoAccountToken);
}
if ('error' in nintendoAccountToken) {
throw new ErrorResponse('[na] ' + nintendoAccountToken.error_description ?? nintendoAccountToken.error, response, nintendoAccountToken);
throw new ErrorResponse<NintendoAccountAuthError>('[na] ' + nintendoAccountToken.error_description ?? nintendoAccountToken.error, response, nintendoAccountToken);
}
debug('Got Nintendo Account token', nintendoAccountToken);
return nintendoAccountToken;
return defineResponse(nintendoAccountToken, response);
}
export async function getNintendoAccountUser(token: NintendoAccountToken) {
@@ -88,15 +96,19 @@ export async function getNintendoAccountUser(token: NintendoAccountToken) {
signal,
}).finally(cancel);
if (response.status !== 200) {
throw new ErrorResponse('[na] Non-200 status code', response, await response.text());
}
const user = await response.json() as NintendoAccountUser | NintendoAccountError;
if ('errorCode' in user) {
throw new ErrorResponse('[na] ' + user.detail, response, user);
throw new ErrorResponse<NintendoAccountError>('[na] ' + user.detail, response, user);
}
debug('Got Nintendo Account user info', user);
return user;
return defineResponse(user, response);
}
export interface NintendoAccountSessionToken {
@@ -214,7 +226,7 @@ export enum NintendoAccountJwtScope {
// 'userNotificationMessage:anyClients:write' = -1,
// 1, 31
// 'offline' = -1,
// 'offline' = -1,
// 'user.links.nintendoNetwork.id' = -1,
}

View File

@@ -2,7 +2,7 @@ import fetch from 'node-fetch';
import createDebug from 'debug';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import { defineResponse, ErrorResponse } from './util.js';
import CoralApi from './coral.js';
import { WebServiceError, Users, AuthToken, UserProfile, Newspapers, Newspaper, Emoticons, Reaction, IslandProfile } from './nooklink-types.js';
import { timeoutSignal } from '../util/misc.js';
@@ -32,7 +32,7 @@ export default class NooklinkApi {
'User-Agent': this.useragent,
'Cookie': '_gtoken=' + encodeURIComponent(this.gtoken),
'dnt': '1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-GB,en-US;q=0.8',
'Origin': 'https://web.sd.lp1.acbaa.srv.nintendo.net',
'Content-Type': 'application/json',
@@ -51,10 +51,10 @@ export default class NooklinkApi {
const data = await response.json() as T | WebServiceError;
if ('code' in data) {
throw new ErrorResponse('[nooklink] Error ' + data.code, response, data);
throw new ErrorResponse<WebServiceError>('[nooklink] Error ' + data.code, response, data);
}
return data;
return defineResponse(data, response);
}
async getUsers() {
@@ -119,7 +119,7 @@ export default class NooklinkApi {
const body = await response.text();
if (response.status !== 200) {
throw new ErrorResponse('[nooklink] Unknown error', response, body);
throw new ErrorResponse('[nooklink] Non-200 status code', response, body);
}
const cookies = response.headers.get('Set-Cookie');
@@ -170,9 +170,8 @@ export class NooklinkUserApi {
'User-Agent': this.useragent,
'Cookie': '_gtoken=' + encodeURIComponent(this.gtoken),
'dnt': '1',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept': 'application/json, text/plain,*/*',
'Accept-Language': 'en-GB,en-US;q=0.8',
'X-Requested-With': 'com.nintendo.znca',
'Origin': 'https://web.sd.lp1.acbaa.srv.nintendo.net',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.auth_token,
@@ -191,10 +190,10 @@ export class NooklinkUserApi {
const data = await response.json() as T | WebServiceError;
if ('code' in data) {
throw new ErrorResponse('[nooklink] Error ' + data.code, response, data);
throw new ErrorResponse<WebServiceError>('[nooklink] Error ' + data.code, response, data);
}
return data;
return defineResponse(data, response);
}
async getUserProfile(id?: string) {

View File

@@ -3,7 +3,7 @@ import createDebug from 'debug';
import { v4 as uuidgen } from 'uuid';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import { defineResponse, ErrorResponse } from './util.js';
import CoralApi from './coral.js';
import { ActiveFestivals, CoopResult, CoopResults, CoopSchedules, HeroRecords, LeagueMatchRankings, NicknameAndIcons, PastFestivals, Records, Result, Results, Schedules, ShareResponse, ShopMerchandises, Stages, Timeline, WebServiceError, XPowerRankingRecords, XPowerRankingSummary } from './splatnet2-types.js';
import { timeoutSignal } from '../util/misc.js';
@@ -61,10 +61,10 @@ export default class SplatNet2Api {
const data = await response.json() as T | WebServiceError;
if ('code' in data) {
throw new ErrorResponse('[splatnet2] ' + data.message, response, data);
throw new ErrorResponse<WebServiceError>('[splatnet2] ' + data.message, response, data);
}
return data;
return defineResponse(data, response);
}
async getRecords() {
@@ -295,7 +295,7 @@ ${colour}
const body = await response.text();
if (response.status !== 200) {
throw new ErrorResponse('[splatnet2] Unknown error', response, body);
throw new ErrorResponse('[splatnet2] Non-200 status code', response, body);
}
const cookies = response.headers.get('Set-Cookie');

View File

@@ -1,6 +1,18 @@
import * as util from 'node:util';
import { Response as NodeFetchResponse } from 'node-fetch';
export const ResponseSymbol = Symbol('Response');
export interface ResponseData<R> {
[ResponseSymbol]: R;
}
export type HasResponse<T, R> = T & ResponseData<R>;
export function defineResponse<T, R>(data: T, response: R) {
Object.defineProperty(data, ResponseSymbol, {enumerable: false, value: response});
return data as HasResponse<T, R>;
}
export class ErrorResponse<T = unknown> extends Error {
readonly body: string | undefined;
readonly data: T | undefined = undefined;

View File

@@ -1,8 +1,8 @@
import fetch, { Response } from 'node-fetch';
import createDebug from 'debug';
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralErrorResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
import { ErrorResponse } from './util.js';
import CoralApi from './coral.js';
import { defineResponse, ErrorResponse, ResponseSymbol } from './util.js';
import CoralApi, { CorrelationIdSymbol, ResponseDataSymbol, Result } from './coral.js';
import { NintendoAccountUser } from './na.js';
import { SavedToken } from '../common/auth/coral.js';
import { timeoutSignal } from '../util/misc.js';
@@ -41,106 +41,100 @@ export default class ZncProxyApi implements CoralApi {
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status === 204) return null!;
if (response.status !== 200) {
if (response.status !== 200 && response.status !== 204) {
throw new ErrorResponse('[zncproxy] Non-200/204 status code', response, await response.text());
}
const data = await response.json() as T;
const data = (response.status === 204 ? {} : await response.json()) as T;
return data;
return defineResponse(data, response);
}
async call<T = unknown>(url: string, parameter = {}): Promise<CoralSuccessResponse<T>> {
async call<T = unknown>(url: string, parameter = {}): Promise<Result<T>> {
throw new Error('Not supported in ZncProxyApi');
}
async getAnnouncements() {
const response = await this.fetch<{announcements: Announcements}>('/announcements');
return {status: CoralStatus.OK as const, result: response.announcements, correlationId: ''};
const result = await this.fetch<{announcements: Announcements}>('/announcements');
return createResult(result, result.announcements);
}
async getFriendList() {
const response = await this.fetch<{friends: Friend[]}>('/friends');
return {status: CoralStatus.OK as const, result: response, correlationId: ''};
const result = await this.fetch<{friends: Friend[]}>('/friends');
return createResult(result, result);
}
async addFavouriteFriend(nsaid: string) {
await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
const result = await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
isFavoriteFriend: true,
}));
return {status: CoralStatus.OK as const, result: {}, correlationId: ''};
return createResult(result, {});
}
async removeFavouriteFriend(nsaid: string) {
await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
const result = await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
isFavoriteFriend: false,
}));
return {status: CoralStatus.OK as const, result: {}, correlationId: ''};
return createResult(result, {});
}
async getWebServices() {
const response = await this.fetch<{webservices: WebService[]}>('/webservices');
return {status: CoralStatus.OK as const, result: response.webservices, correlationId: ''};
const result = await this.fetch<{webservices: WebService[]}>('/webservices');
return createResult(result, result.webservices);
}
async getActiveEvent() {
const response = await this.fetch<{activeevent: ActiveEvent}>('/activeevent');
return {status: CoralStatus.OK as const, result: response.activeevent, correlationId: ''};
const result = await this.fetch<{activeevent: ActiveEvent}>('/activeevent');
return createResult(result, result.activeevent);
}
async getEvent(id: number) {
const response = await this.fetch<{event: Event}>('/event/' + id);
return {status: CoralStatus.OK as const, result: response.event, correlationId: ''};
const result = await this.fetch<{event: Event}>('/event/' + id);
return createResult(result, result.event);
}
async getUser(id: number) {
const response = await this.fetch<{user: User}>('/user/' + id);
return {status: CoralStatus.OK as const, result: response.user, correlationId: ''};
const result = await this.fetch<{user: User}>('/user/' + id);
return createResult(result, result.user);
}
async getUserByFriendCode(friend_code: string, hash?: string) {
const response = await this.fetch<{user: FriendCodeUser}>('/friendcode/' + friend_code);
return {status: CoralStatus.OK as const, result: response.user, correlationId: ''};
const result = await this.fetch<{user: FriendCodeUser}>('/friendcode/' + friend_code);
return createResult(result, result.user);
}
async sendFriendRequest(nsa_id: string): Promise<CoralSuccessResponse<{}>> {
async sendFriendRequest(nsa_id: string): Promise<Result<{}>> {
throw new Error('Not supported in ZncProxyApi');
}
async getCurrentUser() {
const response = await this.fetch<{user: CurrentUser}>('/user');
return {status: CoralStatus.OK as const, result: response.user, correlationId: ''};
const result = await this.fetch<{user: CurrentUser}>('/user');
return createResult(result, result.user);
}
async getFriendCodeUrl() {
const response = await this.fetch<{friendcode: FriendCodeUrl}>('/friendcode');
return {status: CoralStatus.OK as const, result: response.friendcode, correlationId: ''};
const result = await this.fetch<{friendcode: FriendCodeUrl}>('/friendcode');
return createResult(result, result.friendcode);
}
async getCurrentUserPermissions() {
const user = await this.getCurrentUser();
return {
status: CoralStatus.OK as const,
result: {
etag: user.result.etag,
permissions: user.result.permissions,
},
correlationId: '',
};
return createResult(user, {
etag: user.etag,
permissions: user.permissions,
});
}
async updateCurrentUserPermissions(
to: PresencePermissions, from: PresencePermissions, etag: string
): Promise<CoralSuccessResponse<{}>> {
): Promise<Result<{}>> {
throw new Error('Not supported in ZncProxyApi');
}
async getWebServiceToken(id: string) {
const response = await this.fetch<{token: WebServiceToken}>('/webservice/' + id + '/token');
return {status: CoralStatus.OK as const, result: response.token, correlationId: ''};
const result = await this.fetch<{token: WebServiceToken}>('/webservice/' + id + '/token');
return createResult(result, result.token);
}
async getToken(token: string, user: NintendoAccountUser): ReturnType<CoralApi['getToken']> {
@@ -162,6 +156,24 @@ export default class ZncProxyApi implements CoralApi {
}
}
function createResult<T extends {}, R>(data: R & {[ResponseSymbol]: Response}, result: T): Result<T> {
const coral_result: CoralSuccessResponse<T> = {
status: CoralStatus.OK as const,
result,
correlationId: '',
};
Object.defineProperty(result, ResponseSymbol, {enumerable: false, value: data[ResponseSymbol]});
Object.defineProperty(result, ResponseDataSymbol, {enumerable: false, value: coral_result});
Object.defineProperty(result, CorrelationIdSymbol, {enumerable: false, value: ''});
Object.defineProperty(result, 'status', {enumerable: false, value: CoralStatus.OK});
Object.defineProperty(result, 'result', {enumerable: false, value: result});
Object.defineProperty(result, 'correlationId', {enumerable: false, value: ''});
return result as Result<T>;
}
export interface AuthToken {
user: string;
policy?: AuthPolicy;

View File

@@ -3,7 +3,7 @@ import * as fs from 'node:fs/promises';
import fetch from 'node-fetch';
import createDebug from 'debug';
import mkdirp from 'mkdirp';
import { ErrorResponse } from '../api/util.js';
import { ErrorResponse, ResponseSymbol } from '../api/util.js';
import { timeoutSignal } from '../util/misc.js';
import { getUserAgent } from '../util/useragent.js';
import { paths } from '../util/storage.js';
@@ -18,7 +18,6 @@ const MAX_FRESH = 24 * 60 * 60; // 1 day in seconds
const MAX_STALE = 24 * 60 * 60; // 1 day in seconds
const SourceSymbol = Symbol('Source');
const ResponseSymbol = Symbol('Response');
const CachedSymbol = Symbol('Cached');
const default_config: NxapiRemoteConfig = {