mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Coral 3.0.3
This commit is contained in:
parent
2959c923bb
commit
57ce3c21de
1162
src/api/coral.ts
1162
src/api/coral.ts
File diff suppressed because it is too large
Load Diff
10
src/api/f.ts
10
src/api/f.ts
|
|
@ -311,6 +311,7 @@ export type FResult = {
|
|||
f: string;
|
||||
user?: {na_id: string; coral_user_id?: string;};
|
||||
result: unknown;
|
||||
encrypt_request_result?: Uint8Array;
|
||||
} & ({
|
||||
provider: 'flapg';
|
||||
result: FlapgApiResponse;
|
||||
|
|
@ -328,6 +329,15 @@ interface ZncaApiOptions {
|
|||
platform?: string;
|
||||
version?: string;
|
||||
user?: {na_id: string; coral_user_id?: string;};
|
||||
encrypt_request?: EncryptRequestOptions;
|
||||
}
|
||||
interface EncryptRequestOptions {
|
||||
url: string;
|
||||
parameter: AccountLoginParameter | AccountTokenParameter | WebServiceTokenParameter;
|
||||
}
|
||||
|
||||
export async function createZncaApi(options?: ZncaApiOptions) {
|
||||
return getPreferredZncaApiFromEnvironment(options) ?? await getDefaultZncaApi(options);
|
||||
}
|
||||
|
||||
export function getPreferredZncaApiFromEnvironment(options?: ZncaApiOptions): ZncaApi | null;
|
||||
|
|
|
|||
|
|
@ -198,6 +198,8 @@ const na_client_settings = {
|
|||
].join(' '),
|
||||
};
|
||||
|
||||
export type NintendoAccountUserMoon = NintendoAccountUser;
|
||||
|
||||
export class NintendoAccountSessionAuthorisationMoon extends NintendoAccountSessionAuthorisation {
|
||||
protected constructor(
|
||||
authorise_url: string,
|
||||
|
|
@ -220,7 +222,7 @@ export class NintendoAccountSessionAuthorisationMoon extends NintendoAccountSess
|
|||
|
||||
export interface MoonAuthData {
|
||||
nintendoAccountToken: NintendoAccountToken;
|
||||
user: NintendoAccountUser;
|
||||
user: NintendoAccountUserMoon;
|
||||
znma_version: string;
|
||||
znma_build: string;
|
||||
znma_useragent: string;
|
||||
|
|
|
|||
178
src/api/na.ts
178
src/api/na.ts
|
|
@ -11,6 +11,8 @@ const debug = createDebug('nxapi:api:na');
|
|||
export class NintendoAccountSessionAuthorisation {
|
||||
readonly scope: string;
|
||||
|
||||
result: NintendoAccountSessionAuthorisationResult | null = null;
|
||||
|
||||
protected constructor(
|
||||
readonly client_id: string,
|
||||
scope: string | string[],
|
||||
|
|
@ -20,13 +22,23 @@ export class NintendoAccountSessionAuthorisation {
|
|||
readonly redirect_uri = 'npf' + client_id + '://auth',
|
||||
) {
|
||||
this.scope = typeof scope === 'string' ? scope : scope.join(' ');
|
||||
|
||||
Object.defineProperty(this, 'result', {enumerable: false});
|
||||
}
|
||||
|
||||
async getSessionToken(code: string, state?: string): Promise<HasResponse<NintendoAccountSessionToken, Response>>
|
||||
async getSessionToken(params: URLSearchParams): Promise<HasResponse<NintendoAccountSessionToken, Response>>
|
||||
async getSessionToken(code: string | URLSearchParams | null, state?: string | null) {
|
||||
if (this.result) {
|
||||
throw new Error('NintendoAccountSessionAuthorisation already completed');
|
||||
}
|
||||
|
||||
let result_state = state;
|
||||
|
||||
if (code instanceof URLSearchParams) {
|
||||
if (code.get('state') !== this.state) {
|
||||
const result_state = code.get('state');
|
||||
|
||||
if (result_state !== this.state) {
|
||||
throw new TypeError('Invalid state');
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +58,8 @@ export class NintendoAccountSessionAuthorisation {
|
|||
throw new TypeError('Invalid code');
|
||||
}
|
||||
|
||||
Object.defineProperty(this, 'result', {value: {state: result_state, code}, writable: false});
|
||||
|
||||
return getNintendoAccountSessionToken(code, this.verifier, this.client_id);
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +77,11 @@ export class NintendoAccountSessionAuthorisation {
|
|||
}
|
||||
}
|
||||
|
||||
interface NintendoAccountSessionAuthorisationResult {
|
||||
state: string | null | undefined;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class NintendoAccountSessionAuthorisationError extends Error {
|
||||
constructor(readonly code: string, message?: string) {
|
||||
super(message);
|
||||
|
|
@ -182,7 +201,7 @@ export async function getNintendoAccountToken(token: string, client_id: string)
|
|||
return defineResponse(nintendoAccountToken, response);
|
||||
}
|
||||
|
||||
export async function getNintendoAccountUser(token: NintendoAccountToken) {
|
||||
export async function getNintendoAccountUser<S extends NintendoAccountScope>(token: NintendoAccountToken) {
|
||||
debug('Getting Nintendo Account user info');
|
||||
|
||||
const [signal, cancel] = timeoutSignal();
|
||||
|
|
@ -201,7 +220,7 @@ export async function getNintendoAccountUser(token: NintendoAccountToken) {
|
|||
throw await NintendoAccountErrorResponse.fromResponse(response, '[na] Non-200 status code');
|
||||
}
|
||||
|
||||
const user = await response.json() as NintendoAccountUser | NintendoAccountError;
|
||||
const user = await response.json() as NintendoAccountUser<S> | NintendoAccountError;
|
||||
|
||||
if ('errorCode' in user) {
|
||||
throw new NintendoAccountErrorResponse('[na] ' + user.detail, response, user);
|
||||
|
|
@ -221,7 +240,7 @@ export interface NintendoAccountSessionTokenJwtPayload extends JwtPayload {
|
|||
typ: 'session_token';
|
||||
iss: 'https://accounts.nintendo.com';
|
||||
/** Unknown - scopes the token is valid for? */
|
||||
'st:scp': number[];
|
||||
'st:scp': NintendoAccountJwtScope[];
|
||||
/** Subject (Nintendo Account ID) */
|
||||
sub: string;
|
||||
exp: number;
|
||||
|
|
@ -231,7 +250,7 @@ export interface NintendoAccountSessionTokenJwtPayload extends JwtPayload {
|
|||
}
|
||||
|
||||
export interface NintendoAccountToken {
|
||||
scope: string[];
|
||||
scope: NintendoAccountScope[];
|
||||
token_type: 'Bearer';
|
||||
id_token: string;
|
||||
access_token?: string;
|
||||
|
|
@ -246,7 +265,7 @@ export interface NintendoAccountIdTokenJwtPayload extends JwtPayload {
|
|||
aud: string;
|
||||
iss: 'https://accounts.nintendo.com';
|
||||
jti: string;
|
||||
at_hash: string; // ??
|
||||
at_hash: string;
|
||||
typ: 'id_token';
|
||||
country: string;
|
||||
}
|
||||
|
|
@ -258,7 +277,7 @@ export interface NintendoAccountAccessTokenJwtPayload extends JwtPayload {
|
|||
sub: string;
|
||||
iat: number;
|
||||
'ac:grt': number; // ??
|
||||
'ac:scp': number[]; // ??
|
||||
'ac:scp': NintendoAccountJwtScope[];
|
||||
exp: number;
|
||||
/** Audience (client ID) */
|
||||
aud: string;
|
||||
|
|
@ -329,70 +348,121 @@ export enum NintendoAccountJwtScope {
|
|||
// 'userNotificationMessage:anyClients:write' = -1,
|
||||
}
|
||||
|
||||
export interface NintendoAccountUser {
|
||||
emailOptedIn: boolean;
|
||||
language: string;
|
||||
country: string;
|
||||
timezone: {
|
||||
name: string;
|
||||
id: string;
|
||||
utcOffsetSeconds: number;
|
||||
utcOffset: string;
|
||||
};
|
||||
region: null;
|
||||
export interface NintendoAccountUser<S extends NintendoAccountScope = never> {
|
||||
id: string;
|
||||
nickname: string;
|
||||
clientFriendsOptedIn: boolean;
|
||||
mii: Mii | null;
|
||||
iconUri: string;
|
||||
/** requires scope user.screenName */
|
||||
screenName: NintendoAccountScope.USER_SCREENNAME extends S ? string : undefined;
|
||||
/** requires scope user.birthday */
|
||||
birthday: NintendoAccountScope.USER_BIRTHDAY extends S ? string : undefined;
|
||||
isChild: boolean;
|
||||
eachEmailOptedIn: {
|
||||
survey: {
|
||||
updatedAt: number;
|
||||
optedIn: boolean;
|
||||
};
|
||||
deals: {
|
||||
updatedAt: number;
|
||||
optedIn: boolean;
|
||||
};
|
||||
};
|
||||
gender: NintendoAccountGender;
|
||||
/** requires scope user.email */
|
||||
email: NintendoAccountScope.USER_EMAIL extends S ? string : undefined;
|
||||
emailVerified: boolean;
|
||||
phoneNumberEnabled: boolean;
|
||||
/** requires scope user.links[].id/user.links.*.id */
|
||||
links:
|
||||
NintendoAccountScope.USER_LINKS extends S ?
|
||||
Partial<Record<NintendoAccountLinkType, NintendoAccountLink | null>> | null :
|
||||
Partial<Record<NintendoAccountLinkTypes<S>, NintendoAccountLink | null>> | null | undefined;
|
||||
country: string;
|
||||
region: null;
|
||||
language: string;
|
||||
timezone: NintendoAccountTimezone;
|
||||
/** requires scope user.mii */
|
||||
mii: NintendoAccountScope.USER_MII extends S ? Mii | null : undefined;
|
||||
/** requires scope user.mii */
|
||||
candidateMiis: NintendoAccountScope.USER_MII extends S ? unknown[] : undefined;
|
||||
emailOptedIn: boolean;
|
||||
emailOptedInUpdatedAt: number;
|
||||
eachEmailOptedIn: Record<NintendoAccountEmailType, NintendoAccountEmailOptedIn>;
|
||||
clientFriendsOptedIn: boolean;
|
||||
clientFriendsOptedInUpdatedAt: number;
|
||||
analyticsOptedIn: boolean;
|
||||
analyticsOptedInUpdatedAt: number;
|
||||
analyticsPermissions: Record<NintendoAccountAnalyticsType, NintendoAccountAnalyticsPermission>;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
candidateMiis: unknown[];
|
||||
}
|
||||
|
||||
enum NintendoAccountGender {
|
||||
UNKNOWN = 'unknown',
|
||||
FEMALE = 'female',
|
||||
MALE = 'male',
|
||||
}
|
||||
|
||||
enum NintendoAccountLinkType {
|
||||
NINTENDO_NETWORK = 'nintendoNetwork',
|
||||
APPLE = 'apple',
|
||||
GOOGLE = 'google',
|
||||
}
|
||||
interface NintendoAccountLink {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
emailVerified: boolean;
|
||||
analyticsPermissions: {
|
||||
internalAnalysis: {
|
||||
updatedAt: number;
|
||||
permitted: boolean;
|
||||
};
|
||||
targetMarketing: {
|
||||
updatedAt: number;
|
||||
permitted: boolean;
|
||||
};
|
||||
};
|
||||
emailOptedInUpdatedAt: number;
|
||||
birthday: string;
|
||||
screenName: string;
|
||||
gender: string;
|
||||
analyticsOptedInUpdatedAt: number;
|
||||
analyticsOptedIn: boolean;
|
||||
clientFriendsOptedInUpdatedAt: number;
|
||||
}
|
||||
|
||||
type NintendoAccountLinkScope = {
|
||||
[NintendoAccountScope.USER_LINKS_NNID]: NintendoAccountLinkType.NINTENDO_NETWORK;
|
||||
};
|
||||
type NintendoAccountLinkTypes<S extends NintendoAccountScope = NintendoAccountScope.USER_LINKS> =
|
||||
S extends keyof NintendoAccountLinkScope ? NintendoAccountLinkScope[S] : never;
|
||||
|
||||
interface NintendoAccountTimezone {
|
||||
id: string;
|
||||
name: string;
|
||||
utcOffset: string;
|
||||
utcOffsetSeconds: number;
|
||||
}
|
||||
|
||||
enum NintendoAccountEmailType {
|
||||
SURVEY = 'survey',
|
||||
DEALS = 'deals',
|
||||
}
|
||||
interface NintendoAccountEmailOptedIn {
|
||||
optedIn: boolean;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
enum NintendoAccountAnalyticsType {
|
||||
INTERNAL_ANALYSIS = 'internalAnalysis',
|
||||
TARGET_MARKETING = 'targetMarketing',
|
||||
}
|
||||
interface NintendoAccountAnalyticsPermission {
|
||||
updatedAt: number;
|
||||
permitted: boolean;
|
||||
}
|
||||
|
||||
export interface Mii {
|
||||
favoriteColor: string;
|
||||
id: string;
|
||||
updatedAt: number;
|
||||
clientId: '1cfe3a55ed8924d9';
|
||||
type: 'profile';
|
||||
favoriteColor: MiiColour;
|
||||
coreData: {
|
||||
'4': string;
|
||||
};
|
||||
clientId: '1cfe3a55ed8924d9';
|
||||
imageUriTemplate: string;
|
||||
storeData: {
|
||||
'3': string;
|
||||
};
|
||||
imageUriTemplate: string;
|
||||
imageOrigin: string;
|
||||
etag: string;
|
||||
type: 'profile';
|
||||
updatedAt: number;
|
||||
}
|
||||
export enum MiiColour {
|
||||
RED = 'red',
|
||||
ORANGE = 'orange',
|
||||
YELLOW = 'yellow',
|
||||
YELLOWGREEN = 'yellowgreen',
|
||||
GREEN = 'green',
|
||||
BLUE = 'blue',
|
||||
SKYBLUE = 'skyblue',
|
||||
PINK = 'pink',
|
||||
PURPLE = 'purple',
|
||||
BROWN = 'brown',
|
||||
WHITE = 'white',
|
||||
BLACK = 'black',
|
||||
}
|
||||
|
||||
export interface NintendoAccountAuthError {
|
||||
|
|
|
|||
|
|
@ -55,10 +55,10 @@ export class ErrorResponse<T = unknown> extends Error {
|
|||
});
|
||||
}
|
||||
|
||||
static async fromResponse(response: UndiciResponse, message: string) {
|
||||
static async fromResponse<T>(response: UndiciResponse, message: string) {
|
||||
const body = await response.arrayBuffer();
|
||||
|
||||
return new this(message, response, body);
|
||||
return new this<T>(message, response, body);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { fetch, Response } from 'undici';
|
||||
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js';
|
||||
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl, WebService_4, Media, Announcements_4, Friend_4 } from './coral-types.js';
|
||||
import { defineResponse, ErrorResponse, ResponseSymbol } from './util.js';
|
||||
import { CoralApiInterface, CoralAuthData, CorrelationIdSymbol, PartialCoralAuthData, ResponseDataSymbol, Result } from './coral.js';
|
||||
import { AbstractCoralApi, CoralApiInterface, CoralAuthData, CorrelationIdSymbol, PartialCoralAuthData, RequestFlagAddPlatformSymbol, RequestFlagAddProductVersionSymbol, RequestFlagNoParameterSymbol, RequestFlagRequestIdSymbol, RequestFlags, 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,18 +10,25 @@ import { getAdditionalUserAgents, getUserAgent } from '../util/useragent.js';
|
|||
|
||||
const debug = createDebug('nxapi:api:znc-proxy');
|
||||
|
||||
export default class ZncProxyApi implements CoralApiInterface {
|
||||
export default class ZncProxyApi extends AbstractCoralApi implements CoralApiInterface {
|
||||
constructor(
|
||||
private url: string,
|
||||
private url: URL | string,
|
||||
// ZncApi uses the NSO token (valid for a few hours)
|
||||
// ZncProxyApi uses the Nintendo Account session token (valid for two years)
|
||||
public token: string,
|
||||
public useragent = getAdditionalUserAgents()
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async fetchProxyApi<T = unknown>(url: URL | string, method = 'GET', body?: string, headers?: object) {
|
||||
if (typeof url === 'string' && url.startsWith('/')) url = url.substring(1);
|
||||
|
||||
const base_url = typeof this.url === 'string' ? new URL(this.url) : this.url;
|
||||
if (typeof this.url === 'string' && !base_url.pathname.endsWith('/')) base_url.pathname += '/';
|
||||
|
||||
async fetch<T = unknown>(url: string, method = 'GET', body?: string, headers?: object) {
|
||||
const [signal, cancel] = timeoutSignal();
|
||||
const response = await fetch(this.url + url, {
|
||||
const response = await fetch(new URL(url, this.url), {
|
||||
method,
|
||||
headers: Object.assign({
|
||||
'Authorization': 'na ' + this.token,
|
||||
|
|
@ -31,7 +38,8 @@ export default class ZncProxyApi implements CoralApiInterface {
|
|||
signal,
|
||||
}).finally(cancel);
|
||||
|
||||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
const debug_url = typeof url === 'string' ? '/' + url : url.toString();
|
||||
debug('fetch %s %s, response %s', method, debug_url, response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ZncProxyErrorResponse.fromResponse(response, '[zncproxy] Non-2xx status code');
|
||||
|
|
@ -42,70 +50,97 @@ export default class ZncProxyApi implements CoralApiInterface {
|
|||
return defineResponse(data, response);
|
||||
}
|
||||
|
||||
async call<T = unknown>(url: string, parameter = {}): Promise<Result<T>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
async call<T extends {}, R extends {}>(url: string, parameter: R & Partial<RequestFlags> = {} as R): Promise<Result<T>> {
|
||||
const options: [string, unknown][] = [];
|
||||
|
||||
if (parameter[RequestFlagAddPlatformSymbol]) options.push(['add_platform', true]);
|
||||
if (parameter[RequestFlagAddProductVersionSymbol]) options.push(['add_version', true]);
|
||||
if (parameter[RequestFlagNoParameterSymbol]) options.push(['no_parameter', true]);
|
||||
if (RequestFlagRequestIdSymbol in parameter) options.push(['request_id', parameter[RequestFlagRequestIdSymbol]]);
|
||||
|
||||
const result = await this.fetchProxyApi<{result: T}>('call', 'POST', JSON.stringify({
|
||||
url,
|
||||
parameter,
|
||||
|
||||
options: options.length ? Object.fromEntries(options) : undefined,
|
||||
}));
|
||||
|
||||
return createResult(result, result.result);
|
||||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
const result = await this.fetch<{announcements: Announcements}>('/announcements');
|
||||
const result = await this.fetchProxyApi<{announcements: Announcements_4}>('announcements');
|
||||
return createResult(result, result.announcements);
|
||||
}
|
||||
|
||||
async getFriendList() {
|
||||
const result = await this.fetch<{friends: Friend[]}>('/friends');
|
||||
return createResult(result, result);
|
||||
const result = await this.fetchProxyApi<{friends: Friend_4[]; extract_ids?: string[]}>('friends');
|
||||
|
||||
return createResult(result, Object.assign(result, {
|
||||
extractFriendsIds: result.extract_ids ?? result.friends.slice(0, 10).map(f => f.nsaId),
|
||||
}));
|
||||
}
|
||||
|
||||
async addFavouriteFriend(nsa_id: string) {
|
||||
const result = await this.fetch('/friend/' + nsa_id, 'POST', JSON.stringify({
|
||||
const result = await this.fetchProxyApi('friend/' + nsa_id, 'PATCH', JSON.stringify({
|
||||
isFavoriteFriend: true,
|
||||
}));
|
||||
return createResult(result, {});
|
||||
}
|
||||
|
||||
async removeFavouriteFriend(nsa_id: string) {
|
||||
const result = await this.fetch('/friend/' + nsa_id, 'POST', JSON.stringify({
|
||||
const result = await this.fetchProxyApi('friend/' + nsa_id, 'PATCH', JSON.stringify({
|
||||
isFavoriteFriend: false,
|
||||
}));
|
||||
return createResult(result, {});
|
||||
}
|
||||
|
||||
async getWebServices() {
|
||||
const result = await this.fetch<{webservices: WebService[]}>('/webservices');
|
||||
const result = await this.fetchProxyApi<{webservices: WebService_4[]}>('webservices');
|
||||
return createResult(result, result.webservices);
|
||||
}
|
||||
|
||||
async getChats() {
|
||||
const result = await this.fetchProxyApi<{chats: unknown[]}>('chats');
|
||||
return createResult(result, result.chats);
|
||||
}
|
||||
|
||||
async getMedia() {
|
||||
const result = await this.fetchProxyApi<{media: Media[]}>('media');
|
||||
return createResult(result, result);
|
||||
}
|
||||
|
||||
async getActiveEvent() {
|
||||
const result = await this.fetch<{activeevent: ActiveEvent}>('/activeevent');
|
||||
const result = await this.fetchProxyApi<{activeevent: ActiveEvent}>('activeevent');
|
||||
return createResult(result, result.activeevent);
|
||||
}
|
||||
|
||||
async getEvent(id: number) {
|
||||
const result = await this.fetch<{event: Event}>('/event/' + id);
|
||||
const result = await this.fetchProxyApi<{event: Event}>('event/' + id);
|
||||
return createResult(result, result.event);
|
||||
}
|
||||
|
||||
async getUser(id: number) {
|
||||
const result = await this.fetch<{user: User}>('/user/' + id);
|
||||
const result = await this.fetchProxyApi<{user: User}>('user/' + id);
|
||||
return createResult(result, result.user);
|
||||
}
|
||||
|
||||
async getUserByFriendCode(friend_code: string, hash?: string) {
|
||||
const result = await this.fetch<{user: FriendCodeUser}>('/friendcode/' + friend_code);
|
||||
const result = await this.fetchProxyApi<{user: FriendCodeUser}>('friendcode/' + friend_code);
|
||||
return createResult(result, result.user);
|
||||
}
|
||||
|
||||
async sendFriendRequest(nsa_id: string): Promise<Result<{}>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
// async sendFriendRequest(nsa_id: string): Promise<Result<{}>> {
|
||||
// throw new Error('Not supported in ZncProxyApi');
|
||||
// }
|
||||
|
||||
async getCurrentUser() {
|
||||
const result = await this.fetch<{user: CurrentUser}>('/user');
|
||||
const result = await this.fetchProxyApi<{user: CurrentUser}>('user');
|
||||
return createResult(result, result.user);
|
||||
}
|
||||
|
||||
async getFriendCodeUrl() {
|
||||
const result = await this.fetch<{friendcode: FriendCodeUrl}>('/friendcode');
|
||||
const result = await this.fetchProxyApi<{friendcode: FriendCodeUrl}>('friendcode');
|
||||
return createResult(result, result.friendcode);
|
||||
}
|
||||
|
||||
|
|
@ -118,14 +153,14 @@ export default class ZncProxyApi implements CoralApiInterface {
|
|||
});
|
||||
}
|
||||
|
||||
async updateCurrentUserPermissions(
|
||||
to: PresencePermissions, from: PresencePermissions, etag: string
|
||||
): Promise<Result<{}>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
// async updateCurrentUserPermissions(
|
||||
// to: PresencePermissions, from: PresencePermissions, etag: string
|
||||
// ): Promise<Result<{}>> {
|
||||
// throw new Error('Not supported in ZncProxyApi');
|
||||
// }
|
||||
|
||||
async getWebServiceToken(id: number) {
|
||||
const result = await this.fetch<{token: WebServiceToken}>('/webservice/' + id + '/token');
|
||||
const result = await this.fetchProxyApi<{token: WebServiceToken}>('webservice/' + id + '/token');
|
||||
return createResult(result, result.token);
|
||||
}
|
||||
|
||||
|
|
@ -140,8 +175,8 @@ export default class ZncProxyApi implements CoralApiInterface {
|
|||
}
|
||||
|
||||
async renewToken() {
|
||||
const data = await this.fetch<SavedToken>('/auth');
|
||||
data.proxy_url = this.url;
|
||||
const data = await this.fetchProxyApi<SavedToken>('auth');
|
||||
data.proxy_url = this.url.toString();
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -157,7 +192,7 @@ export default class ZncProxyApi implements CoralApiInterface {
|
|||
|
||||
static async createWithSessionToken(url: string, token: string) {
|
||||
const nso = new this(url, token);
|
||||
const data = await nso.fetch<SavedToken>('/auth');
|
||||
const data = await nso.fetchProxyApi<SavedToken>('auth');
|
||||
data.proxy_url = url;
|
||||
|
||||
return {nso, data};
|
||||
|
|
@ -190,6 +225,8 @@ export interface AuthToken {
|
|||
created_at: number;
|
||||
}
|
||||
export interface AuthPolicy {
|
||||
api?: boolean;
|
||||
|
||||
announcements?: boolean;
|
||||
list_friends?: boolean;
|
||||
list_friends_presence?: boolean;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { ColorSchemeName, LayoutChangeEvent, Platform, StyleProp, StyleSheet, us
|
|||
import { i18n, TFunction } from 'i18next';
|
||||
import { I18nextProvider, initReactI18next } from 'react-i18next';
|
||||
import type { User as DiscordUser } from 'discord-rpc';
|
||||
import { NintendoAccountUserCoral } from '../../api/coral.js';
|
||||
import { NintendoAccountUserMoon } from '../../api/moon.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
import { DiscordPresence } from '../../discord/types.js';
|
||||
import ipc, { events } from './ipc.js';
|
||||
import { NintendoAccountUser } from '../../api/na.js';
|
||||
import { SavedToken } from '../../common/auth/coral.js';
|
||||
import { SavedMoonToken } from '../../common/auth/moon.js';
|
||||
import { DiscordPresence } from '../../discord/types.js';
|
||||
import ipc, { events } from './ipc.js';
|
||||
import { BACKGROUND_COLOUR_MAIN_DARK, BACKGROUND_COLOUR_MAIN_LIGHT, DEFAULT_ACCENT_COLOUR } from './constants.js';
|
||||
import createI18n from '../i18n/index.js';
|
||||
|
||||
|
|
@ -243,7 +244,7 @@ export function useColourScheme() {
|
|||
}
|
||||
|
||||
export interface User {
|
||||
user: NintendoAccountUser;
|
||||
user: NintendoAccountUserCoral | NintendoAccountUserMoon;
|
||||
nso: SavedToken | null;
|
||||
nsotoken: string | undefined;
|
||||
moon: SavedMoonToken | null;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import * as persist from 'node-persist';
|
|||
import express, { Request, RequestHandler, Response } from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
import type { Arguments as ParentArguments } from './index.js';
|
||||
import CoralApi, { CoralApiInterface, CoralErrorResponse } from '../../api/coral.js';
|
||||
import { Announcement, CoralStatus, CurrentUser, Friend, FriendCodeUrl, FriendCodeUser, Presence } from '../../api/coral-types.js';
|
||||
import CoralApi, { CoralApiInterface, CoralErrorResponse, RequestFlagAddPlatformSymbol, RequestFlagAddProductVersionSymbol, RequestFlagNoParameterSymbol, RequestFlagRequestId, RequestFlagRequestIdSymbol, RequestFlags } from '../../api/coral.js';
|
||||
import { Announcement, Announcement_4, CoralStatus, CurrentUser, Friend, FriendCodeUrl, FriendCodeUser, Presence } from '../../api/coral-types.js';
|
||||
import ZncProxyApi, { AuthPolicy, AuthToken, ZncPresenceEventStreamEvent } from '../../api/znc-proxy.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
|
|
@ -134,6 +134,9 @@ class Server extends HttpServer {
|
|||
app.post('/api/znc/tokens', bodyParser.json(),
|
||||
this.createProxyRequestHandler(r => this.handleCreateTokenRequest(r), true));
|
||||
|
||||
app.post('/api/znc/call', this.authTokenMiddleware, bodyParser.json(),
|
||||
this.createProxyRequestHandler(r => this.handleApiCallRequest(r)));
|
||||
|
||||
app.get('/api/znc/announcements', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createProxyRequestHandler(r => this.handleAnnouncementsRequest(r), true));
|
||||
|
||||
|
|
@ -155,6 +158,8 @@ class Server extends HttpServer {
|
|||
this.createProxyRequestHandler(r => this.handleFriendRequest(r, r.req.params.nsaid)));
|
||||
app.post('/api/znc/friend/:nsaid', bodyParser.json(),
|
||||
this.createProxyRequestHandler(r => this.handleUpdateFriendRequest(r, r.req.params.nsaid), true));
|
||||
app.patch('/api/znc/friend/:nsaid', bodyParser.json(),
|
||||
this.createProxyRequestHandler(r => this.handleUpdateFriendRequest(r, r.req.params.nsaid), true));
|
||||
app.get('/api/znc/friend/:nsaid/presence', this.authTokenMiddleware, this.localAuthMiddleware,
|
||||
this.createProxyRequestHandler(r => this.handleFriendPresenceRequest(r, r.req.params.nsaid)));
|
||||
|
||||
|
|
@ -321,7 +326,7 @@ class Server extends HttpServer {
|
|||
|
||||
async handleAuthRequest({user}: RequestDataWithUser) {
|
||||
if (user.nso instanceof ZncProxyApi) {
|
||||
return user.nso.fetch('/auth');
|
||||
return user.nso.fetchProxyApi('/auth');
|
||||
} else {
|
||||
return user.data;
|
||||
}
|
||||
|
|
@ -386,6 +391,56 @@ class Server extends HttpServer {
|
|||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Coral API call
|
||||
//
|
||||
|
||||
async handleApiCallRequest({req, policy}: RequestData) {
|
||||
if (policy && !policy.api) {
|
||||
throw new ResponseError(403, 'token_unauthorised');
|
||||
}
|
||||
|
||||
const flags: Partial<RequestFlags> = {};
|
||||
|
||||
if (req.body.options?.add_platform) flags[RequestFlagAddPlatformSymbol] = true;
|
||||
if (req.body.options?.add_version) flags[RequestFlagAddProductVersionSymbol] = true;
|
||||
if (req.body.options?.no_parameter) flags[RequestFlagNoParameterSymbol] = true;
|
||||
|
||||
if (req.body.options && 'request_id' in req.body.options) {
|
||||
if (typeof req.body.options.request_id !== 'number' || !RequestFlagRequestId[req.body.options.request_id]) {
|
||||
throw new ResponseError(400, 'invalid_request', 'Invalid options.request_id');
|
||||
}
|
||||
|
||||
flags[RequestFlagRequestIdSymbol] = req.body.options.request_id;
|
||||
}
|
||||
|
||||
if (typeof req.body.url !== 'string') {
|
||||
throw new ResponseError(400, 'invalid_request', 'Invalid url field');
|
||||
}
|
||||
|
||||
if (!('parameter' in req.body) && flags[RequestFlagNoParameterSymbol]) {
|
||||
// parameter is excluded for /v3/User/Permissions/ShowSelf
|
||||
// parameter is excluded for /v3/Friend/CreateFriendCodeUrl
|
||||
// allow just not providing it
|
||||
} else if (typeof req.body.parameter !== 'object' || !req.body.parameter) {
|
||||
// parameter is an array for /v5/PushNotification/Settings/Update
|
||||
throw new ResponseError(400, 'invalid_request', 'Invalid parameter field');
|
||||
}
|
||||
|
||||
const user = await this.getCoralUser(req);
|
||||
|
||||
if (!(user.nso instanceof CoralApi) && !(user.nso instanceof ZncProxyApi)) {
|
||||
throw new ResponseError(500, 'unknown_error');
|
||||
}
|
||||
|
||||
const result = await user.nso.call(req.body.url, {
|
||||
...req.body.parameter ?? null,
|
||||
...flags,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//
|
||||
// Announcements
|
||||
// This is cached for all users.
|
||||
|
|
@ -398,7 +453,7 @@ class Server extends HttpServer {
|
|||
|
||||
const user = await this.getCoralUser(req);
|
||||
|
||||
const announcements: Announcement[] = user.announcements.result;
|
||||
const announcements: Announcement_4[] = user.announcements.result;
|
||||
return {announcements};
|
||||
}
|
||||
|
||||
|
|
@ -451,6 +506,7 @@ class Server extends HttpServer {
|
|||
const user = await this.getCoralUser(req);
|
||||
|
||||
const friends = await user.getFriends();
|
||||
const extract_ids = user.friends.result.extractFriendsIds;
|
||||
const updated = user.updated.friends;
|
||||
|
||||
res.setHeader('Cache-Control', 'private, immutable, max-age=' + cacheMaxAge(updated, this.update_interval));
|
||||
|
|
@ -458,6 +514,8 @@ class Server extends HttpServer {
|
|||
return {
|
||||
friends: policy?.friends ?
|
||||
friends.filter(f => policy.friends!.includes(f.nsaId)) : friends,
|
||||
extract_ids: policy?.friends ?
|
||||
extract_ids.filter(id => policy.friends!.includes(id)) : extract_ids,
|
||||
updated,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
await storage.getItem('NintendoAccountToken.' + usernsid);
|
||||
const {nso, data} = await getToken(storage, token, argv.zncProxyUrl);
|
||||
|
||||
const tokens = await nso.fetch<AuthTokens>('/tokens');
|
||||
const tokens = await nso.fetchProxyApi<AuthTokens>('/tokens');
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
|
|
@ -138,7 +138,7 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
friends_presence: argv.policyFriendsPresence as string[] | undefined,
|
||||
} : null;
|
||||
|
||||
const auth = await nso.fetch<{token: string;} & AuthToken>('/tokens', 'POST', JSON.stringify({policy}), {
|
||||
const auth = await nso.fetchProxyApi<{token: string;} & AuthToken>('/tokens', 'POST', JSON.stringify({policy}), {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -85,13 +85,13 @@ export class ZncNotifications extends Loop {
|
|||
imageUri: '',
|
||||
channel: FriendRouteChannel.FRIEND_CODE,
|
||||
},
|
||||
presence: await nso.fetch<Presence>('/friend/' + r.friend + '/presence'),
|
||||
presence: await nso.fetchProxyApi<Presence>('/friend/' + r.friend + '/presence'),
|
||||
};
|
||||
|
||||
return friend;
|
||||
}
|
||||
|
||||
return (await nso.fetch<{friend: Friend}>('/friend/' + r.friend)).friend;
|
||||
return (await nso.fetchProxyApi<{friend: Friend}>('/friend/' + r.friend)).friend;
|
||||
}));
|
||||
}
|
||||
if (req.includes('webservices')) {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ import * as crypto from 'node:crypto';
|
|||
import * as persist from 'node-persist';
|
||||
import { Response } from 'undici';
|
||||
import createDebug from '../util/debug.js';
|
||||
import CoralApi, { CoralApiInterface, Result } from '../api/coral.js';
|
||||
import CoralApi, { CoralApiInterface, NintendoAccountUserCoral, Result } from '../api/coral.js';
|
||||
import ZncProxyApi from '../api/znc-proxy.js';
|
||||
import { Announcements, Friends, Friend, GetActiveEventResult, CoralSuccessResponse, WebService, WebServices, CoralError } from '../api/coral-types.js';
|
||||
import { Announcements, Friends, Friend, GetActiveEventResult, CoralSuccessResponse, WebService, WebServices, CoralError, Announcements_4, Friends_4, WebServices_4, ListMedia, ListChat, WebService_4, CurrentUser } 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');
|
||||
|
||||
|
|
@ -64,14 +63,20 @@ export default class Users<T extends UserData> {
|
|||
return new Users(async token => {
|
||||
const {nso, data} = await getToken(storage, token, znc_proxy_url, ratelimit);
|
||||
|
||||
const [announcements, friends, webservices, active_event] = await Promise.all([
|
||||
const [announcements, friends, webservices, chats, media, active_event, coral_user] = await Promise.all([
|
||||
nso.getAnnouncements(),
|
||||
nso.getFriendList(),
|
||||
nso.getWebServices(),
|
||||
nso.getChats(),
|
||||
nso.getMedia(),
|
||||
nso.getActiveEvent(),
|
||||
nso.getCurrentUser(),
|
||||
]);
|
||||
|
||||
const user = new CoralUser(nso, data, announcements, friends, webservices, active_event);
|
||||
const user = new CoralUser(
|
||||
nso, data,
|
||||
announcements, friends, webservices, chats, media, active_event, coral_user,
|
||||
);
|
||||
|
||||
if (nso instanceof CoralApi && nso.onTokenExpired) {
|
||||
const renewToken = nso.onTokenExpired;
|
||||
|
|
@ -98,10 +103,13 @@ export default class Users<T extends UserData> {
|
|||
export interface CoralUserData<T extends CoralApiInterface = CoralApi> extends UserData {
|
||||
nso: T;
|
||||
data: SavedToken;
|
||||
announcements: CoralSuccessResponse<Announcements>;
|
||||
friends: CoralSuccessResponse<Friends>;
|
||||
webservices: CoralSuccessResponse<WebServices>;
|
||||
announcements: CoralSuccessResponse<Announcements_4>;
|
||||
friends: CoralSuccessResponse<Friends_4>;
|
||||
webservices: CoralSuccessResponse<WebServices_4>;
|
||||
chats: CoralSuccessResponse<ListChat>;
|
||||
media: CoralSuccessResponse<ListMedia>;
|
||||
active_event: CoralSuccessResponse<GetActiveEventResult>;
|
||||
user: CoralSuccessResponse<CurrentUser>;
|
||||
}
|
||||
|
||||
export class CoralUser<T extends CoralApiInterface = CoralApi> implements CoralUserData<T> {
|
||||
|
|
@ -115,22 +123,27 @@ export class CoralUser<T extends CoralApiInterface = CoralApi> implements CoralU
|
|||
announcements: Date.now(),
|
||||
friends: Date.now(),
|
||||
webservices: Date.now(),
|
||||
chats: Date.now(),
|
||||
media: Date.now(),
|
||||
active_event: Date.now(),
|
||||
user: Date.now(),
|
||||
};
|
||||
|
||||
delay_retry_after_error = 5 * 1000; // 5 seconds
|
||||
update_interval = 10 * 1000; // 10 seconds
|
||||
update_interval_announcements = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
onUpdatedWebServices: ((webservices: Result<WebServices>) => void) | null = null;
|
||||
onUpdatedWebServices: ((webservices: Result<WebServices_4>) => void) | null = null;
|
||||
|
||||
constructor(
|
||||
public nso: T,
|
||||
public data: SavedToken,
|
||||
public announcements: CoralSuccessResponse<Announcements>,
|
||||
public friends: CoralSuccessResponse<Friends>,
|
||||
public webservices: CoralSuccessResponse<WebServices>,
|
||||
public announcements: CoralSuccessResponse<Announcements_4>,
|
||||
public friends: CoralSuccessResponse<Friends_4>,
|
||||
public webservices: CoralSuccessResponse<WebServices_4>,
|
||||
public chats: CoralSuccessResponse<ListChat>,
|
||||
public media: CoralSuccessResponse<ListMedia>,
|
||||
public active_event: CoralSuccessResponse<GetActiveEventResult>,
|
||||
public user: CoralSuccessResponse<CurrentUser>,
|
||||
) {}
|
||||
|
||||
private async update(key: keyof CoralUser['updated'], callback: () => Promise<void>, ttl: number) {
|
||||
|
|
@ -159,14 +172,19 @@ export class CoralUser<T extends CoralApiInterface = CoralApi> implements CoralU
|
|||
|
||||
async getAnnouncements() {
|
||||
await this.update('announcements', async () => {
|
||||
// Always requested together when refreshing notifications page
|
||||
this.getWebServices();
|
||||
|
||||
this.announcements = await this.nso.getAnnouncements();
|
||||
}, this.update_interval_announcements);
|
||||
}, this.update_interval);
|
||||
|
||||
return this.announcements.result;
|
||||
}
|
||||
|
||||
async getFriends() {
|
||||
await this.update('friends', async () => {
|
||||
// No simultaneous requests when refreshing friend list page
|
||||
|
||||
this.friends = await this.nso.getFriendList();
|
||||
}, this.update_interval);
|
||||
|
||||
|
|
@ -175,6 +193,9 @@ export class CoralUser<T extends CoralApiInterface = CoralApi> implements CoralU
|
|||
|
||||
async getWebServices() {
|
||||
await this.update('webservices', async () => {
|
||||
// Always requested together when refreshing notifications page
|
||||
this.getAnnouncements();
|
||||
|
||||
const webservices = this.webservices = await this.nso.getWebServices();
|
||||
|
||||
this.onUpdatedWebServices?.call(null, webservices);
|
||||
|
|
@ -183,14 +204,69 @@ export class CoralUser<T extends CoralApiInterface = CoralApi> implements CoralU
|
|||
return this.webservices.result;
|
||||
}
|
||||
|
||||
async getChats() {
|
||||
await this.update('chats', async () => {
|
||||
// Always requested together when refreshing main page
|
||||
Promise.all([
|
||||
this.getAnnouncements(),
|
||||
this.getFriends(),
|
||||
this.getWebServices(),
|
||||
this.getMedia(),
|
||||
this.getActiveEvent(),
|
||||
]);
|
||||
|
||||
this.chats = await this.nso.getChats();
|
||||
}, this.update_interval);
|
||||
|
||||
return this.chats.result;
|
||||
}
|
||||
|
||||
async getMedia() {
|
||||
await this.update('media', async () => {
|
||||
// No simultaneous requests when refreshing media page
|
||||
|
||||
this.media = await this.nso.getMedia();
|
||||
}, this.update_interval);
|
||||
|
||||
return this.media.result.media;
|
||||
}
|
||||
|
||||
async getActiveEvent() {
|
||||
await this.update('active_event', async () => {
|
||||
// Always requested together when refreshing main page
|
||||
Promise.all([
|
||||
this.getAnnouncements(),
|
||||
this.getFriends(),
|
||||
this.getWebServices(),
|
||||
this.getChats(),
|
||||
this.getMedia(),
|
||||
]);
|
||||
|
||||
this.active_event = await this.nso.getActiveEvent();
|
||||
}, this.update_interval);
|
||||
|
||||
return this.active_event.result;
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
await this.update('user', async () => {
|
||||
// Always requested together when refreshing main page
|
||||
Promise.all([
|
||||
this.getAnnouncements(),
|
||||
this.getFriends(),
|
||||
this.getWebServices(),
|
||||
this.getChats(),
|
||||
this.getMedia(),
|
||||
]);
|
||||
|
||||
// or, then user page requests /v4/User/ShowSelf, /v3/User/Permissions/ShowSelf, /v4/User/PlayLog/Show
|
||||
|
||||
this.user = await this.nso.getCurrentUser();
|
||||
}, this.update_interval);
|
||||
|
||||
return this.user.result;
|
||||
}
|
||||
|
||||
async addFriend(nsa_id: string) {
|
||||
if (!(this.nso instanceof CoralApi)) {
|
||||
throw new Error('Cannot send friend requests using Coral API proxy');
|
||||
|
|
@ -233,7 +309,7 @@ export interface CachedWebServicesList {
|
|||
|
||||
async function maybeUpdateWebServicesListCache(
|
||||
cached_webservices: Map<string, string>, store: Store, // storage: persist.LocalStorage,
|
||||
user: NintendoAccountUser, webservices: WebService[]
|
||||
user: NintendoAccountUserCoral, webservices: WebService_4[]
|
||||
) {
|
||||
const webservices_hash = crypto.createHash('sha256').update(JSON.stringify(webservices)).digest('hex');
|
||||
if (cached_webservices.get(user.language) === webservices_hash) return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user