Use package.json exports field

This commit is contained in:
Samuel Elliott 2022-07-23 17:42:41 +01:00
parent d7a7d32823
commit dddb15aceb
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
15 changed files with 181 additions and 16 deletions

View File

@ -1,7 +1,7 @@
nxapi
===
Access the Nintendo Switch Online and Nintendo Switch Parental Controls app APIs. Includes Discord Rich Presence, friend notifications and data downloads.
JavaScript library and command line and Electron app for accessing the Nintendo Switch Online and Nintendo Switch Parental Controls app APIs. Show your Nintendo Switch presence in Discord, get friend notifications on desktop, and download and access SplatNet 2, NookLink and Parental Controls data.
[![Discord](https://img.shields.io/discord/998657768594608138?color=5865f2&label=Discord)](https://discord.com/invite/4D82rFkXRv)
@ -39,7 +39,7 @@ Access the Nintendo Switch Online and Nintendo Switch Parental Controls app APIs
- Download island newspapers from and send messages and reactions using NookLink
- Download all Nintendo Switch Parental Controls usage records
The API library and types are exported for use in JavaScript/TypeScript software. The app/commands properly cache access tokens and try to handle requests to appear as Nintendo's apps - if using nxapi as a library you will need to handle this yourself.
The API library and types are exported for use in JavaScript/TypeScript software. The app/commands properly cache access tokens and try to handle requests to appear as Nintendo's apps - if using nxapi as a library you will need to handle this yourself. [More information.](#usage-as-a-typescriptjavascript-library)
#### Electron app
@ -744,6 +744,67 @@ const pkg = JSON.parse(await readFile(resolve(fileURLToPath(import.meta.url), '.
addUserAgent(pkg.name + '/' + pkg.version + ' (+' + pkg.repository.url + ')');
```
#### Usage as a TypeScript/JavaScript library
nxapi exports it's API library and types. [See src/exports.](src/exports)
> You must set a user agent string using the `addUserAgent` function when using anything that contacts non-Nintendo APIs, such as the splatnet2statink API.
> Please read https://github.com/frozenpandaman/splatnet2statink/wiki/api-docs if you intend to share anything you create.
> nxapi uses native ECMAScript modules. nxapi also uses features like top-level await, so it cannot be converted to CommonJS using Rollup or similar. If you need to use nxapi from CommonJS modules or other module systems, use a [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import).
> If you need any help using nxapi as a library [join the Discord server](https://discord.com/invite/4D82rFkXRv) or [create a discussion](https://github.com/samuelthomas2774/nxapi/discussions/new).
Example authenticating to the Nintendo Switch Online app:
> This is a simplified example of authenticating to the Coral API and using cached tokens. More logic is required to ensure you are using these APIs properly - [see src/common/auth/nso.ts for the authentication functions used in nxapi's CLI and Electron app](src/common/auth/nso.ts).
```ts
import { addUserAgent } from 'nxapi';
import CoralApi from 'nxapi/coral';
addUserAgent('your-script/1.0.0 (+https://github.com/...)');
declare function getCachedCoralToken(): [string, Date];
declare function setCachedCoralToken(token: string, expires_at: Date): void;
declare function getNintendoAccountSessionToken(): string;
let coral;
try {
const [token, expires_at] = getCachedCoralToken();
if (expires_at.getTime() > Date.now()) throw new Error('Token expired');
coral = new CoralApi(token);
} catch (err) {
const na_session_token = getNintendoAccountSessionToken();
const {nso, data} = await CoralApi.createWithSessionToken(na_session_token);
setCachedCoralToken(data.credential.accessToken, Date.now() + (data.credential.expiresIn * 1000));
coral = nso;
}
const friends = await coral.getFriendList();
```
Example getting SplatNet 2 records:
> This example does not include authenticating to SplatNet 2. To benefit from the caching in the nxapi command, the `nxapi splatnet2 token --json` command can be used in most scripts. For example:
>
> ```sh
> # your-script.js can then read the iksm_session, unique player ID and region from `JSON.parse(process.env.SPLATNET_TOKEN)`
> SPLATNET_TOKEN=`nxapi splatnet2 token --json` node your-script.js
> ```
```ts
import SplatNet2Api from 'nxapi/splatnet2';
const iksm_session = '...';
const splatnet2 = new SplatNet2Api(iksm_session);
const records = await splatnet2.getRecords();
```
### Links
- Nintendo Switch Online app API docs

View File

@ -17,6 +17,13 @@
"resources/common",
"bin"
],
"exports": {
".": "./dist/exports/index.js",
"./coral": "./dist/exports/coral.js",
"./moon": "./dist/exports/moon.js",
"./splatnet2": "./dist/exports/splatnet2.js",
"./nooklink": "./dist/exports/nooklink.js"
},
"bin": {
"nxapi": "bin/nxapi.js"
},

View File

@ -10,7 +10,7 @@ const debugFlapg = createDebug('nxapi:api:flapg');
const debugImink = createDebug('nxapi:api:imink');
const debugZncaApi = createDebug('nxapi:api:znca-api');
abstract class ZncaApi {
export abstract class ZncaApi {
constructor(
public useragent?: string
) {}
@ -291,7 +291,7 @@ export type FResult = {
result: AndroidZncaFResponse;
});
function getZncaApiFromEnvironment(useragent?: string): ZncaApi {
export function getZncaApiFromEnvironment(useragent?: string): ZncaApi {
if (process.env.NXAPI_ZNCA_API) {
if (process.env.NXAPI_ZNCA_API === 'flapg') {
return new ZncaApiFlapg(useragent);

View File

@ -178,9 +178,14 @@ export type PresenceUrlResponse =
CurrentUser | {user: CurrentUser} |
Friend | {friend: Friend};
export async function getPresenceFromUrl(presence_url: string) {
export async function getPresenceFromUrl(presence_url: string, useragent?: string) {
const [signal, cancel] = timeoutSignal();
const response = await fetch(presence_url, {signal}).finally(cancel);
const response = await fetch(presence_url, {
headers: {
'User-Agent': getUserAgent(useragent),
},
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', 'GET', presence_url, response.status);

View File

@ -6,15 +6,19 @@ import { dir } from '../../util/product.js';
export const bundlepath = path.resolve(dir, 'dist', 'app', 'bundle');
export async function getNativeImageFromUrl(url: URL | string) {
const response = await fetch(url.toString());
export async function getNativeImageFromUrl(url: URL | string, useragent?: string) {
const response = await fetch(url.toString(), {
headers: {
'User-Agent': useragent ?? '',
},
});
const image = await response.arrayBuffer();
return nativeImage.createFromBuffer(Buffer.from(image));
}
export async function tryGetNativeImageFromUrl(url: URL | string) {
export async function tryGetNativeImageFromUrl(url: URL | string, useragent?: string) {
try {
return await getNativeImageFromUrl(url);
return await getNativeImageFromUrl(url, useragent);
} catch (err) {}
return undefined;

View File

@ -273,7 +273,11 @@ export class WebServiceIpc {
debug('Downloading image %s to %s as %s', req.image_url, dir, filename);
const response = await fetch(req.image_url);
const response = await fetch(req.image_url, {
headers: {
'User-Agent': '',
},
});
const image = await response.arrayBuffer();
await fs.writeFile(path.join(dir, filename), Buffer.from(image));

View File

@ -6,6 +6,7 @@ import { getToken } from '../../common/auth/nso.js';
import { AuthPolicy, AuthToken } from '../../api/znc-proxy.js';
import { Argv } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getUserAgent } from '../../util/useragent.js';
const debug = createDebug('cli:nso:znc-proxy-tokens');
@ -157,6 +158,7 @@ export function builder(yargs: Argv<ParentArguments>) {
method: 'DELETE',
headers: {
'Authorization': 'Bearer ' + argv.token,
'User-Agent': getUserAgent(),
},
});
debug('fetch %s %s, response %d', 'DELETE', '/token', response.status);

View File

@ -8,6 +8,8 @@ import { DiscordPresenceContext, DiscordPresencePlayTime, getDiscordPresence, ge
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
import { initStorage } from '../../util/storage.js';
import { getToken } from '../../common/auth/nso.js';
import { timeoutSignal } from '../../util/misc.js';
import { getUserAgent } from '../../util/useragent.js';
const debug = createDebug('cli:util:discord-activity');
@ -234,7 +236,14 @@ export async function getDiscordApplicationRpc(id: string) {
const url = 'https://discord.com/api/v9/applications/' + id + '/rpc';
const response = await fetch(url);
const [signal, cancel] = timeoutSignal();
const response = await fetch(url, {
headers: {
'User-Agent': getUserAgent(),
},
signal,
}).finally(cancel);
debug('fetch %s %s, response %s', 'GET', url, response.status);
if (response.status !== 200) {

View File

@ -108,7 +108,12 @@ export async function dumpProfileImage(
debug('Fetching profile image', share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image_response = await fetch(share.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing profile image %s', image_filename);
@ -142,7 +147,12 @@ export async function dumpChallenges(
debug('Fetching challenge image for %s', challenge.key, share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image_response = await fetch(share.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing challenge image %s', filename);

View File

@ -53,7 +53,12 @@ export async function dumpResults(
debug('Fetching battle results summary image', share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image_response = await fetch(share.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing battle results summary image %s', filename);
@ -111,7 +116,12 @@ export async function dumpResults(
debug('Fetching battle results image', share);
const [signal, cancel] = timeoutSignal();
const image_response = await fetch(share.url, {signal}).finally(cancel);
const image_response = await fetch(share.url, {
headers: {
'User-Agent': splatnet.useragent,
},
signal,
}).finally(cancel);
const image = await image_response.arrayBuffer();
debug('Writing battle results image %s', filename);

22
src/exports/coral.ts Normal file
View File

@ -0,0 +1,22 @@
export { default } from '../api/coral.js';
export * from '../api/coral-types.js';
export { default as ZncProxyApi } from '../api/znc-proxy.js';
export {
ZncaApi,
getZncaApiFromEnvironment,
f,
ZncaApiFlapg,
FlapgIid,
FlapgApiResponse,
ZncaApiImink,
IminkFResponse,
IminkFError,
ZncaApiNxapi,
AndroidZncaFResponse,
AndroidZncaFError,
} from '../api/f.js';

3
src/exports/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { getTitleIdFromEcUrl } from '../util/misc.js';
export { ErrorResponse } from '../api/util.js';
export { addUserAgent } from '../util/useragent.js';

2
src/exports/moon.ts Normal file
View File

@ -0,0 +1,2 @@
export { default } from '../api/moon.js';
export * from '../api/moon-types.js';

7
src/exports/nooklink.ts Normal file
View File

@ -0,0 +1,7 @@
export {
default as NooklinkApi,
NooklinkUserApi,
MessageType,
} from '../api/nooklink.js';
export * from '../api/nooklink-types.js';

19
src/exports/splatnet2.ts Normal file
View File

@ -0,0 +1,19 @@
export {
default,
LeagueType,
LeagueRegion,
ShareColour as ShareProfileColour,
toLeagueId,
} from '../api/splatnet2.js';
export {
Season as XRankSeason,
Rule as XPowerRankingRule,
getAllSeasons as getXRankSeasons,
getSeason as getXRankSeason,
getNextSeason as getNextXRankSeason,
getPreviousSeason as getPreviousXRankSeason,
toSeasonId as toXRankSeasonId,
} from '../api/splatnet2-xrank.js';
export * from '../api/splatnet2-types.js';