Add CoralApiInterface

This commit is contained in:
Samuel Elliott
2023-07-04 21:01:25 +01:00
parent 865a09ee99
commit d1785ee1fc
15 changed files with 214 additions and 169 deletions

View File

@@ -1,13 +1,14 @@
import fetch, { Response } from 'node-fetch';
import { v4 as uuidgen } from 'uuid';
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, WebServiceTokenParameter } from './coral-types.js';
import { generateAuthData, getNintendoAccountToken, getNintendoAccountUser, NintendoAccountSessionAuthorisation, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse, ResponseSymbol } from './util.js';
import createDebug from '../util/debug.js';
import { JwtPayload } from '../util/jwt.js';
import { getAdditionalUserAgents } from '../util/useragent.js';
import { timeoutSignal } from '../util/misc.js';
import { getAdditionalUserAgents } from '../util/useragent.js';
import type { CoralRemoteConfig } from '../common/remote-config.js';
import { AccountLogin, AccountLoginParameter, AccountToken, AccountTokenParameter, Announcements, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, CurrentUser, CurrentUserPermissions, Event, FriendCodeUrl, FriendCodeUser, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, WebServiceTokenParameter } from './coral-types.js';
import { f, FResult, HashMethod } from './f.js';
import { generateAuthData, getNintendoAccountToken, getNintendoAccountUser, NintendoAccountSessionAuthorisation, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { ErrorResponse, ResponseSymbol } from './util.js';
const debug = createDebug('nxapi:api:coral');
@@ -40,7 +41,39 @@ export interface ResultData<T> {
correlationId: string;
}
export default class CoralApi {
export interface CoralApiInterface {
getAnnouncements(): Promise<Result<Announcements>>;
getFriendList(): Promise<Result<Friends>>;
addFavouriteFriend(nsa_id: string): Promise<Result<{}>>;
removeFavouriteFriend(nsa_id: string): Promise<Result<{}>>;
getWebServices(): Promise<Result<WebServices>>;
getActiveEvent(): Promise<Result<GetActiveEventResult>>;
getEvent(id: number): Promise<Result<Event>>;
getUser(id: number): Promise<Result<User>>;
getUserByFriendCode(friend_code: string, hash?: string): Promise<Result<FriendCodeUser>>;
getCurrentUser(): Promise<Result<CurrentUser>>;
getFriendCodeUrl(): Promise<Result<FriendCodeUrl>>;
getCurrentUserPermissions(): Promise<Result<CurrentUserPermissions>>;
getWebServiceToken(id: number): Promise<Result<WebServiceToken>>;
}
export interface ClientInfo {
platform: string;
version: string;
useragent: string;
}
const RemoteConfigSymbol = Symbol('RemoteConfigSymbol');
const ClientInfoSymbol = Symbol('CoralClientInfo');
const CoralUserIdSymbol = Symbol('CoralUserId');
const NintendoAccountIdSymbol = Symbol('NintendoAccountId');
export default class CoralApi implements CoralApiInterface {
[RemoteConfigSymbol]!: CoralRemoteConfig | null;
[ClientInfoSymbol]: ClientInfo;
[CoralUserIdSymbol]: string;
[NintendoAccountIdSymbol]: string;
onTokenExpired: ((data?: CoralErrorResponse, res?: Response) => Promise<CoralAuthData | void>) | null = null;
/** @internal */
_renewToken: Promise<void> | null = null;
@@ -50,11 +83,30 @@ export default class CoralApi {
protected constructor(
public token: string,
public useragent: string | null = getAdditionalUserAgents(),
public coral_user_id: string,
public na_id: string,
readonly znca_version = ZNCA_VERSION,
readonly znca_useragent = ZNCA_USER_AGENT,
) {}
coral_user_id: string,
na_id: string,
znca_version = ZNCA_VERSION,
znca_useragent = ZNCA_USER_AGENT,
config?: CoralRemoteConfig,
) {
this[ClientInfoSymbol] = {platform: ZNCA_PLATFORM, version: znca_version, useragent: znca_useragent};
this[CoralUserIdSymbol] = coral_user_id;
this[NintendoAccountIdSymbol] = na_id;
Object.defineProperty(this, RemoteConfigSymbol, {enumerable: false, value: config ?? null});
Object.defineProperty(this, 'token', {enumerable: false, value: this.token});
Object.defineProperty(this, '_renewToken', {enumerable: false, value: this._renewToken});
Object.defineProperty(this, '_token_expired', {enumerable: false, value: this._token_expired});
}
/** @internal */
get znca_version() {
return this[ClientInfoSymbol].version;
}
/** @internal */
get znca_useragent() {
return this[ClientInfoSymbol].useragent;
}
async fetch<T = unknown>(
url: string, method = 'GET', body?: string, headers?: object,
@@ -79,11 +131,11 @@ export default class CoralApi {
const response = await fetch(ZNC_URL + url, {
method,
headers: Object.assign({
'X-Platform': ZNCA_PLATFORM,
'X-ProductVersion': this.znca_version,
'X-Platform': this[ClientInfoSymbol].platform,
'X-ProductVersion': this[ClientInfoSymbol].version,
'Authorization': 'Bearer ' + this.token,
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': this.znca_useragent,
'User-Agent': this[ClientInfoSymbol].useragent,
}, headers),
body,
signal,
@@ -148,15 +200,15 @@ export default class CoralApi {
return this.call<Friends>('/v3/Friend/List');
}
async addFavouriteFriend(nsaid: string) {
async addFavouriteFriend(nsa_id: string) {
return this.call<{}>('/v3/Friend/Favorite/Create', {
nsaId: nsaid,
nsaId: nsa_id,
});
}
async removeFavouriteFriend(nsaid: string) {
async removeFavouriteFriend(nsa_id: string) {
return this.call<{}>('/v3/Friend/Favorite/Delete', {
nsaId: nsaid,
nsaId: nsa_id,
});
}
@@ -226,10 +278,10 @@ export default class CoralApi {
await this._renewToken;
const data = await f(this.token, HashMethod.WEB_SERVICE, {
platform: ZNCA_PLATFORM,
version: this.znca_version,
platform: this[ClientInfoSymbol].platform,
version: this[ClientInfoSymbol].version,
useragent: this.useragent ?? getAdditionalUserAgents(),
user: {na_id: this.na_id, coral_user_id: this.coral_user_id},
user: {na_id: this[NintendoAccountIdSymbol], coral_user_id: this[CoralUserIdSymbol]},
});
const req: WebServiceTokenParameter = {
@@ -268,10 +320,10 @@ export default class CoralApi {
nintendoAccountToken: NintendoAccountToken, user: NintendoAccountUser,
): Promise<PartialCoralAuthData> {
const fdata = await f(nintendoAccountToken.id_token, HashMethod.CORAL, {
platform: ZNCA_PLATFORM,
version: this.znca_version,
platform: this[ClientInfoSymbol].platform,
version: this[ClientInfoSymbol].version,
useragent: this.useragent ?? getAdditionalUserAgents(),
user: {na_id: user.id, coral_user_id: this.coral_user_id},
user: {na_id: user.id, coral_user_id: this[CoralUserIdSymbol]},
});
const req: AccountTokenParameter = {
@@ -305,11 +357,10 @@ export default class CoralApi {
return data;
}
/** @private */
setTokenWithSavedToken(data: CoralAuthData | PartialCoralAuthData) {
protected setTokenWithSavedToken(data: CoralAuthData | PartialCoralAuthData) {
this.token = data.credential.accessToken;
this.coral_user_id = '' + data.nsoAccount.user.id;
if ('user' in data) this.na_id = data.user.id;
this[CoralUserIdSymbol] = '' + data.nsoAccount.user.id;
if ('user' in data) this[NintendoAccountIdSymbol] = data.user.id;
this._token_expired = false;
}

View File

@@ -295,10 +295,12 @@ export enum NintendoAccountScope {
}
export enum NintendoAccountJwtScope {
'openid' = 0,
'offline' = 1,
'user' = 8,
'user.birthday' = 9,
'user.mii' = 17,
'user.screenName' = 23,
'user.links.nintendoNetwork.id' = 31,
'moonUser:administration' = 320,
'moonDevice:create' = 321,
'moonOwnedDevice:administration' = 325,
@@ -323,10 +325,6 @@ export enum NintendoAccountJwtScope {
// 'pointWallet' = -1,
// 'userNotificationMessage:anyClients' = -1,
// 'userNotificationMessage:anyClients:write' = -1,
// 1, 31
// 'offline' = -1,
// 'user.links.nintendoNetwork.id' = -1,
}
export interface NintendoAccountUser {

View File

@@ -2,7 +2,7 @@ import fetch, { Response } from 'node-fetch';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
import CoralApi from './coral.js';
import { CoralApiInterface } from './coral.js';
import { WebServiceError, Users, AuthToken, UserProfile, Newspapers, Newspaper, Emoticons, Reaction, IslandProfile } from './nooklink-types.js';
import createDebug from '../util/debug.js';
import { timeoutSignal } from '../util/misc.js';
@@ -107,8 +107,8 @@ export default class NooklinkApi {
return NooklinkUserApi._createWithNooklinkApi(this, user_id);
}
async renewTokenWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await NooklinkApi.loginWithCoral(nso, user);
async renewTokenWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const data = await NooklinkApi.loginWithCoral(coral, user);
this.setTokenWithSavedToken(data);
return data;
}
@@ -124,8 +124,8 @@ export default class NooklinkApi {
this._token_expired = false;
}
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await this.loginWithCoral(nso, user);
static async createWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const data = await this.loginWithCoral(coral, user);
return {nooklink: this.createWithSavedToken(data), data};
}
@@ -133,11 +133,11 @@ export default class NooklinkApi {
return new this(data.gtoken, data.useragent);
}
static async loginWithCoral(nso: CoralApi, user: NintendoAccountUser) {
static async loginWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const { default: { coral_gws_nooklink: config } } = await import('../common/remote-config.js');
if (!config) throw new Error('Remote configuration prevents NookLink authentication');
const webserviceToken = await nso.getWebServiceToken(NOOKLINK_WEBSERVICE_ID);
const webserviceToken = await coral.getWebServiceToken(NOOKLINK_WEBSERVICE_ID);
return this.loginWithWebServiceToken(webserviceToken, user);
}

View File

@@ -3,7 +3,7 @@ import { v4 as uuidgen } from 'uuid';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
import { defineResponse, ErrorResponse } from './util.js';
import CoralApi from './coral.js';
import { CoralApiInterface } 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 createDebug from '../util/debug.js';
import { timeoutSignal } from '../util/misc.js';
@@ -240,8 +240,8 @@ ${colour}
});
}
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await this.loginWithCoral(nso, user);
static async createWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const data = await this.loginWithCoral(coral, user);
return {splatnet: this.createWithSavedToken(data), data};
}
@@ -269,8 +269,8 @@ ${colour}
);
}
static async loginWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const webserviceToken = await nso.getWebServiceToken(SPLATNET2_WEBSERVICE_ID);
static async loginWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const webserviceToken = await coral.getWebServiceToken(SPLATNET2_WEBSERVICE_ID);
return this.loginWithWebServiceToken(webserviceToken, user);
}

View File

@@ -1,7 +1,7 @@
import fetch, { Response } from 'node-fetch';
import { BankaraBattleHistoriesRefetchResult, BankaraBattleHistoriesRefetchVariables, GraphQLRequest, GraphQLResponse, GraphQLSuccessResponse, KnownRequestId, LatestBattleHistoriesRefetchResult, LatestBattleHistoriesRefetchVariables, MyOutfitInput, PagerUpdateBattleHistoriesByVsModeResult, PagerUpdateBattleHistoriesByVsModeVariables, PrivateBattleHistoriesRefetchResult, PrivateBattleHistoriesRefetchVariables, RegularBattleHistoriesRefetchResult, RegularBattleHistoriesRefetchVariables, RequestId, ResultTypes, VariablesTypes, XBattleHistoriesRefetchResult, XBattleHistoriesRefetchVariables } from 'splatnet3-types/splatnet3';
import { WebServiceToken } from './coral-types.js';
import CoralApi from './coral.js';
import { CoralApiInterface } from './coral.js';
import { NintendoAccountUser } from './na.js';
import { BulletToken } from './splatnet3-types.js';
import { defineResponse, ErrorResponse, HasResponse, ResponseSymbol } from './util.js';
@@ -928,8 +928,8 @@ export default class SplatNet3Api {
//
//
async renewTokenWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await SplatNet3Api.loginWithCoral(nso, user);
async renewTokenWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const data = await SplatNet3Api.loginWithCoral(coral, user);
this.setTokenWithSavedToken(data);
return data;
}
@@ -948,8 +948,8 @@ export default class SplatNet3Api {
this._token_expired = false;
}
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await this.loginWithCoral(nso, user);
static async createWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const data = await this.loginWithCoral(coral, user);
return {splatnet: this.createWithSavedToken(data), data};
}
@@ -977,11 +977,11 @@ export default class SplatNet3Api {
);
}
static async loginWithCoral(nso: CoralApi, user: NintendoAccountUser) {
static async loginWithCoral(coral: CoralApiInterface, user: NintendoAccountUser) {
const { default: { coral_gws_splatnet3: config } } = await import('../common/remote-config.js');
if (!config) throw new Error('Remote configuration prevents SplatNet 3 authentication');
const webserviceToken = await nso.getWebServiceToken(SPLATNET3_WEBSERVICE_ID);
const webserviceToken = await coral.getWebServiceToken(SPLATNET3_WEBSERVICE_ID);
return this.loginWithWebServiceToken(webserviceToken, user);
}

View File

@@ -1,7 +1,7 @@
import fetch, { Response } from 'node-fetch';
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralErrorResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
import { defineResponse, ErrorResponse, ResponseSymbol } from './util.js';
import CoralApi, { CoralAuthData, CorrelationIdSymbol, PartialCoralAuthData, ResponseDataSymbol, Result } from './coral.js';
import { CoralApiInterface, CoralAuthData, CorrelationIdSymbol, PartialCoralAuthData, ResponseDataSymbol, Result } from './coral.js';
import { NintendoAccountToken, NintendoAccountUser } from './na.js';
import { SavedToken } from '../common/auth/coral.js';
import createDebug from '../util/debug.js';
@@ -10,21 +10,7 @@ import { getAdditionalUserAgents, getUserAgent } from '../util/useragent.js';
const debug = createDebug('nxapi:api:znc-proxy');
export default class ZncProxyApi implements CoralApi {
// Not used by ZncProxyApi
onTokenExpired: ((data?: CoralErrorResponse, res?: Response) => Promise<CoralAuthData | void>) | null = null;
/** @internal */
_renewToken: Promise<void> | null = null;
/** @internal */
_token_expired = false;
/** @internal */
na_id = '';
/** @internal */
coral_user_id = '';
readonly znca_version = '';
readonly znca_useragent = '';
export default class ZncProxyApi implements CoralApiInterface {
constructor(
private url: string,
// ZncApi uses the NSO token (valid for a few hours)
@@ -70,15 +56,15 @@ export default class ZncProxyApi implements CoralApi {
return createResult(result, result);
}
async addFavouriteFriend(nsaid: string) {
const result = await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
async addFavouriteFriend(nsa_id: string) {
const result = await this.fetch('/friend/' + nsa_id, 'POST', JSON.stringify({
isFavoriteFriend: true,
}));
return createResult(result, {});
}
async removeFavouriteFriend(nsaid: string) {
const result = await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
async removeFavouriteFriend(nsa_id: string) {
const result = await this.fetch('/friend/' + nsa_id, 'POST', JSON.stringify({
isFavoriteFriend: false,
}));
return createResult(result, {});
@@ -143,7 +129,7 @@ export default class ZncProxyApi implements CoralApi {
return createResult(result, result.token);
}
async getToken(token: string, user: NintendoAccountUser): ReturnType<CoralApi['getToken']> {
async getToken(token: string, user: NintendoAccountUser): Promise<PartialCoralAuthData> {
throw new Error('Not supported in ZncProxyApi');
}
@@ -165,8 +151,7 @@ export default class ZncProxyApi implements CoralApi {
throw new Error('Not supported in ZncProxyApi');
}
/** @private */
setTokenWithSavedToken(data: CoralAuthData | PartialCoralAuthData) {
protected setTokenWithSavedToken(data: CoralAuthData | PartialCoralAuthData) {
throw new Error('Not supported in ZncProxyApi');
}