diff --git a/src/api/coral-types.ts b/src/api/coral-types.ts index dbb4d4b..5c6989d 100644 --- a/src/api/coral-types.ts +++ b/src/api/coral-types.ts @@ -34,7 +34,17 @@ export enum CoralStatus { MULTIPLE_LOGIN = 9426, UPGRADE_REQUIRED = 9427, ACCOUNT_DISABLED = 9428, + RATE_LIMIT_EXCEEDED = 9437, MEMBERSHIP_REQUIRED = 9450, + INVALID_FRIEND_REQUEST = 9460, + SENDER_FRIEND_LIMIT_EXCEEDED = 9461, + RECEIVER_FRIEND_LIMIT_EXCEEDED = 9462, + FRIEND_REQUEST_NOT_ACCEPTED = 9463, + DUPLICATE_FRIEND_REQUEST = 9464, + PRECONDITION_FAILED = 9465, + RESOURCE_LIMIT_EXCEEDED = 9466, + ALREADY_FRIEND = 9467, + SENDER_BLOCKS_RECEIVER_FRIEND_REQUEST = 9468, SERVICE_CLOSED = 9499, INTERNAL_SERVER_ERROR = 9500, SERVICE_UNAVAILABLE = 9501, @@ -128,6 +138,21 @@ export interface Game { sysDescription: string; } +/** /v3/Friend/CreateFriendCodeUrl */ +export interface FriendCodeUrl { + url: string; + friendCode: string; +} + +/** /v3/Friend/GetUserByFriendCode, /v3/Friend/GetUserByFriendCodeHash */ +export interface FriendCodeUser { + id: number; + nsaId: string; + imageUrl: string; + name: string; + extras: {}; +} + /** /v1/Game/ListWebServices */ export type WebServices = WebService[]; diff --git a/src/api/coral.ts b/src/api/coral.ts index 53abf4e..503a74a 100644 --- a/src/api/coral.ts +++ b/src/api/coral.ts @@ -2,7 +2,7 @@ import fetch, { Response } from 'node-fetch'; import { v4 as uuidgen } from 'uuid'; import createDebug from 'debug'; import { f, FlapgIid } from './f.js'; -import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse } from './coral-types.js'; +import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl } from './coral-types.js'; import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountUser } from './na.js'; import { ErrorResponse } from './util.js'; import { JwtPayload } from '../util/jwt.js'; @@ -12,12 +12,15 @@ const debug = createDebug('nxapi:api:coral'); const ZNCA_PLATFORM = 'Android'; const ZNCA_PLATFORM_VERSION = '8.0.0'; -const ZNCA_VERSION = '2.1.1'; +const ZNCA_VERSION = '2.2.0'; const ZNCA_USER_AGENT = `com.nintendo.znca/${ZNCA_VERSION}(${ZNCA_PLATFORM}/${ZNCA_PLATFORM_VERSION})`; const ZNC_URL = 'https://api-lp1.znc.srv.nintendo.net'; export const ZNCA_CLIENT_ID = '71b963c1b7b6d119'; +const FRIEND_CODE = /^\d{4}-\d{4}-\d{4}$/; +const FRIEND_CODE_HASH = /^[A-Za-z0-9]{10}$/; + export default class CoralApi { onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise) | null = null; /** @internal */ @@ -123,10 +126,32 @@ export default class CoralApi { }); } + async getUserByFriendCode(friend_code: string, hash?: string) { + if (!FRIEND_CODE.test(friend_code)) throw new Error('Invalid friend code'); + if (hash && !FRIEND_CODE_HASH.test(hash)) throw new Error('Invalid friend code hash'); + + return hash ? this.call('/v3/Friend/GetUserByFriendCodeHash', { + friendCode: friend_code, + friendCodeHash: hash, + }) : this.call('/v3/Friend/GetUserByFriendCode', { + friendCode: friend_code, + }); + } + + async sendFriendRequest(nsa_id: string) { + return this.call<{}>('/v3/FriendRequest/Create', { + nsaId: nsa_id, + }); + } + async getCurrentUser() { return this.call('/v3/User/ShowSelf'); } + async getFriendCodeUrl() { + return this.call('/v3/Friend/CreateFriendCodeUrl'); + } + async getCurrentUserPermissions() { return this.call('/v3/User/Permissions/ShowSelf'); } diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts index ba6a7f2..75cc304 100644 --- a/src/app/main/ipc.ts +++ b/src/app/main/ipc.ts @@ -164,6 +164,7 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) { ipcMain.handle('nxapi:webserviceapi:requestGameWebToken', e => webserviceipc.requestGameWebToken(e)); ipcMain.handle('nxapi:webserviceapi:restorePersistentData', e => webserviceipc.restorePersistentData(e)); ipcMain.handle('nxapi:webserviceapi:storePersistentData', (e, data: string) => webserviceipc.storePersistentData(e, data)); + ipcMain.handle('nxapi:webserviceapi:completeLoading', e => webserviceipc.completeLoading(e)); store.on('update-nintendo-accounts', () => sendToAllWindows('nxapi:accounts:shouldrefresh')); store.on('update-discord-presence-source', () => sendToAllWindows('nxapi:discord:shouldrefresh')); diff --git a/src/app/main/webservices.ts b/src/app/main/webservices.ts index bf840aa..c9a9ca4 100644 --- a/src/app/main/webservices.ts +++ b/src/app/main/webservices.ts @@ -338,4 +338,10 @@ export class WebServiceIpc { const key = 'WebServicePersistentData.' + nsoAccount.user.nsaId + '.' + webservice.id; await store.storage.setItem(key, data); } + + async completeLoading(event: IpcMainInvokeEvent): Promise { + const {nsoAccount, webservice} = this.getWindowData(event.sender); + + debug('Web service %s, user %s, called completeLoading', webservice.name, nsoAccount.user.name); + } } diff --git a/src/app/preload-webservice/ipc.ts b/src/app/preload-webservice/ipc.ts index b505c34..a056d3a 100644 --- a/src/app/preload-webservice/ipc.ts +++ b/src/app/preload-webservice/ipc.ts @@ -14,6 +14,7 @@ const ipc = { requestGameWebToken: () => ipcRenderer.invoke('nxapi:webserviceapi:requestGameWebToken') as Promise, restorePersistentData: () => ipcRenderer.invoke('nxapi:webserviceapi:restorePersistentData') as Promise, storePersistentData: (data: string) => ipcRenderer.invoke('nxapi:webserviceapi:storePersistentData', data) as Promise, + completeLoading: () => ipcRenderer.invoke('nxapi:webserviceapi:completeLoading') as Promise, }; export default ipc; diff --git a/src/app/preload-webservice/znca-js-api.ts b/src/app/preload-webservice/znca-js-api.ts index a2fa0c3..2330d74 100644 --- a/src/app/preload-webservice/znca-js-api.ts +++ b/src/app/preload-webservice/znca-js-api.ts @@ -29,7 +29,7 @@ declare global { onPersistentDataRestore?: (data: string) => void; // NookLink storePersistentData?: (data: string) => void; - onPersistentDataStore?: () => void; + onPersistentDataStore?: (data: string) => void; // NookLink openQRCodeReader?: (data: string) => void; @@ -40,6 +40,18 @@ declare global { closeQRCodeReader?: () => void; // NookLink closeQRCodeReaderFromPhotoLibrary?: () => void; + + // Unused + sendMessage?(data: string): void; + // Unused + copyToClipboard?(data: string): void; + + openQRCodeReaderForCheckin?(data: string): void; + onQRCodeReadForCheckin?(data: string): void; + downloadImages?(imagesJson: string): void; + completeLoading?(): void; + closeWebView?(): void; + reloadExtension?(): void; } } @@ -109,7 +121,7 @@ function storePersistentData(data: string) { debug('storePersistentData called', data); ipc.storePersistentData(data).then(() => { - window.onPersistentDataStore?.call(null); + window.onPersistentDataStore?.call(null, ''); }); } @@ -146,7 +158,52 @@ function closeQrCodeReaderFromPhotoLibrary() { // } +function openQRCodeReaderForCheckin(data: string) { + // + + Promise.resolve().then(() => { + const base64EncodeText = ''; + window.onQRCodeReadForCheckin?.call(null, base64EncodeText); + }); +} + window.openQRCodeReader = openQrCodeReader; window.openQRCodeReaderFromPhotoLibrary = openQrCodeReaderFromPhotoLibrary; window.closeQRCodeReader = closeQrCodeReader; window.closeQRCodeReaderFromPhotoLibrary = closeQrCodeReaderFromPhotoLibrary; +window.openQRCodeReaderForCheckin = openQRCodeReaderForCheckin; + +// +// Other +// + +function sendMessage(data: string) { + // +} + +function copyToClipboard(data: string) { + // +} + +function downloadImages(imagesJson: string) { + // +} + +function completeLoading() { + ipc.completeLoading(); +} + +function closeWebView() { + window.close(); +} + +function reloadExtension() { + // +} + +window.sendMessage = sendMessage; +window.copyToClipboard = copyToClipboard; +window.downloadImages = downloadImages; +window.completeLoading = completeLoading; +window.closeWebView = closeWebView; +window.reloadExtension = reloadExtension;