diff --git a/src/api/coral-types.ts b/src/api/coral-types.ts index 8daf7ce..b9ffbca 100644 --- a/src/api/coral-types.ts +++ b/src/api/coral-types.ts @@ -81,6 +81,20 @@ export interface AccountLogin { /** /v3/Account/GetToken, /v3/Extension/Account/GetToken */ export type AccountToken = AccountLogin; +/** /v4/Account/Login */ +export interface AccountLogin_4 { + user: Exclude & { + links: Exclude; + }; + webApiServerCredential: { + accessToken: string; + expiresIn: number; + }; +} + +/** /v4/Account/GetToken, /v4/Extension/Account/GetToken */ +export type AccountToken_4 = AccountLogin_4; + export interface AccountTokenParameter { naIdToken: string; naBirthday: string; @@ -177,22 +191,38 @@ export interface FriendRoute { userName: string; shopUri: string; imageUri: string; - // if not IN_APP all other properties are empty strings + // if not IN_APP/NINTENDO_ACCOUNT all other properties are empty strings channel: FriendRouteChannel; } export enum FriendRouteChannel { /** Added from friend code lookup on a Switch console or using coral */ FRIEND_CODE = 'FRIEND_CODE', - /** Added from users you've played with */ - IN_APP = 'IN_APP', /** Added from search for local users */ NX_FACED = 'NX_FACED', + /** + * Added from users you've played with + * Shows the application details (appName, shopUri, imageUri) and in-game name (userName) + */ + IN_APP = 'IN_APP', + /** + * Added from friend suggestions for smart device game friends linked to the same Nintendo Account + * Shows the client name (FriendRoute.appName) + */ + NINTENDO_ACCOUNT = 'NINTENDO_ACCOUNT', '3DS' = '3DS', - - // Wii U, Facebook, Twitter suggestions? + NNID = 'NNID', + TWITTER = 'TWITTER', + FACEBOOK = 'FACEBOOK', + WECHAT = 'WECHAT', + /** Added from GameChat */ + CAMPUS = 'CAMPUS', } +export type CreateFriendRequestChannel = + FriendRouteChannel.FRIEND_CODE | + FriendRouteChannel.CAMPUS; + export interface Presence { state: PresenceState; /** @@ -624,6 +654,8 @@ export enum FeedbackTopic { // - /v1/Voip/Mute // - /v5/Chat/Show // - /v5/Chat/List +// - /v5/Chat/Member/Show +// - /v5/Chat/FriendCandidate/List // - /v1/Support/ReportUser // // Also, announcement types other than OPERATION and FRIEND_REQUEST diff --git a/src/api/coral.ts b/src/api/coral.ts index f767ded..c8f8a10 100644 --- a/src/api/coral.ts +++ b/src/api/coral.ts @@ -5,7 +5,7 @@ import { JwtPayload } from '../util/jwt.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_4, BlockingUsers, CoralError, CoralResponse, CoralStatus, CoralSuccessResponse, CurrentUser, CurrentUserPermissions, Event, Friend_4, FriendCodeUrl, FriendCodeUser, Friends_4, GetActiveEventResult, ListChat, ListHashtag, ListHashtagParameter, ListMedia, ListMediaParameter, ListPushNotificationSettings, Media, PlayLogPermissions, PresencePermissions, PushNotificationPlayInvitationScope, ReceivedFriendRequest, ReceivedFriendRequests, SentFriendRequests, ShowUserLogin, UpdatePushNotificationSettingsParameter, UpdatePushNotificationSettingsParameterItem, User, UserPlayLog, WebServices_4, WebServiceToken, WebServiceTokenParameter } from './coral-types.js'; +import { AccountLogin, AccountLogin_4, AccountLoginParameter, AccountToken, AccountToken_4, AccountTokenParameter, Announcements_4, BlockingUsers, CoralError, CoralResponse, CoralStatus, CoralSuccessResponse, CreateFriendRequestChannel, CurrentUser, CurrentUserPermissions, Event, Friend_4, FriendCodeUrl, FriendCodeUser, FriendRouteChannel, Friends_4, GetActiveEventResult, ListChat, ListHashtag, ListHashtagParameter, ListMedia, ListPushNotificationSettings, Media, PlayLogPermissions, PresencePermissions, PushNotificationPlayInvitationScope, ReceivedFriendRequest, ReceivedFriendRequests, SentFriendRequests, ShowUserLogin, UpdatePushNotificationSettingsParameter, UpdatePushNotificationSettingsParameterItem, User, UserPlayLog, WebServices_4, WebServiceToken, WebServiceTokenParameter } from './coral-types.js'; import { createZncaApi, DecryptResponseResult, FResult, HashMethod, RequestEncryptionProvider, ZncaApi, ZncaApiNxapi } from './f.js'; import { generateAuthData, getNintendoAccountToken, getNintendoAccountUser, NintendoAccountScope, NintendoAccountSessionAuthorisation, NintendoAccountToken, NintendoAccountUser } from './na.js'; import { ErrorResponse, ResponseSymbol } from './util.js'; @@ -15,9 +15,11 @@ const debug = createDebug('nxapi:api:coral'); const ZNCA_PLATFORM = 'Android'; const ZNCA_PLATFORM_VERSION = '12'; -export const ZNCA_VERSION = '3.0.2'; // TODO: update to 3.1.0 +export const ZNCA_VERSION = '3.2.0'; // TODO: update to 3.1.0 const ZNCA_USER_AGENT = `com.nintendo.znca/${ZNCA_VERSION}(${ZNCA_PLATFORM}/${ZNCA_PLATFORM_VERSION})`; +export const ZNCA_API_COMPATIBILITY_VERSION = 'hio87-mJks_e9GNF'; + const ZNC_URL = 'https://api-lp1.znc.srv.nintendo.net'; export const ZNCA_CLIENT_ID = '71b963c1b7b6d119'; @@ -93,9 +95,7 @@ export abstract class AbstractCoralApi { } async getMedia() { - return this.call('/v4/Media/List', { - count: 100, - }); + return this.call('/v4/Media/List'); } async getHashtags(media: Media) { @@ -186,9 +186,10 @@ export abstract class AbstractCoralApi { }); } - async sendFriendRequest(nsa_id: string) { - return this.call('/v3/FriendRequest/Create', { + async sendFriendRequest(nsa_id: string, channel: CreateFriendRequestChannel = FriendRouteChannel.FRIEND_CODE) { + return this.call('/v4/FriendRequest/Create', { nsaId: nsa_id, + channel, }); } @@ -591,7 +592,7 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf const fdata = await provider.genf(nintendoAccountToken.id_token, HashMethod.CORAL, { na_id: user.id, coral_user_id: '' + this[CoralUserIdSymbol], }, provider.supportsEncryption() ? { - url: ZNC_URL + '/v3/Account/GetToken', + url: ZNC_URL + '/v4/Account/GetToken', parameter, } : undefined); @@ -609,13 +610,13 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf }); if (provider.supportsEncryption()) { - const result = await provider.encryptRequest(ZNC_URL + '/v3/Account/GetToken', null, body); + const result = await provider.encryptRequest(ZNC_URL + '/v4/Account/GetToken', null, body); body = new EncryptedRequestBody(provider, result.data, body); } } - const data = await this.fetch('/v3/Account/GetToken', 'POST', body, undefined, { + const data = await this.fetch('/v4/Account/GetToken', 'POST', body, undefined, { [RequestFlagAddPlatformSymbol]: true, [RequestFlagAddProductVersionSymbol]: true, [RequestFlagNoAuthenticationSymbol]: true, @@ -719,7 +720,7 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf const fdata = await provider.genf(nintendoAccountToken.id_token, HashMethod.CORAL, { na_id: user.id, }, provider.supportsEncryption() ? { - url: ZNC_URL + '/v3/Account/Login', + url: ZNC_URL + '/v4/Account/Login', parameter, } : undefined); @@ -743,7 +744,7 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf }); if (provider.supportsEncryption()) { - const result = await provider.encryptRequest(ZNC_URL + '/v3/Account/Login', null, body); + const result = await provider.encryptRequest(ZNC_URL + '/v4/Account/Login', null, body); encrypted = [provider]; body = result.data; @@ -759,22 +760,22 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf }); const [signal, cancel] = timeoutSignal(); - const response = await fetch(ZNC_URL + '/v3/Account/Login', { + const response = await fetch(ZNC_URL + '/v4/Account/Login', { method: 'POST', headers, body, signal, }).finally(cancel); - debug('fetch %s %s, response %s', 'POST', '/v3/Account/Login', response.status); + debug('fetch %s %s, response %s', 'POST', '/v4/Account/Login', response.status); if (response.status !== 200) { throw await CoralErrorResponse.fromResponse(response, '[znc] Non-200 status code'); } - const data: CoralResponse = encrypted ? + const data: CoralResponse = encrypted ? JSON.parse((await encrypted[0].decryptResponse(new Uint8Array(await response.arrayBuffer()))).data) : - await response.json() as CoralResponse; + await response.json() as CoralResponse; if ('errorMessage' in data) { throw new CoralErrorResponse('[znc] ' + data.errorMessage, response, data); @@ -1048,7 +1049,7 @@ export interface CoralAuthData { nintendoAccountToken: NintendoAccountToken; user: NintendoAccountUserCoral; f: FResult; - nsoAccount: AccountLogin; + nsoAccount: AccountLogin | AccountLogin_4; credential: AccountLogin['webApiServerCredential']; znca_version: string; znca_useragent: string; diff --git a/src/api/f.ts b/src/api/f.ts index 488ccee..81ea1db 100644 --- a/src/api/f.ts +++ b/src/api/f.ts @@ -6,7 +6,7 @@ import createDebug from '../util/debug.js'; import { timeoutSignal } from '../util/misc.js'; import { getUserAgent } from '../util/useragent.js'; import { client_assertion_provider, client_auth_provider, ClientAssertionProviderInterface } from '../util/nxapi-auth.js'; -import { ZNCA_VERSION } from './coral.js'; +import { ZNCA_API_COMPATIBILITY_VERSION } from './coral.js'; import { AccountLoginParameter, AccountTokenParameter, WebServiceTokenParameter } from './coral-types.js'; const debugFlapg = createDebug('nxapi:api:flapg'); @@ -345,6 +345,7 @@ export class ZncaApiNxapi extends ZncaApi implements RequestEncryptionProvider { this.auth = auth; this.headers.set('User-Agent', getUserAgent(useragent)); + this.headers.append('X-znca-Client-Version', ZNCA_API_COMPATIBILITY_VERSION); } static create( @@ -384,7 +385,6 @@ export class ZncaApiNxapi extends ZncaApi implements RequestEncryptionProvider { headers.set('Accept', 'application/json'); if (this.app?.platform) headers.append('X-znca-Platform', this.app.platform); if (this.app?.version) headers.append('X-znca-Version', this.app.version); - if (ZNCA_VERSION) headers.append('X-znca-Client-Version', ZNCA_VERSION); if (this.auth?.token) headers.append('Authorization', 'Bearer ' + this.auth.token.token); const [signal, cancel] = timeoutSignal(); @@ -450,7 +450,6 @@ export class ZncaApiNxapi extends ZncaApi implements RequestEncryptionProvider { headers.set('Accept', 'application/octet-stream'); if (this.app?.platform) headers.append('X-znca-Platform', this.app.platform); if (this.app?.version) headers.append('X-znca-Version', this.app.version); - if (ZNCA_VERSION) headers.append('X-znca-Client-Version', ZNCA_VERSION); if (this.auth?.token) headers.append('Authorization', 'Bearer ' + this.auth.token.token); const [signal, cancel] = timeoutSignal(); @@ -504,7 +503,6 @@ export class ZncaApiNxapi extends ZncaApi implements RequestEncryptionProvider { headers.set('Accept', request_nsa_assertion ? 'application/json' : 'text/plain'); if (this.app?.platform) headers.append('X-znca-Platform', this.app.platform); if (this.app?.version) headers.append('X-znca-Version', this.app.version); - if (ZNCA_VERSION) headers.append('X-znca-Client-Version', ZNCA_VERSION); if (this.auth?.token) headers.append('Authorization', 'Bearer ' + this.auth.token.token); const [signal, cancel] = timeoutSignal(); diff --git a/src/cli/nso/user.ts b/src/cli/nso/user.ts index 921bcbb..ed92837 100644 --- a/src/cli/nso/user.ts +++ b/src/cli/nso/user.ts @@ -43,6 +43,6 @@ export async function handler(argv: ArgumentsCamelCase) { nso.getCurrentUser(), ]) : []; - console.log('Nintendo Account', data.user); + console.log('Nintendo Account', data.user, 'naUser' in data.nsoAccount ? data.nsoAccount.naUser : null); console.log('Nintendo Switch user', current_user ?? data.nsoAccount.user); }