mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-07-03 16:40:52 -05:00
Update znc types, add other endpoints and use /v3/Account/GetToken to renew the app token
This commit is contained in:
parent
8bd6f306f8
commit
a53eccd91e
|
|
@ -1,9 +1,10 @@
|
|||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { ActiveEvent, Announcement, CurrentUser, Friend, WebService, WebServiceToken } 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';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
|
||||
const debug = createDebug('api:znc-proxy');
|
||||
|
||||
|
|
@ -30,6 +31,8 @@ export default class ZncProxyApi implements ZncApi {
|
|||
|
||||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
if (response.status === 204) return null!;
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new ErrorResponse('[zncproxy] Unknown error', response);
|
||||
}
|
||||
|
|
@ -40,33 +43,80 @@ export default class ZncProxyApi implements ZncApi {
|
|||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
const response = await this.fetch<{announcements: Announcement[]}>('/announcements');
|
||||
return {status: 0 as const, result: response.announcements, correlationId: ''};
|
||||
const response = await this.fetch<{announcements: Announcements}>('/announcements');
|
||||
return {status: ZncStatus.OK as const, result: response.announcements, correlationId: ''};
|
||||
}
|
||||
|
||||
async getFriendList() {
|
||||
const response = await this.fetch<{friends: Friend[]}>('/friends');
|
||||
return {status: 0 as const, result: response, correlationId: ''};
|
||||
return {status: ZncStatus.OK as const, result: response, correlationId: ''};
|
||||
}
|
||||
|
||||
async addFavouriteFriend(nsaid: string) {
|
||||
await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
|
||||
isFavoriteFriend: true,
|
||||
}));
|
||||
return {status: ZncStatus.OK as const, result: {}, correlationId: ''};
|
||||
}
|
||||
|
||||
async removeFavouriteFriend(nsaid: string) {
|
||||
await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
|
||||
isFavoriteFriend: false,
|
||||
}));
|
||||
return {status: ZncStatus.OK as const, result: {}, correlationId: ''};
|
||||
}
|
||||
|
||||
async getWebServices() {
|
||||
const response = await this.fetch<{webservices: WebService[]}>('/webservices');
|
||||
return {status: 0 as const, result: response.webservices, correlationId: ''};
|
||||
return {status: ZncStatus.OK as const, result: response.webservices, correlationId: ''};
|
||||
}
|
||||
|
||||
async getActiveEvent() {
|
||||
const response = await this.fetch<{activeevent: ActiveEvent}>('/activeevent');
|
||||
return {status: 0 as const, result: response.activeevent, correlationId: ''};
|
||||
return {status: ZncStatus.OK as const, result: response.activeevent, correlationId: ''};
|
||||
}
|
||||
|
||||
async getEvent(id: number) {
|
||||
const response = await this.fetch<{event: Event}>('/event/' + id);
|
||||
return {status: ZncStatus.OK as const, result: response.event, correlationId: ''};
|
||||
}
|
||||
|
||||
async getUser(id: number) {
|
||||
const response = await this.fetch<{user: User}>('/user/' + id);
|
||||
return {status: ZncStatus.OK as const, result: response.user, correlationId: ''};
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
const response = await this.fetch<{user: CurrentUser}>('/user');
|
||||
return {status: 0 as const, result: response.user, correlationId: ''};
|
||||
return {status: ZncStatus.OK as const, result: response.user, correlationId: ''};
|
||||
}
|
||||
|
||||
async getCurrentUserPermissions() {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
return {
|
||||
status: ZncStatus.OK as const,
|
||||
result: {
|
||||
etag: user.result.etag,
|
||||
permissions: user.result.permissions,
|
||||
},
|
||||
correlationId: '',
|
||||
};
|
||||
}
|
||||
|
||||
async updateCurrentUserPermissions(
|
||||
to: PresencePermissions, from: PresencePermissions, etag: string
|
||||
): Promise<ZncSuccessResponse<{}>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
|
||||
async getWebServiceToken(id: string) {
|
||||
const response = await this.fetch<{token: WebServiceToken}>('/webservice/' + id + '/token');
|
||||
return {status: 0 as const, result: response.token, correlationId: ''};
|
||||
return {status: ZncStatus.OK as const, result: response.token, correlationId: ''};
|
||||
}
|
||||
|
||||
async getToken(token: string, user: NintendoAccountUser): Promise<ZncSuccessResponse<WebServiceToken>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
|
||||
async renewToken() {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
|
||||
export interface ZncSuccessResponse<T = unknown> {
|
||||
status: 0;
|
||||
status: ZncStatus.OK;
|
||||
result: T;
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
export interface ZncErrorResponse {
|
||||
status: number;
|
||||
status: ZncStatus | number;
|
||||
errorMessage: string;
|
||||
correlationId: string;
|
||||
}
|
||||
export enum ZncStatus {
|
||||
OK = 0,
|
||||
|
||||
BAD_REQUEST = 9400,
|
||||
INVALID_TOKEN = 9403,
|
||||
TOKEN_EXPIRED = 9404,
|
||||
UPGRADE_REQUIRED = 9427,
|
||||
}
|
||||
|
||||
export type ZncResponse<T = unknown> = ZncSuccessResponse<T> | ZncErrorResponse;
|
||||
|
||||
/** /v3/Account/Login */
|
||||
export interface AccountLogin {
|
||||
user: CurrentUser;
|
||||
webApiServerCredential: {
|
||||
|
|
@ -25,6 +34,9 @@ export interface AccountLogin {
|
|||
};
|
||||
}
|
||||
|
||||
/** /v1/Announcement/List */
|
||||
export type Announcements = Announcement[];
|
||||
|
||||
export interface Announcement {
|
||||
announcementId: number;
|
||||
priority: number;
|
||||
|
|
@ -34,6 +46,7 @@ export interface Announcement {
|
|||
description: string;
|
||||
}
|
||||
|
||||
/** /v3/Friend/List */
|
||||
export interface Friends {
|
||||
friends: Friend[];
|
||||
}
|
||||
|
|
@ -82,6 +95,9 @@ export interface Game {
|
|||
sysDescription: string;
|
||||
}
|
||||
|
||||
/** /v1/Game/ListWebServices */
|
||||
export type WebServices = WebService[];
|
||||
|
||||
export interface WebService {
|
||||
id: number;
|
||||
uri: string;
|
||||
|
|
@ -96,10 +112,48 @@ export interface WebServiceAttribute {
|
|||
attrKey: string;
|
||||
}
|
||||
|
||||
export interface ActiveEvent {
|
||||
// ??
|
||||
/** /v1/Event/GetActiveEvent */
|
||||
export type ActiveEvent = _ActiveEvent | {};
|
||||
|
||||
export interface _ActiveEvent extends Event {
|
||||
activateId: string;
|
||||
}
|
||||
|
||||
/** /v1/Event/Show */
|
||||
export interface Event {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
shareUri: string;
|
||||
ownerUserId: number;
|
||||
members: EventMember[];
|
||||
passCode: string;
|
||||
eventType: 3; // ??
|
||||
allowJoinGameWithoutCoral: boolean;
|
||||
game: {
|
||||
id: number;
|
||||
};
|
||||
imageUri: string;
|
||||
}
|
||||
|
||||
export interface EventMember {
|
||||
id: number;
|
||||
name: string;
|
||||
imageUri: string;
|
||||
isPlaying: boolean;
|
||||
isInvited: boolean;
|
||||
isJoinedVoip: boolean;
|
||||
}
|
||||
|
||||
/** /v3/User/Show */
|
||||
export interface User {
|
||||
id: number;
|
||||
nsaId: string;
|
||||
imageUri: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** /v3/User/ShowSelf */
|
||||
export interface CurrentUser {
|
||||
id: number;
|
||||
nsaId: string;
|
||||
|
|
@ -127,12 +181,22 @@ export interface CurrentUser {
|
|||
};
|
||||
presence: Presence;
|
||||
}
|
||||
|
||||
export enum PresencePermissions {
|
||||
FRIENDS = 'FRIENDS',
|
||||
FAVORITE_FRIENDS = 'FAVORITE_FRIENDS',
|
||||
SELF = 'SELF',
|
||||
}
|
||||
|
||||
/** /v3/User/Permissions/ShowSelf */
|
||||
export interface CurrentUserPermissions {
|
||||
etag: string;
|
||||
permissions: {
|
||||
presence: PresencePermissions;
|
||||
};
|
||||
}
|
||||
|
||||
/** /v2/Game/GetWebServiceToken */
|
||||
export interface WebServiceToken {
|
||||
accessToken: string;
|
||||
expiresIn: number;
|
||||
|
|
|
|||
100
src/api/znc.ts
100
src/api/znc.ts
|
|
@ -2,8 +2,8 @@ import fetch from 'node-fetch';
|
|||
import { v4 as uuidgen } from 'uuid';
|
||||
import createDebug from 'debug';
|
||||
import { flapg, FlapgIid, genfc } from './f.js';
|
||||
import { AccountLogin, ActiveEvent, Announcement, CurrentUser, Friends, WebService, WebServiceToken, ZncResponse } from './znc-types.js';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser } from './na.js';
|
||||
import { AccountLogin, ActiveEvent, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, PresencePermissions, User, WebServices, WebServiceToken, ZncResponse, ZncStatus } from './znc-types.js';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse, JwtPayload } from './util.js';
|
||||
|
||||
const debug = createDebug('api:znc');
|
||||
|
|
@ -20,7 +20,8 @@ export default class ZncApi {
|
|||
static useragent: string | null = null;
|
||||
|
||||
constructor(
|
||||
public token: string
|
||||
public token: string,
|
||||
public useragent: string | null = ZncApi.useragent
|
||||
) {}
|
||||
|
||||
async fetch<T = unknown>(url: string, method = 'GET', body?: string, headers?: object) {
|
||||
|
|
@ -43,7 +44,7 @@ export default class ZncApi {
|
|||
if ('errorMessage' in data) {
|
||||
throw new ErrorResponse('[znc] ' + data.errorMessage, response, data);
|
||||
}
|
||||
if (data.status !== 0) {
|
||||
if (data.status !== ZncStatus.OK) {
|
||||
throw new ErrorResponse('[znc] Unknown error', response, data);
|
||||
}
|
||||
|
||||
|
|
@ -51,17 +52,33 @@ export default class ZncApi {
|
|||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
return this.fetch<Announcement[]>('/v1/Announcement/List', 'POST', '{"parameter":{}}');
|
||||
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/Create', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
nsaId: nsaid,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async getWebServices() {
|
||||
const uuid = uuidgen();
|
||||
|
||||
return this.fetch<WebService[]>('/v1/Game/ListWebServices', 'POST', JSON.stringify({
|
||||
return this.fetch<WebServices>('/v1/Game/ListWebServices', 'POST', JSON.stringify({
|
||||
requestId: uuid,
|
||||
}));
|
||||
}
|
||||
|
|
@ -70,17 +87,52 @@ export default class ZncApi {
|
|||
return this.fetch<ActiveEvent>('/v1/Event/GetActiveEvent', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async getEvent(id: number) {
|
||||
return this.fetch<Event>('/v1/Event/Show', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
id,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async getUser(id: number) {
|
||||
return this.fetch<User>('/v3/User/Show', 'POST', JSON.stringify({
|
||||
parameter: {
|
||||
id,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
return this.fetch<CurrentUser>('/v3/User/ShowSelf', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
async getCurrentUserPermissions() {
|
||||
return this.fetch<CurrentUserPermissions>('/v3/User/Permissions/ShowSelf', 'POST', '{"parameter":{}}');
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
etag,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async getWebServiceToken(id: string) {
|
||||
const uuid = uuidgen();
|
||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||
|
||||
const useragent = this.useragent ?? undefined;
|
||||
const data = process.env.ZNCA_API_URL ?
|
||||
await genfc(process.env.ZNCA_API_URL + '/f', this.token, timestamp, uuid, FlapgIid.APP) :
|
||||
await flapg(this.token, timestamp, uuid, FlapgIid.APP);
|
||||
await genfc(process.env.ZNCA_API_URL + '/f', this.token, timestamp, uuid, FlapgIid.APP, useragent) :
|
||||
await flapg(this.token, timestamp, uuid, FlapgIid.APP, useragent);
|
||||
|
||||
const req = {
|
||||
id,
|
||||
|
|
@ -95,17 +147,43 @@ export default class ZncApi {
|
|||
}));
|
||||
}
|
||||
|
||||
async getToken(token: string, user: NintendoAccountUser) {
|
||||
const uuid = uuidgen();
|
||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||
|
||||
// Nintendo Account token
|
||||
const nintendoAccountToken = await getNintendoAccountToken(token, ZNCA_CLIENT_ID);
|
||||
|
||||
const id_token = nintendoAccountToken.id_token;
|
||||
const useragent = this.useragent ?? undefined;
|
||||
const data = process.env.ZNCA_API_URL ?
|
||||
await genfc(process.env.ZNCA_API_URL + '/f', id_token, timestamp, uuid, FlapgIid.NSO, useragent) :
|
||||
await flapg(id_token, timestamp, uuid, FlapgIid.NSO, useragent);
|
||||
|
||||
const req = {
|
||||
naBirthday: user.birthday,
|
||||
timestamp,
|
||||
f: data.f,
|
||||
requestId: uuid,
|
||||
naIdToken: this.token,
|
||||
};
|
||||
|
||||
return this.fetch<WebServiceToken>('/v3/Account/GetToken', 'POST', JSON.stringify({
|
||||
parameter: req,
|
||||
}));
|
||||
}
|
||||
|
||||
static async createWithSessionToken(token: string, useragent = ZncApi.useragent) {
|
||||
const data = await this.loginWithSessionToken(token, useragent);
|
||||
|
||||
return {
|
||||
nso: new this(data.credential.accessToken),
|
||||
nso: new this(data.credential.accessToken, useragent),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async renewToken(token: string, useragent = ZncApi.useragent) {
|
||||
const data = await ZncApi.loginWithSessionToken(token, useragent);
|
||||
async renewToken(token: string) {
|
||||
const data = await ZncApi.loginWithSessionToken(token, this.useragent);
|
||||
|
||||
this.token = data.credential.accessToken;
|
||||
|
||||
|
|
|
|||
|
|
@ -343,6 +343,24 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
}));
|
||||
});
|
||||
|
||||
app.get('/api/znc/friends/favourites', authToken, (req, res, next) => {
|
||||
if (!req.zncAuthPolicy) return next();
|
||||
if (!req.zncAuthPolicy.list_friends) return tokenUnauthorised(req, res);
|
||||
next();
|
||||
}, localAuth, nsoAuth, getFriendsData, async (req, res) => {
|
||||
const [friends, updated] = cached_friendsdata.get(req.zncAuth!.user.id)!;
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
friends: friends.filter(f => {
|
||||
if (req.zncAuthPolicy?.friends && !req.zncAuthPolicy.friends.includes(f.nsaId)) return false;
|
||||
|
||||
return f.isFavoriteFriend;
|
||||
}),
|
||||
updated,
|
||||
}));
|
||||
});
|
||||
|
||||
app.get('/api/znc/friends/presence', authToken, (req, res, next) => {
|
||||
if (!req.zncAuthPolicy) return next();
|
||||
if (!req.zncAuthPolicy.list_friends_presence) return tokenUnauthorised(req, res);
|
||||
|
|
@ -365,6 +383,30 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
res.end(JSON.stringify(presence));
|
||||
});
|
||||
|
||||
app.get('/api/znc/friends/favourites/presence', authToken, (req, res, next) => {
|
||||
if (!req.zncAuthPolicy) return next();
|
||||
if (!req.zncAuthPolicy.list_friends_presence) return tokenUnauthorised(req, res);
|
||||
next();
|
||||
}, localAuth, nsoAuth, getFriendsData, async (req, res) => {
|
||||
const [friends, updated] = cached_friendsdata.get(req.zncAuth!.user.id)!;
|
||||
const presence: Record<string, Presence> = {};
|
||||
|
||||
for (const friend of friends) {
|
||||
if (req.zncAuthPolicy) {
|
||||
const p = req.zncAuthPolicy;
|
||||
if (p.friends_presence && !p.friends_presence.includes(friend.nsaId)) continue;
|
||||
if (p.friends && !p.friends_presence && !p.friends.includes(friend.nsaId)) continue;
|
||||
}
|
||||
|
||||
if (!friend.isFavoriteFriend) continue;
|
||||
|
||||
presence[friend.nsaId] = friend.presence;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify(presence));
|
||||
});
|
||||
|
||||
app.get('/api/znc/friend/:nsaid', authToken, (req, res, next) => {
|
||||
if (!req.zncAuthPolicy) return next();
|
||||
if (!req.zncAuthPolicy.friend) return tokenUnauthorised(req, res);
|
||||
|
|
@ -388,6 +430,56 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
res.end(JSON.stringify({friend, updated}));
|
||||
});
|
||||
|
||||
app.post('/api/znc/friend/:nsaid', nsoAuth, getFriendsData, bodyParser.json(), async (req, res) => {
|
||||
const [friends, updated] = cached_friendsdata.get(req.zncAuth!.user.id)!;
|
||||
const friend = friends.find(f => f.nsaId === req.params.nsaid);
|
||||
|
||||
if (!friend) {
|
||||
res.statusCode = 404;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: 'not_found',
|
||||
error_message: 'The user is not friends with the authenticated user.',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if ('isFavoriteFriend' in req.body &&
|
||||
req.body.isFavoriteFriend !== true &&
|
||||
req.body.isFavoriteFriend !== false
|
||||
) {
|
||||
res.statusCode = 400;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: 'invalid_request',
|
||||
error_message: 'Invalid value for isFavoriteFriend',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if ('isFavoriteFriend' in req.body) {
|
||||
try {
|
||||
if (friend.isFavoriteFriend !== req.body.isFavoriteFriend) {
|
||||
if (req.body.isFavoriteFriend) await req.znc!.addFavouriteFriend(friend.nsaId);
|
||||
if (!req.body.isFavoriteFriend) await req.znc!.removeFavouriteFriend(friend.nsaId);
|
||||
} else {
|
||||
// No change
|
||||
}
|
||||
} catch (err) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.get('/api/znc/friend/:nsaid/presence', authToken, (req, res, next) => {
|
||||
if (!req.zncAuthPolicy) return next();
|
||||
if (!req.zncAuthPolicy.friend_presence) return tokenUnauthorised(req, res);
|
||||
|
|
@ -452,6 +544,42 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
res.end(JSON.stringify({activeevent, updated}));
|
||||
});
|
||||
|
||||
app.get('/api/znc/event/:id', nsoAuth, async (req, res) => {
|
||||
try {
|
||||
const response = await req.znc!.getEvent(parseInt(req.params.id));
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
event: response.result,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/znc/user/:id', nsoAuth, async (req, res) => {
|
||||
try {
|
||||
const response = await req.znc!.getUser(parseInt(req.params.id));
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
user: response.result,
|
||||
}));
|
||||
} catch (err) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.end(JSON.stringify({
|
||||
error: err,
|
||||
error_message: (err as Error).message,
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Nintendo Switch user data
|
||||
//
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user