├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── discord-title-configuration.yml └── funding.yml ├── .gitignore ├── .gitlab-ci.yml ├── .vscode ├── generate-schemas.sh ├── launch.json ├── schema │ ├── moon │ │ ├── dailysummary.schema.json │ │ └── monthlysummary.schema.json │ ├── nooklink │ │ └── newspaper.schema.json │ ├── splatnet2 │ │ ├── coop-result.schema.json │ │ ├── coop-summary.schema.json │ │ ├── hero.schema.json │ │ ├── ni.schema.json │ │ ├── records.schema.json │ │ ├── result.schema.json │ │ ├── results-summary.schema.json │ │ └── timeline.schema.json │ └── splatnet3 │ │ ├── coop-result.schema.json │ │ └── result.schema.json └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── bin └── nxapi.js ├── docker-compose.yml ├── docs ├── cli.md └── lib │ ├── README.md │ ├── coral.md │ ├── index.md │ ├── moon.md │ ├── nooklink.md │ ├── splatnet2.md │ └── splatnet3.md ├── package-lock.json ├── package.json ├── resources ├── app.png ├── app │ └── menu-icon.png ├── build │ ├── app │ │ ├── cli-linux.sh │ │ ├── cli-macos.sh │ │ └── deb │ │ │ ├── postinst │ │ │ └── postrm │ ├── ci-main-version.js │ └── ci-package-json.js ├── cli │ └── fonts │ │ ├── opensans-normal-400.ttf │ │ └── opensans-normal-500.ttf ├── common │ └── remote-config.json ├── discord-activity-splatoon3.png ├── discord-activity.png ├── docker-entrypoint.sh ├── menu-app.png └── notification.png ├── rollup.config.js ├── src ├── api │ ├── coral-types.ts │ ├── coral.ts │ ├── f.ts │ ├── moon-types.ts │ ├── moon.ts │ ├── na.ts │ ├── nooklink-types.ts │ ├── nooklink.ts │ ├── splatnet2-types.ts │ ├── splatnet2-xrank.ts │ ├── splatnet2.ts │ ├── splatnet3-types.ts │ ├── splatnet3.ts │ ├── util.ts │ └── znc-proxy.ts ├── app │ ├── README.md │ ├── app-entry.cts │ ├── app-init.ts │ ├── browser │ │ ├── add-account-manual │ │ │ └── index.tsx │ │ ├── add-friend │ │ │ └── index.tsx │ │ ├── components │ │ │ ├── button.tsx │ │ │ ├── friend-code.tsx │ │ │ ├── icons │ │ │ │ ├── add-outline.tsx │ │ │ │ ├── util.ts │ │ │ │ └── warning.tsx │ │ │ ├── index.ts │ │ │ └── nintendo-switch-user.tsx │ │ ├── constants.ts │ │ ├── discord │ │ │ └── index.tsx │ │ ├── friend │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── main │ │ │ ├── discord-setup.tsx │ │ │ ├── discord.tsx │ │ │ ├── event.tsx │ │ │ ├── friends.tsx │ │ │ ├── index.tsx │ │ │ ├── main.tsx │ │ │ ├── section.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── status-updates.tsx │ │ │ ├── update.tsx │ │ │ └── webservices.tsx │ │ ├── preferences │ │ │ └── index.tsx │ │ ├── react-native-web.d.ts │ │ └── util.tsx │ ├── common │ │ └── types.ts │ ├── i18n │ │ ├── index.ts │ │ └── locale │ │ │ ├── de-de.ts │ │ │ ├── en-gb.ts │ │ │ ├── es-es.ts │ │ │ └── ja-jp.ts │ ├── main │ │ ├── app-menu.ts │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── menu.ts │ │ ├── monitor.ts │ │ ├── na-auth.ts │ │ ├── support.ts │ │ ├── util.ts │ │ ├── webservices.ts │ │ └── windows.ts │ ├── preload-webservice │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── loading.ts │ │ ├── quirks │ │ │ ├── nooklink.ts │ │ │ ├── splatnet2.ts │ │ │ └── splatnet3.ts │ │ └── znca-js-api.ts │ └── preload │ │ └── index.ts ├── cli-entry.ts ├── cli.ts ├── cli │ ├── android-znca-api-server-frida.ts │ ├── app.ts │ ├── commands.ts │ ├── nooklink │ │ ├── commands.ts │ │ ├── dump-newspapers.ts │ │ ├── index.ts │ │ ├── island.ts │ │ ├── keyboard.ts │ │ ├── newspaper.ts │ │ ├── newspapers.ts │ │ ├── post-reaction.ts │ │ ├── reactions.ts │ │ ├── user-token.ts │ │ ├── user.ts │ │ └── users.ts │ ├── nso │ │ ├── active-event.ts │ │ ├── add-friend.ts │ │ ├── announcements.ts │ │ ├── auth.ts │ │ ├── commands.ts │ │ ├── friendcode.ts │ │ ├── friends.ts │ │ ├── http-server.ts │ │ ├── index.ts │ │ ├── lookup.ts │ │ ├── notify.ts │ │ ├── permissions.ts │ │ ├── presence.ts │ │ ├── token.ts │ │ ├── user.ts │ │ ├── webservices.ts │ │ ├── webservicetoken.ts │ │ └── znc-proxy-tokens.ts │ ├── pctl │ │ ├── auth.ts │ │ ├── commands.ts │ │ ├── daily-summaries.ts │ │ ├── devices.ts │ │ ├── dump-summaries.ts │ │ ├── index.ts │ │ ├── monthly-summaries.ts │ │ ├── monthly-summary.ts │ │ ├── settings.ts │ │ ├── token.ts │ │ └── user.ts │ ├── presence-server.ts │ ├── splatnet2 │ │ ├── battles.ts │ │ ├── challenges.ts │ │ ├── commands.ts │ │ ├── dump-records.ts │ │ ├── dump-results.ts │ │ ├── hero.ts │ │ ├── index.ts │ │ ├── monitor.ts │ │ ├── schedule.ts │ │ ├── stages.ts │ │ ├── token.ts │ │ ├── user.ts │ │ ├── weapons.ts │ │ └── x-rank-seasons.ts │ ├── splatnet3 │ │ ├── battles.ts │ │ ├── commands.ts │ │ ├── dump-album.ts │ │ ├── dump-fests.ts │ │ ├── dump-records.ts │ │ ├── dump-results.ts │ │ ├── festival.ts │ │ ├── festivals.ts │ │ ├── friends.ts │ │ ├── index.ts │ │ ├── monitor.ts │ │ ├── schedule.ts │ │ ├── token.ts │ │ └── user.ts │ ├── users.ts │ └── util │ │ ├── captureid.ts │ │ ├── commands.ts │ │ ├── decrypt-log-archive.ts │ │ ├── discord-activity.ts │ │ ├── discord-rpc.ts │ │ ├── export-discord-titles.ts │ │ ├── index.ts │ │ ├── log-archive.ts │ │ ├── presence-embed-render.ts │ │ ├── presence-embed-server.ts │ │ ├── remote-config.ts │ │ ├── status.ts │ │ ├── storage.ts │ │ └── validate-discord-titles.ts ├── client │ ├── coral.ts │ ├── na.ts │ ├── splatnet3.ts │ ├── storage │ │ ├── index.ts │ │ └── local.ts │ ├── users.ts │ └── util.ts ├── common │ ├── auth │ │ ├── coral.ts │ │ ├── moon.ts │ │ ├── na.ts │ │ ├── nooklink.ts │ │ ├── splatnet2.ts │ │ ├── splatnet3.ts │ │ └── util.ts │ ├── constants.ts │ ├── globals.ts │ ├── notify.ts │ ├── presence-embed.ts │ ├── presence.ts │ ├── remote-config.ts │ ├── splatnet2 │ │ ├── dump-records.ts │ │ ├── dump-results.ts │ │ └── monitor.ts │ ├── status.ts │ ├── update.ts │ └── users.ts ├── discord │ ├── monitor │ │ └── splatoon3.ts │ ├── rpc.ts │ ├── titles.ts │ ├── titles │ │ ├── README.md │ │ ├── capcom.ts │ │ ├── concernedape.ts │ │ ├── epicgames.ts │ │ ├── index.ts │ │ ├── mojang.ts │ │ ├── nintendo.ts │ │ ├── phoenix-labs.ts │ │ ├── thatgamecompany.ts │ │ └── the-pokémon-company.ts │ ├── types.ts │ └── util.ts ├── exports │ ├── coral.ts │ ├── index.ts │ ├── moon.ts │ ├── nintendo-account.ts │ ├── nooklink.ts │ ├── splatnet2.ts │ └── splatnet3.ts ├── index.ts └── util │ ├── debug.ts │ ├── errors.ts │ ├── eventsource.ts │ ├── http-server.ts │ ├── http.ts │ ├── jwt.ts │ ├── loop.ts │ ├── misc.ts │ ├── net.ts │ ├── product.ts │ ├── storage.ts │ ├── support.ts │ ├── table.ts │ ├── undici-proxy.ts │ ├── useragent.ts │ └── yargs.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | data 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: Report something that isn't working as expected 3 | 4 | labels: 5 | - bug 6 | 7 | body: 8 | 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Please remember to search other issues before opening an issue, but make sure it is the same as the issue you have before commenting on other issues, for example make sure any error messages are the same. 13 | 14 | Not all issues are bugs - some error messages may indicate a different problem, for example, the error code `ENOTFOUND` may mean there is a problem with your network. Make sure your Internet connection is working and there are no [known service issues](https://nxapi-status.fancy.org.uk). 15 | 16 | Make sure you test the latest development build as the issue may have already been fixed: [macOS/Linux](https://gitlab.fancy.org.uk/samuel/nxapi/-/jobs/artifacts/main/browse/app?job=build-app), [Windows](https://gitlab.fancy.org.uk/samuel/nxapi/-/jobs/artifacts/main/browse/app?job=build-windows). 17 | 18 | > [!IMPORTANT] 19 | > The latest release of nxapi no longer works due to changes by Nintendo. If you are using nxapi 1.6.1 or before and do not update to the latest development build you will see an error message from the f-generation API as the `X-znca-Client-Version` header is not sent. 20 | 21 | - type: textarea 22 | id: what-you-did 23 | attributes: 24 | label: What did you try to do? 25 | description: | 26 | Describe the steps you took to reproduce the issue. You can upload screenshots or screen recordings. Please don't add links to files or take photos of your screen using a phone or camera. 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: what-should-happen 32 | attributes: 33 | label: What should happen? 34 | description: | 35 | Describe what you expected to happen. 36 | 37 | - type: textarea 38 | id: what-did-happen 39 | attributes: 40 | label: What did happen? 41 | description: | 42 | Describe what actually happened instead of what you expected. You can upload screenshots or screen recordings. Please don't add links to files or take photos of your screen using a phone or camera. 43 | validations: 44 | required: true 45 | 46 | - type: textarea 47 | id: other-information 48 | attributes: 49 | label: Other information 50 | description: | 51 | Include any other relevant information, such as when the problem started, or if it only happens sometimes, and information about your system such as your operating system and nxapi version. 52 | 53 | You can upload log files from nxapi by selecting export logs in the help menu. On Windows/Linux, press the Alt key in the main window to temporarily show the application menus. This will generate a file containing nxapi's debug logs, which are stored on your computer for 14 days. Log files may contain sensitive information, but the export logs option encrypts the saved logs so only @samuelthomas2774 can read them. You can also use the `nxapi util log-archive` command to generate an encrypted log archive. 54 | validations: 55 | required: true 56 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | 3 | contact_links: 4 | 5 | - name: Discussions 6 | about: Suggest new features and ask questions. 7 | url: https://github.com/samuelthomas2774/nxapi/discussions 8 | 9 | - name: Discord 10 | about: https://discord.com/invite/4D82rFkXRv 11 | url: https://discord.com/invite/4D82rFkXRv 12 | 13 | - name: nxapi Service Status 14 | about: Check the status of services used by nxapi. Issues may take a few minutes to appear here. 15 | url: https://nxapi-status.fancy.org.uk 16 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | ko_fi: samuelelliott 2 | custom: 3 | - https://www.buymeacoffee.com/samuelelliott 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | data 4 | docker-compose.override.yml 5 | .env 6 | .vscode/schema/generated 7 | -------------------------------------------------------------------------------- /.vscode/generate-schemas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p .vscode/schema/generated 4 | 5 | npx ts-json-schema-generator --path src/api/moon-types.ts --expose all --no-type-check > .vscode/schema/generated/moon-types.schema.json 6 | npx ts-json-schema-generator --path src/api/splatnet2-types.ts --expose all --no-type-check > .vscode/schema/generated/splatnet2-types.schema.json 7 | npx ts-json-schema-generator --path src/api/nooklink-types.ts --expose all --no-type-check > .vscode/schema/generated/nooklink-types.schema.json 8 | npx ts-json-schema-generator --path src/api/splatnet3-types.ts --expose all --no-type-check > .vscode/schema/generated/splatnet3-types.schema.json 9 | 10 | npx ts-json-schema-generator --path src/common/remote-config.ts --type NxapiRemoteConfig --no-type-check > .vscode/schema/generated/remote-config.schema.json 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Electron app", 9 | "type": "node", 10 | "request": "launch", 11 | "cwd": "${workspaceFolder}", 12 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 13 | "osx": { 14 | "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron" 15 | }, 16 | "windows": { 17 | "runtimeExecutable": "${workspaceFolder}/node_modules/electron/dist/electron.exe" 18 | }, 19 | "args": [ 20 | "dist/app/app-entry.cjs" 21 | ], 22 | "outputCapture": "std", 23 | "env": { 24 | "DEBUG": "*,-express:*,-body-parser:*", 25 | "DEBUG_COLORS": "1", 26 | "FORCE_COLOR": "3" 27 | }, 28 | "envFile": "${workspaceFolder}/.env" 29 | }, 30 | { 31 | "name": "Coral API proxy", 32 | "type": "node", 33 | "request": "launch", 34 | "cwd": "${workspaceFolder}", 35 | "program": "bin/nxapi.js", 36 | "args": [ 37 | "nso", 38 | "http-server", 39 | "--listen", 40 | "[::1]:8080", 41 | "--no-require-token" 42 | ], 43 | "outputCapture": "std", 44 | "env": { 45 | "DEBUG": "*,-express:*,-body-parser:*", 46 | "DEBUG_COLORS": "1", 47 | "FORCE_COLOR": "3" 48 | }, 49 | "envFile": "${workspaceFolder}/.env" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /.vscode/schema/moon/dailysummary.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/moon-types.schema.json#/definitions/DailySummary" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/moon/monthlysummary.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/moon-types.schema.json#/definitions/MonthlySummary" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/nooklink/newspaper.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/nooklink-types.schema.json#/definitions/Newspaper" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/coop-result.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/CoopResultWithPlayerNicknameAndIcons" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/coop-summary.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/CoopResults" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/hero.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/HeroRecords" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/ni.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/NicknameAndIcon" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/records.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/Records" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/result.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/ResultWithPlayerNicknameAndIcons" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/results-summary.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/Results" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet2/timeline.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$ref": "../generated/splatnet2-types.schema.json#/definitions/Timeline" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet3/coop-result.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "properties": { 5 | "result": { 6 | "$ref": "../generated/splatnet3-types.schema.json#/definitions/CoopHistoryDetail" 7 | } 8 | }, 9 | "required": [ 10 | "result" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/schema/splatnet3/result.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "properties": { 5 | "result": { 6 | "$ref": "../generated/splatnet3-types.schema.json#/definitions/VsHistoryDetail" 7 | } 8 | }, 9 | "required": [ 10 | "result" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "**/data/persist/*": "json" 4 | }, 5 | "json.schemas": [ 6 | { 7 | "fileMatch": ["**/pctl-daily-*.json"], 8 | "url": "./.vscode/schema/moon/dailysummary.schema.json" 9 | }, 10 | { 11 | "fileMatch": ["**/pctl-monthly-*.json"], 12 | "url": "./.vscode/schema/moon/monthlysummary.schema.json" 13 | }, 14 | 15 | { 16 | "fileMatch": ["**/splatnet2-records-*.json", "!**-latest.json"], 17 | "url": "./.vscode/schema/splatnet2/records.schema.json" 18 | }, 19 | { 20 | "fileMatch": ["**/splatnet2-ni-*.json"], 21 | "url": "./.vscode/schema/splatnet2/ni.schema.json" 22 | }, 23 | { 24 | "fileMatch": ["**/splatnet2-timeline-*.json"], 25 | "url": "./.vscode/schema/splatnet2/timeline.schema.json" 26 | }, 27 | { 28 | "fileMatch": ["**/splatnet2-hero-*.json"], 29 | "url": "./.vscode/schema/splatnet2/hero.schema.json" 30 | }, 31 | { 32 | "fileMatch": ["**/splatnet2-results-summary-*.json", "!**/splatnet2-results-summary-image-*", "!**-latest.json"], 33 | "url": "./.vscode/schema/splatnet2/results-summary.schema.json" 34 | }, 35 | { 36 | "fileMatch": ["**/splatnet2-result-*.json", "!**/splatnet2-result-image-*.json"], 37 | "url": "./.vscode/schema/splatnet2/result.schema.json" 38 | }, 39 | { 40 | "fileMatch": ["**/splatnet2-coop-summary-*.json", "!**-latest.json"], 41 | "url": "./.vscode/schema/splatnet2/coop-summary.schema.json" 42 | }, 43 | { 44 | "fileMatch": ["**/splatnet2-coop-result-*.json"], 45 | "url": "./.vscode/schema/splatnet2/coop-result.schema.json" 46 | }, 47 | 48 | { 49 | "fileMatch": ["**/nooklink-newspaper-*.json"], 50 | "url": "./.vscode/schema/nooklink/newspaper.schema.json" 51 | }, 52 | 53 | { 54 | "fileMatch": ["**/splatnet3-result-*.json"], 55 | "url": "./.vscode/schema/splatnet3/result.schema.json" 56 | }, 57 | { 58 | "fileMatch": ["**/splatnet3-coop-result-*.json"], 59 | "url": "./.vscode/schema/splatnet3/coop-result.schema.json" 60 | }, 61 | 62 | { 63 | "fileMatch": ["**/resources/common/remote-config.json", "**/data/remote-config.json"], 64 | "url": "./.vscode/schema/generated/remote-config.schema.json" 65 | }, 66 | ], 67 | "typescript.tsdk": "node_modules/typescript/lib", 68 | "typescript.preferences.quoteStyle": "single", 69 | } 70 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 as build 2 | 3 | WORKDIR /app 4 | 5 | ADD package.json /app 6 | ADD package-lock.json /app 7 | 8 | RUN npm install 9 | 10 | COPY src /app/src 11 | COPY bin /app/bin 12 | ADD tsconfig.json /app 13 | 14 | RUN npx tsc 15 | 16 | FROM node:20 17 | 18 | WORKDIR /app 19 | 20 | ADD package.json /app 21 | ADD package-lock.json /app 22 | 23 | RUN npm ci --production 24 | 25 | COPY bin /app/bin 26 | COPY resources /app/resources 27 | COPY resources/cli/fonts /usr/local/share/fonts 28 | COPY --from=build /app/dist /app/dist 29 | 30 | RUN ln -s /app/bin/nxapi.js /usr/local/bin/nxapi 31 | ENV NXAPI_DATA_PATH=/data 32 | ENV NODE_ENV=production 33 | 34 | VOLUME [ "/data" ] 35 | 36 | ENTRYPOINT [ "/app/resources/docker-entrypoint.sh" ] 37 | CMD [ "--help" ] 38 | -------------------------------------------------------------------------------- /bin/nxapi.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import('../dist/cli-entry.js'); 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | # 5 | # Coral/znc proxy 6 | # 7 | 8 | znc-proxy: 9 | build: . 10 | command: nso http-server --listen \[::]:80 11 | restart: unless-stopped 12 | labels: 13 | traefik.enable: true 14 | traefik.http.routers.nxapi-znc.entrypoints: websecure 15 | traefik.http.routers.nxapi-znc.rule: Host(`${TRAEFIK_HOST:-nxapi.ta.fancy.org.uk}`) && PathPrefix(`/api/znc/`) 16 | traefik.http.routers.nxapi-znc.tls: true 17 | traefik.http.services.nxapi-znc.loadbalancer.server.port: 80 18 | environment: 19 | DEBUG: '*,-express:*,-body-parser:*' 20 | # ZNCA_API_URL: http://znca-api/api/znca 21 | env_file: .env 22 | volumes: 23 | - data:/data 24 | 25 | # 26 | # Presence server 27 | # 28 | # Start with docker compose --profile presence-server up -d. 29 | # 30 | # Users used to fetch presence data must have a saved session token, e.g. by 31 | # running docker compose run --rm -it presence-server nso auth/token. If not 32 | # all users have access to SplatNet 3, remove the --splatnet3 option and don't 33 | # start the presence-splatnet3-proxy service. 34 | # 35 | # The znc-proxy and presence-splatnet3-proxy services allow the presence-server 36 | # service to be scaled without sending additional requests to Nintendo servers. 37 | # The znc-proxy and presence-splatnet3-proxy services should not be scaled as 38 | # they handle fetching data from Nintendo. 39 | # 40 | 41 | presence-server: 42 | build: . 43 | command: presence-server --listen \[::]:80 --splatnet3 --splatnet3-fest-votes 44 | restart: unless-stopped 45 | profiles: 46 | - presence-server 47 | labels: 48 | traefik.enable: true 49 | traefik.http.routers.nxapi-presence.entrypoints: websecure 50 | traefik.http.routers.nxapi-presence.rule: Host(`${TRAEFIK_HOST:-nxapi.ta.fancy.org.uk}`) && (Path(`/api/presence`) || PathPrefix(`/api/presence/`) || PathPrefix(`/api/splatnet3/resources/`)) 51 | traefik.http.routers.nxapi-presence.tls: true 52 | traefik.http.services.nxapi-presence.loadbalancer.server.port: 80 53 | environment: 54 | DEBUG: '*,-express:*,-send' 55 | ZNC_PROXY_URL: http://znc-proxy/api/znc 56 | NXAPI_PRESENCE_SERVER_USER: ${NXAPI_PRESENCE_SERVER_USER:-} 57 | NXAPI_PRESENCE_SERVER_SPLATNET3_PROXY_URL: http://presence-splatnet3-proxy/api/splatnet3-presence 58 | volumes: 59 | - data:/data 60 | 61 | presence-splatnet3-proxy: 62 | build: . 63 | command: presence-server --listen \[::]:80 --splatnet3 --splatnet3-proxy --splatnet3-record-fest-votes 64 | restart: unless-stopped 65 | profiles: 66 | - presence-server 67 | environment: 68 | DEBUG: '*,-express:*' 69 | ZNC_PROXY_URL: http://znc-proxy/api/znc 70 | volumes: 71 | - data:/data 72 | 73 | volumes: 74 | data: 75 | -------------------------------------------------------------------------------- /docs/lib/README.md: -------------------------------------------------------------------------------- 1 | index.md -------------------------------------------------------------------------------- /resources/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/app.png -------------------------------------------------------------------------------- /resources/app/menu-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/app/menu-icon.png -------------------------------------------------------------------------------- /resources/build/app/cli-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run as /opt/Nintendo Switch Online/nxapi 4 | 5 | APP_BUNDLE_PATH="$(dirname "$0")" 6 | export ELECTRON_RUN_AS_NODE=1 7 | 8 | exec "$APP_BUNDLE_PATH/nxapi-app" "$APP_BUNDLE_PATH/resources/app/dist/bundle/cli-bundle.js" $@ 9 | -------------------------------------------------------------------------------- /resources/build/app/cli-macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run as Nintendo Switch Online.app/Contents/bin/nxapi 4 | 5 | APP_BUNDLE_PATH="$(dirname "$0")/../.." 6 | export ELECTRON_RUN_AS_NODE=1 7 | 8 | exec "$APP_BUNDLE_PATH/Contents/MacOS/Nintendo Switch Online" "$APP_BUNDLE_PATH/Contents/Resources/app/dist/bundle/cli-bundle.js" $@ 9 | -------------------------------------------------------------------------------- /resources/build/app/deb/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ln -sf '/opt/Nintendo Switch Online/nxapi' '/usr/bin/nxapi' 4 | 5 | # Link to the binary 6 | ln -sf '/opt/Nintendo Switch Online/nxapi-app' '/usr/bin/nxapi-app' 7 | 8 | # SUID chrome-sandbox for Electron 5+ 9 | chmod 4755 '/opt/Nintendo Switch Online/chrome-sandbox' || true 10 | 11 | update-mime-database /usr/share/mime || true 12 | update-desktop-database /usr/share/applications || true 13 | -------------------------------------------------------------------------------- /resources/build/app/deb/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -f '/usr/bin/nxapi' 4 | 5 | # Delete the link to the binary 6 | rm -f '/usr/bin/nxapi-app' 7 | -------------------------------------------------------------------------------- /resources/build/ci-main-version.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | import * as child_process from 'node:child_process'; 3 | import * as util from 'node:util'; 4 | import { fileURLToPath } from 'node:url'; 5 | 6 | const execFile = util.promisify(child_process.execFile); 7 | 8 | const options = {cwd: fileURLToPath(new URL('../..', import.meta.url))}; 9 | const git = (...args) => execFile('git', args, options).then(({stdout}) => stdout.toString().trim()); 10 | 11 | const pkg = JSON.parse(await fs.readFile(new URL('../../package.json', import.meta.url), 'utf-8')); 12 | 13 | const [revision, branch_str, changed_files_str, tags_str, commit_count_str] = await Promise.all([ 14 | process.env.CI_COMMIT_SHA || git('rev-parse', 'HEAD'), 15 | process.env.CI_COMMIT_BRANCH || git('rev-parse', '--abbrev-ref', 'HEAD'), 16 | git('diff', '--name-only', 'HEAD'), 17 | git('log', '--tags', '--no-walk', '--pretty=%D'), 18 | git('rev-list', '--count', 'HEAD'), 19 | ]); 20 | 21 | const branch = branch_str && branch_str !== 'HEAD' ? branch_str : null; 22 | const changed_files = changed_files_str.length ? changed_files_str.split('\n') : []; 23 | const tags = tags_str.split(/\n|, /).filter(t => t.startsWith('tag: ')).map(t => t.substr(5)); 24 | const last_tagged_version = tags.find(t => t.startsWith('v'))?.substr(1) ?? null; 25 | const last_version = last_tagged_version ?? pkg.version; 26 | const commit_count = parseInt(commit_count_str); 27 | 28 | console.warn({ 29 | version: pkg.version, 30 | last_tagged_version, 31 | last_version, 32 | revision, 33 | branch, 34 | changed_files, 35 | tags, 36 | tags_str, 37 | commit_count, 38 | }); 39 | 40 | if (!last_tagged_version || pkg.version !== last_tagged_version) { 41 | console.warn('Last tagged version does not match package.json version', { 42 | version: pkg.version, 43 | tag: last_tagged_version ? 'v' + last_tagged_version : null, 44 | }); 45 | process.exit(); 46 | } 47 | 48 | const last_tagged_version_commit_count = parseInt(await git('rev-list', '--count', 'v' + last_version)); 49 | const commit_count_since_last_version = commit_count - last_tagged_version_commit_count; 50 | 51 | console.warn({ 52 | last_tagged_version_commit_count, 53 | commit_count_since_last_version, 54 | }); 55 | 56 | if (commit_count_since_last_version <= 0) { 57 | console.warn('No changes since last tagged version'); 58 | process.exit(); 59 | } 60 | 61 | const version = last_version + 62 | '-next.' + (commit_count_since_last_version - 1) + 63 | '+sha.' + revision.substr(0, 7); 64 | 65 | console.warn({ 66 | version, 67 | }); 68 | 69 | console.log(version); 70 | -------------------------------------------------------------------------------- /resources/build/ci-package-json.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | import * as child_process from 'node:child_process'; 3 | import * as util from 'node:util'; 4 | import { fileURLToPath } from 'node:url'; 5 | 6 | const execFile = util.promisify(child_process.execFile); 7 | const options = {cwd: fileURLToPath(new URL('../..', import.meta.url))}; 8 | const git = (...args) => execFile('git', args, options).then(({stdout}) => stdout.toString().trim()); 9 | 10 | const pkg = JSON.parse(await fs.readFile(new URL('../../package.json', import.meta.url), 'utf-8')); 11 | 12 | const [revision, branch, changed_files] = await Promise.all([ 13 | process.env.CI_COMMIT_SHA || git('rev-parse', 'HEAD'), 14 | process.env.CI_COMMIT_BRANCH || git('rev-parse', '--abbrev-ref', 'HEAD'), 15 | git('diff', '--name-only', 'HEAD'), 16 | ]); 17 | 18 | if (process.argv[2] === 'gitlab') { 19 | pkg.name = process.env.GITLAB_NPM_PACKAGE_NAME ?? pkg.name; 20 | pkg.publishConfig = {access: 'public'}; 21 | } 22 | 23 | if (process.argv[2] === 'github') { 24 | pkg.name = process.env.GITHUB_NPM_PACKAGE_NAME ?? pkg.name; 25 | pkg.repository = { 26 | type: 'git', 27 | url: 'https://github.com/' + process.env.GITHUB_REPOSITORY + '.git', 28 | }; 29 | pkg.publishConfig = {access: 'public'}; 30 | } 31 | 32 | pkg.version = process.env.VERSION || pkg.version; 33 | pkg.__nxapi_release = process.env.CI_COMMIT_TAG; 34 | 35 | if (process.argv[2] === 'docker') { 36 | pkg.__nxapi_docker = process.argv[3]; 37 | } 38 | 39 | pkg.__nxapi_git = pkg.__nxapi_git ?? { 40 | revision, 41 | branch: branch && branch !== 'HEAD' ? branch : null, 42 | changed_files: changed_files.length ? changed_files.split('\n') : [], 43 | }; 44 | 45 | await fs.writeFile(new URL('../../package.json', import.meta.url), JSON.stringify(pkg, null, 4) + '\n', 'utf-8'); 46 | -------------------------------------------------------------------------------- /resources/cli/fonts/opensans-normal-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/cli/fonts/opensans-normal-400.ttf -------------------------------------------------------------------------------- /resources/cli/fonts/opensans-normal-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/cli/fonts/opensans-normal-500.ttf -------------------------------------------------------------------------------- /resources/common/remote-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "require_version": [], 3 | "log_encryption_key": "E2Sii_7drCzK-68RsEoArmopiAIlZD_6TMA2F_UAAU0", 4 | "coral": { 5 | "znca_version": "2.12.0" 6 | }, 7 | "coral_auth": { 8 | "default": [ 9 | "nxapi", 10 | "https:\/\/nxapi-znca-api.fancy.org.uk\/api\/znca" 11 | ], 12 | "splatnet2statink": null, 13 | "flapg": null, 14 | "imink": {} 15 | }, 16 | "moon": { 17 | "znma_version": "1.18.0", 18 | "znma_build": "275" 19 | }, 20 | "coral_gws_nooklink": { 21 | "blanco_version": "2.1.1" 22 | }, 23 | "coral_gws_splatnet3": { 24 | "app_ver": "4.0.0-091d4283", 25 | "version": "4.0.0", 26 | "revision": "091d428399dc86fd3a7fc43d64bd33b8bd1e875d" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/discord-activity-splatoon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/discord-activity-splatoon3.png -------------------------------------------------------------------------------- /resources/discord-activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/discord-activity.png -------------------------------------------------------------------------------- /resources/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /data/android 4 | 5 | # Logs will be captured by Docker if enabled 6 | # This is set here so that running another process with `docker exec` (which 7 | # doesn't capture logs) will still write to a file by default 8 | export NXAPI_DEBUG_FILE=0 9 | 10 | exec /app/bin/nxapi.js --data-path /data "$@" 11 | -------------------------------------------------------------------------------- /resources/menu-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/menu-app.png -------------------------------------------------------------------------------- /resources/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samuelthomas2774/nxapi/4888544f11e14b2d9220ec85d880f72cab57e9e3/resources/notification.png -------------------------------------------------------------------------------- /src/api/splatnet3-types.ts: -------------------------------------------------------------------------------- 1 | /** /bullet_tokens */ 2 | export interface BulletToken { 3 | bulletToken: string; 4 | lang: string; 5 | is_noe_country: 'true' | 'false'; 6 | } 7 | -------------------------------------------------------------------------------- /src/api/util.ts: -------------------------------------------------------------------------------- 1 | import * as util from 'node:util'; 2 | import { Response as UndiciResponse } from 'undici'; 3 | 4 | export const ResponseSymbol = Symbol('Response'); 5 | const ErrorResponseSymbol = Symbol('IsErrorResponse'); 6 | 7 | export interface ResponseData { 8 | [ResponseSymbol]: R; 9 | } 10 | export type HasResponse = T & ResponseData; 11 | 12 | export function defineResponse(data: T, response: R) { 13 | Object.defineProperty(data, ResponseSymbol, {enumerable: false, value: response}); 14 | return data as HasResponse; 15 | } 16 | 17 | export class ErrorResponse extends Error { 18 | readonly body: string | undefined; 19 | readonly data: T | undefined = undefined; 20 | 21 | constructor( 22 | message: string, 23 | readonly response: Response | UndiciResponse, 24 | body?: string | ArrayBuffer | T 25 | ) { 26 | super(message); 27 | 28 | Object.defineProperty(this, ErrorResponseSymbol, {enumerable: false, value: ErrorResponseSymbol}); 29 | 30 | if (body instanceof ArrayBuffer) { 31 | body = (new TextDecoder()).decode(body); 32 | } 33 | 34 | if (typeof body === 'string') { 35 | this.body = body; 36 | try { 37 | this.data = body ? JSON.parse(body) : undefined; 38 | } catch (err) {} 39 | } else if (typeof body !== 'undefined') { 40 | this.data = body; 41 | } 42 | 43 | const stack = this.stack ?? (this.name + ': ' + message); 44 | const lines = stack.split('\n'); 45 | const head = lines.shift()!; 46 | 47 | Object.defineProperty(this, 'stack', { 48 | value: head + '\n' + 49 | ' from ' + response.url + ' (' + response.status + ' ' + response.statusText + ')\n' + 50 | ' ' + util.inspect(this.data ? this.data : this.body, { 51 | compact: true, 52 | maxStringLength: 100, 53 | }).replace(/\n/g, '\n ') + 54 | (lines.length ? '\n' + lines.join('\n') : ''), 55 | }); 56 | } 57 | 58 | static async fromResponse(response: UndiciResponse, message: string) { 59 | const body = await response.arrayBuffer(); 60 | 61 | return new this(message, response, body); 62 | } 63 | } 64 | 65 | Object.defineProperty(ErrorResponse, Symbol.hasInstance, { 66 | configurable: true, 67 | value: (instance: ErrorResponse) => { 68 | return instance && ErrorResponseSymbol in instance; 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /src/app/README.md: -------------------------------------------------------------------------------- 1 | Electron app 2 | --- 3 | 4 | The Electron app is bundled into ~4 files in `dist/app/bundle` using Rollup. The main process code is not bundled for development, but is when packaging the app at `dist/bundle` (with the command line executable at `dist/bundle/cli-bundle.js`). 5 | 6 | [electron.ts](electron.ts) exports all Electron APIs used in the main process. This is because the `electron` module doesn't actually exist - Electron patches the `require` function (but not the module importer). Additionally Electron does not support using a standard JavaScript module as the app entrypoint, so [app-entry.cts](app-entry.cts) (a CommonJS module) is used to import the actual app entrypoint after the `ready` event. 7 | 8 | Electron APIs used in renderer processes should be imported directly from the `electron` module as they are always bundled into a CommonJS module. 9 | 10 | Any files outside this directory must not depend on anything here, as this directory won't be included in the npm package. 11 | -------------------------------------------------------------------------------- /src/app/app-entry.cts: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | 3 | // Do anything that must be run before the app is ready... 4 | 5 | Promise.all([ 6 | // @ts-expect-error 7 | typeof __NXAPI_BUNDLE_APP_INIT__ !== 'undefined' ? import(__NXAPI_BUNDLE_APP_INIT__) : import('./app-init.js'), 8 | electron.app.whenReady(), 9 | ]) 10 | // @ts-expect-error 11 | .then(() => typeof __NXAPI_BUNDLE_APP_MAIN__ !== 'undefined' ? import(__NXAPI_BUNDLE_APP_MAIN__) : import('./main/index.js')) 12 | .then(m => m.init.call(null)) 13 | .catch(err => { 14 | electron.dialog.showErrorBox('Error during startup', err?.stack ?? err?.message ?? err); 15 | process.exit(1); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/app-init.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import { init as initDebug } from '../util/debug.js'; 3 | import { paths } from '../util/product.js'; 4 | 5 | await initDebug(join(paths.log, 'app')); 6 | -------------------------------------------------------------------------------- /src/app/browser/components/friend-code.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react'; 2 | import { StyleSheet, Text } from 'react-native'; 3 | import { CurrentUser } from '../../../api/coral-types.js'; 4 | import ipc from '../ipc.js'; 5 | 6 | export default function FriendCode(props: { 7 | friendcode: CurrentUser['links']['friendCode']; 8 | } | { 9 | id: string; 10 | }) { 11 | const friendcode = useMemo(() => 'friendcode' in props ? props.friendcode : { 12 | id: props.id, 13 | regenerable: false, 14 | regenerableAt: 0, 15 | }, ['friendcode' in props ? props.friendcode : null, 'id' in props ? props.id : null]); 16 | 17 | const onFriendCodeContextMenu = useCallback(() => { 18 | ipc.showFriendCodeMenu(friendcode); 19 | }, [ipc, friendcode]); 20 | 21 | return SW-{friendcode.id}; 26 | } 27 | 28 | const styles = StyleSheet.create({ 29 | friendCodeValue: { 30 | userSelect: 'all', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/browser/components/icons/add-outline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, Text } from 'react-native'; 3 | import { svg_styles } from './util.js'; 4 | 5 | const IconWeb = React.memo((props: { 6 | title?: string; 7 | }) => 8 | 9 | {props.title ? {props.title} : null} 10 | 11 | 12 | ); 13 | 14 | export default Platform.OS === 'web' ? IconWeb : React.memo(() => null); 15 | -------------------------------------------------------------------------------- /src/app/browser/components/icons/util.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const svg_styles: React.CSSProperties = { 4 | height: '1.2em', 5 | width: 'auto', 6 | fill: 'currentColor', 7 | alignSelf: 'center', 8 | verticalAlign: -4, 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/browser/components/icons/warning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Platform, Text } from 'react-native'; 3 | import { svg_styles } from './util.js'; 4 | 5 | const IconWeb = React.memo((props: { 6 | title?: string; 7 | }) => 8 | 9 | {props.title ? {props.title} : null} 10 | 11 | 12 | ); 13 | 14 | export default Platform.OS === 'web' ? IconWeb : React.memo(() => null); 15 | -------------------------------------------------------------------------------- /src/app/browser/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Button } from './button.js'; 2 | export { default as NintendoSwitchUser } from './nintendo-switch-user.js'; 3 | export { NintendoSwitchUsers } from './nintendo-switch-user.js'; 4 | export { default as FriendCode } from './friend-code.js'; 5 | -------------------------------------------------------------------------------- /src/app/browser/components/nintendo-switch-user.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, StyleSheet } from 'react-native'; 3 | import { CurrentUser, Friend } from '../../../api/coral-types.js'; 4 | 5 | export default function NintendoSwitchUser(props: { 6 | friend: Friend; 7 | nickname?: string; 8 | } | { 9 | user: CurrentUser; 10 | nickname?: string; 11 | }) { 12 | const user = 'friend' in props ? props.friend : props.user; 13 | 14 | return <> 15 | 17 | {' '} 18 | {user.name} 19 | {props.nickname && user.name !== props.nickname ? ' (' + props.nickname + ')' : ''} 20 | ; 21 | } 22 | 23 | export function NintendoSwitchUsers(props: { 24 | users: Parameters[0][]; 25 | }) { 26 | return <> 27 | {props.users.map((u, i) => 28 | {i === 0 ? '' : ', '} 29 | 30 | )} 31 | ; 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | userImage: { 36 | borderRadius: 8, 37 | // @ts-expect-error react-native-web 38 | verticalAlign: -3 as 'auto', 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/app/browser/constants.ts: -------------------------------------------------------------------------------- 1 | import ipc from './ipc.js'; 2 | 3 | export const NSO_COLOUR = '#e60012'; 4 | export const NSO_COLOUR_DARK: `${typeof NSO_COLOUR}e0` = `${NSO_COLOUR}e0`; 5 | export const DISCORD_COLOUR = '#5865f2'; 6 | 7 | export const BACKGROUND_COLOUR_MAIN_LIGHT = ipc.platform === 'win32' ? '#ffffff' : '#ececec'; 8 | export const BACKGROUND_COLOUR_MAIN_DARK = ipc.platform === 'win32' ? '#000000' : '#252424'; 9 | 10 | export const BACKGROUND_COLOUR_SECONDARY_LIGHT = '#ffffff'; 11 | export const BACKGROUND_COLOUR_SECONDARY_DARK = '#353535'; 12 | 13 | export const UPDATE_COLOUR = '#006064e0'; 14 | 15 | export const HIGHLIGHT_COLOUR_LIGHT = '#00000020'; 16 | export const HIGHLIGHT_COLOUR_DARK = '#ffffff20'; 17 | 18 | export const BORDER_COLOUR_LIGHT = '#00000020'; 19 | export const BORDER_COLOUR_DARK = '#00000080'; 20 | export const BORDER_COLOUR_SECONDARY_DARK = '#ffffff20'; 21 | 22 | export const TEXT_COLOUR_LIGHT = '#212121'; 23 | export const TEXT_COLOUR_DARK = '#f5f5f5'; 24 | export const TEXT_COLOUR_ACTIVE = '#3ba55d'; 25 | 26 | export const DEFAULT_ACCENT_COLOUR = NSO_COLOUR.substr(1).toUpperCase() + 'FF'; 27 | -------------------------------------------------------------------------------- /src/app/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import { config } from './ipc.js'; 3 | import App from './main/index.js'; 4 | import Friend from './friend/index.js'; 5 | import DiscordSetup from './discord/index.js'; 6 | import AddFriend from './add-friend/index.js'; 7 | import Preferences from './preferences/index.js'; 8 | import AddAccountManualPrompt from './add-account-manual/index.js'; 9 | 10 | AppRegistry.registerComponent('App', () => App); 11 | AppRegistry.registerComponent('Friend', () => Friend); 12 | AppRegistry.registerComponent('DiscordPresence', () => DiscordSetup); 13 | AppRegistry.registerComponent('AddFriend', () => AddFriend); 14 | AppRegistry.registerComponent('Preferences', () => Preferences); 15 | AppRegistry.registerComponent('AddAccountManualPrompt', () => AddAccountManualPrompt); 16 | 17 | const style = window.document.createElement('style'); 18 | 19 | style.textContent = ` 20 | :root { 21 | user-select: none; 22 | overflow-x: hidden; 23 | } 24 | *:focus-visible { 25 | outline-style: solid; 26 | outline-width: medium; 27 | } 28 | input, 29 | input:focus-visible { 30 | outline: none 0; 31 | } 32 | `; 33 | 34 | window.document.head.appendChild(style); 35 | 36 | const rootTag = window.document.createElement('div'); 37 | 38 | rootTag.style.minHeight = '100vh'; 39 | window.document.body.appendChild(rootTag); 40 | 41 | AppRegistry.runApplication(config.type, { 42 | rootTag, 43 | initialProps: config.props, 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/browser/ipc.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events'; 2 | import createDebug from 'debug'; 3 | import type { NxapiElectronIpc } from '../preload/index.js'; 4 | 5 | const debug = createDebug('app:browser:ipc'); 6 | 7 | declare global { 8 | interface Window { 9 | nxapiElectronIpc: NxapiElectronIpc; 10 | } 11 | } 12 | 13 | export const events = new EventEmitter(); 14 | events.setMaxListeners(0); 15 | 16 | const ipc = { 17 | ...window.nxapiElectronIpc, 18 | 19 | events, 20 | }; 21 | 22 | events.on('newListener', (event: string, listener: (...args: any[]) => void) => { 23 | ipc.registerEventListener(event, listener); 24 | }); 25 | events.on('removeListener', (event: string, listener: (...args: any[]) => void) => { 26 | ipc.removeEventListener(event, listener); 27 | }); 28 | 29 | export default ipc; 30 | 31 | export const config = ipc.getWindowData(); 32 | 33 | debug('Window configuration', config); 34 | -------------------------------------------------------------------------------- /src/app/browser/main/section.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 3 | import { useAccentColour, useColourScheme } from '../util.js'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { BORDER_COLOUR_LIGHT, BORDER_COLOUR_SECONDARY_DARK, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js'; 6 | import ipc from '../ipc.js'; 7 | import Warning from '../components/icons/warning.js'; 8 | 9 | export default function Section(props: React.PropsWithChildren<{ 10 | title: string; 11 | loading?: boolean; 12 | error?: Error; 13 | headerButtons?: React.ReactNode; 14 | }>) { 15 | const theme = useColourScheme() === 'light' ? light : dark; 16 | const accent_colour = useAccentColour(); 17 | const { t, i18n } = useTranslation('main_window', { keyPrefix: 'main_section' }); 18 | 19 | const showErrorDetails = useCallback(() => { 20 | alert(props.error); 21 | }, [props.error]); 22 | 23 | return 24 | 25 | {props.title} 26 | {props.loading ? : 28 | props.error ? 29 | 30 | : null} 31 | {props.headerButtons} 32 | 33 | 34 | {props.children} 35 | ; 36 | } 37 | 38 | export const HEADER_SIZE = ipc.platform === 'win32' ? 24 : 14; 39 | 40 | const styles = StyleSheet.create({ 41 | container: { 42 | marginBottom: ipc.platform === 'win32' ? 10 : 0, 43 | borderBottomWidth: ipc.platform === 'win32' ? 0 : 1, 44 | }, 45 | header: { 46 | paddingVertical: ipc.platform === 'win32' ? 20 : 16, 47 | paddingHorizontal: ipc.platform === 'win32' ? 24 : 20, 48 | flexDirection: 'row', 49 | }, 50 | headerText: { 51 | flex: 1, 52 | fontSize: HEADER_SIZE, 53 | }, 54 | activityIndicator: { 55 | marginLeft: 10, 56 | }, 57 | iconTouchable: { 58 | marginLeft: 10, 59 | }, 60 | icon: { 61 | fontSize: HEADER_SIZE, 62 | }, 63 | }); 64 | 65 | const light = StyleSheet.create({ 66 | container: { 67 | borderBottomColor: BORDER_COLOUR_LIGHT, 68 | }, 69 | text: { 70 | color: TEXT_COLOUR_LIGHT, 71 | }, 72 | }); 73 | 74 | const dark = StyleSheet.create({ 75 | container: { 76 | borderBottomColor: BORDER_COLOUR_SECONDARY_DARK, 77 | }, 78 | text: { 79 | color: TEXT_COLOUR_DARK, 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /src/app/browser/main/status-updates.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | import ipc from '../ipc.js'; 4 | import { useAccentColour, useEventListener } from '../util.js'; 5 | import type { StatusUpdate } from '../../../common/status.js'; 6 | import { BORDER_COLOUR_SECONDARY_DARK, TEXT_COLOUR_DARK, UPDATE_COLOUR } from '../constants.js'; 7 | import { Button } from '../components/index.js'; 8 | 9 | enum StatusUpdateFlag { 10 | HIDDEN = 0, 11 | } 12 | 13 | export default function StatusUpdates() { 14 | const accent_colour = useAccentColour(); 15 | 16 | const [status_updates, setStatusUpdateData] = useState(null); 17 | useEffect(() => (ipc.getStatusUpdateData().then(setStatusUpdateData), undefined), [ipc]); 18 | useEventListener(ipc.events, 'status-updates', setStatusUpdateData, [ipc.events]); 19 | 20 | return status_updates?.map(status_update => status_update.flags & (1 << StatusUpdateFlag.HIDDEN) ? null : 21 | {status_update.content} 22 | {status_update.action ? 23 |