Update znc types, add other endpoints and use /v3/Account/GetToken to renew the app token

This commit is contained in:
Samuel Elliott 2022-04-06 23:17:54 +01:00
parent 8bd6f306f8
commit a53eccd91e
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
4 changed files with 343 additions and 23 deletions

View File

@ -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() {

View File

@ -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;

View File

@ -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;

View File

@ -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
//