├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── contributing │ └── guide.md └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── debug-ts.js ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── config │ └── test.json ├── index.ts └── lib │ ├── api │ ├── auth.spec.ts │ ├── auth.ts │ ├── live-services.spec.ts │ ├── live-services.ts │ ├── matchmaking.spec.ts │ ├── matchmaking.ts │ ├── prod-trackmania.spec.ts │ ├── prod-trackmania.ts │ ├── ubi-services.spec.ts │ └── ubi-services.ts │ └── main.ts ├── test-helper.js ├── tsconfig.json ├── tsconfig.module.json └── tslint.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files that will always have CRLF line endings on checkout. 2 | *.sln text eol=crlf 3 | 4 | # Set the default behavior, in case people don't have core.autocrlf set. 5 | * text=auto -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | 4 | 5 | * **What is the current behavior?** (You can also link to an open issue here) 6 | 7 | 8 | 9 | * **What is the new behavior (if this is a feature change)?** 10 | 11 | 12 | 13 | * **Other information**: 14 | -------------------------------------------------------------------------------- /.github/contributing/guide.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Get started 4 | 5 | First of all **[fork](https://guides.github.com/activities/forking/)** the repo, then if you dont already have, install **[nodejs](https://nodejs.org/en/download/)**. If you're running VSCode you should install TSlint and Prettier extensions. 6 | 7 | After that's done, go ahead and do `npm i` in the project root. 8 | 9 | Now, if you want to be able to run test's, you will need to get proper enviroment variables. 10 | 11 | Create `.env` in project root and add `EMAIL=your email` and `PASSWORD=your password`. 12 | 13 | Then do `npm run config:create` to create temporary credentials for the test to run from. 14 | 15 | Now you can watch and rebuild the project on save, and rerun relevant tests, do `npm run watch` 16 | 17 | Project has prehooks for commit and push to reset the test credentials file, you can do that manually by `npm run config:reset` 18 | 19 | If anything's unclear, package.json has the npm scripts and information about them. 20 | 21 | ## Features 22 | 23 | Now, for example if you wanted to add a new endpoint to the module, here's a quick run down how that would happen. 24 | 25 | First of all, file names go by the endpoint, for example prod.trackmania.core.nadeo.online is prod-trackmania. Test file will always be named {filename}.spec.ts. 26 | 27 | Then, to get started adding a endpoint, you should reference how any other endpoint is done. 28 | 29 | 1. Pick a endpoint you wish to add from the **[api documentation repo](https://github.com/The-Firexx/trackmania2020apidocumentation)** 30 | 2. Create test, console.log the response 31 | 3. Create function 32 | 4. Run the test (you can add test.only() to only run the relevant test) 33 | 5. From the response, create types. 34 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | release: 5 | types: [published] 6 | push: 7 | branches: 8 | - master 9 | - gh-actions 10 | pull_request: 11 | types: [review_requested, ready_for_review] 12 | branches: 13 | - master 14 | - gh-actions 15 | 16 | jobs: 17 | build: 18 | if: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }} 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | node-version: [14.x] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | - name: Set up Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v1 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | 34 | - name: Cache dependencies 35 | uses: actions/cache@v2 36 | with: 37 | path: | 38 | **/node_modules 39 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 40 | 41 | - name: Install dependencies 42 | run: npm i 43 | 44 | - name: Run the tests and generate coverage report 45 | run: npm run cov:actions 46 | env: 47 | EMAIL: ${{ secrets.EMAIL }} 48 | PASSWORD: ${{ secrets.PASSWORD }} 49 | 50 | - name: Upload coverage to Codecov 51 | uses: codecov/codecov-action@v1 52 | 53 | - name: Build docs 54 | run: npm run doc:html 55 | 56 | - name: Deploy draft to Netlify 57 | uses: South-Paw/action-netlify-deploy@v1.0.4 58 | with: 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | netlify-auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }} 61 | netlify-site-id: ${{ secrets.NETLIFY_SITE_ID }} 62 | build-dir: './build/docs' 63 | draft: true 64 | comment-on-commit: true 65 | docs: 66 | if: ${{ github.event_name == 'release' }} 67 | runs-on: ubuntu-latest 68 | 69 | strategy: 70 | matrix: 71 | node-version: [14.x] 72 | 73 | steps: 74 | - name: Checkout repository 75 | uses: actions/checkout@v2 76 | 77 | - name: Set up Node.js ${{ matrix.node-version }} 78 | uses: actions/setup-node@v1 79 | with: 80 | node-version: ${{ matrix.node-version }} 81 | 82 | - name: Cache dependencies 83 | uses: actions/cache@v2 84 | with: 85 | path: | 86 | **/node_modules 87 | key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} 88 | 89 | - name: Install dependencies 90 | run: npm i 91 | 92 | - name: Build docs 93 | run: npm run doc:html 94 | 95 | - name: Publish to netlify 96 | uses: South-Paw/action-netlify-deploy@v1.0.4 97 | with: 98 | github-token: ${{ secrets.GITHUB_TOKEN }} 99 | netlify-auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }} 100 | netlify-site-id: ${{ secrets.NETLIFY_SITE_ID }} 101 | build-dir: './build/docs' 102 | comment-on-commit: true 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | test 4 | src/**.js 5 | .idea/* 6 | 7 | coverage 8 | coverage-ts 9 | .nyc_output 10 | *.log 11 | 12 | yarn.lock 13 | 14 | .env -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | tsconfig.json 4 | tsconfig.module.json 5 | tslint.json 6 | .travis.yml 7 | .github 8 | .prettierignore 9 | .vscode 10 | build/docs 11 | **/*.spec.* 12 | coverage 13 | coverage-ts 14 | .nyc_output 15 | *.log 16 | .env 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | jsxBracketSameLine: true, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | tabWidth: 4, 6 | semi: false, 7 | bracketSpacing: true, 8 | arrowParens: 'avoid', 9 | printWidth: 90, 10 | endOfLine: 'crlf', 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/debug-ts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const meow = require('meow'); 3 | const path = require('path'); 4 | 5 | const tsFile = getTSFile(); 6 | const jsFile = TS2JS(tsFile); 7 | 8 | replaceCLIArg(tsFile, jsFile); 9 | 10 | // Ava debugger 11 | require('ava/profile'); 12 | 13 | /** 14 | * get ts file path from CLI args 15 | * 16 | * @return string path 17 | */ 18 | function getTSFile() { 19 | const cli = meow(); 20 | return cli.input[0]; 21 | } 22 | 23 | /** 24 | * get associated compiled js file path 25 | * 26 | * @param tsFile path 27 | * @return string path 28 | */ 29 | function TS2JS(tsFile) { 30 | const srcFolder = path.join(__dirname, '..', 'src'); 31 | const distFolder = path.join(__dirname, '..', 'build', 'main'); 32 | 33 | const tsPathObj = path.parse(tsFile); 34 | 35 | return path.format({ 36 | dir: tsPathObj.dir.replace(srcFolder, distFolder), 37 | ext: '.js', 38 | name: tsPathObj.name, 39 | root: tsPathObj.root 40 | }); 41 | } 42 | 43 | /** 44 | * replace a value in CLI args 45 | * 46 | * @param search value to search 47 | * @param replace value to replace 48 | * @return void 49 | */ 50 | function replaceCLIArg(search, replace) { 51 | process.argv[process.argv.indexOf(search)] = replace; 52 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [{ 4 | "type": "node", 5 | "request": "launch", 6 | "name": "Debug Project", 7 | // we test in `build` to make cleanup fast and easy 8 | "cwd": "${workspaceFolder}/build", 9 | // Replace this with your project root. If there are multiple, you can 10 | // automatically run the currently visible file with: "program": ${file}" 11 | "program": "${workspaceFolder}/src/cli/cli.ts", 12 | // "args": ["--no-install"], 13 | "outFiles": ["${workspaceFolder}/build/main/**/*.js"], 14 | "skipFiles": [ 15 | "/**/*.js", 16 | "${workspaceFolder}/node_modules/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: build", 19 | "stopOnEntry": true, 20 | "smartStep": true, 21 | "runtimeArgs": ["--nolazy"], 22 | "env": { 23 | "TYPESCRIPT_STARTER_REPO_URL": "${workspaceFolder}" 24 | }, 25 | "console": "externalTerminal" 26 | }, 27 | { 28 | "type": "node", 29 | "request": "launch", 30 | "name": "Debug Spec", 31 | "program": "${workspaceRoot}/.vscode/debug-ts.js", 32 | "args": ["${file}"], 33 | "skipFiles": ["/**/*.js"], 34 | // Consider using `npm run watch` or `yarn watch` for faster debugging 35 | // "preLaunchTask": "npm: build", 36 | // "smartStep": true, 37 | "runtimeArgs": ["--nolazy"] 38 | }] 39 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | // "typescript.implementationsCodeLens.enabled": true 4 | // "typescript.referencesCodeLens.enabled": true 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.0.27](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.26...v0.0.27) (2021-02-16) 6 | 7 | ### [0.0.26](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.25...v0.0.26) (2020-08-22) 8 | 9 | ### [0.0.25](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.24...v0.0.25) (2020-08-22) 10 | 11 | ### [0.0.24](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.23...v0.0.24) (2020-08-22) 12 | 13 | ### [0.0.23](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.22...v0.0.23) (2020-08-22) 14 | 15 | ### [0.0.22](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.20...v0.0.22) (2020-08-06) 16 | 17 | ### [0.0.21](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.20...v0.0.21) (2020-08-06) 18 | 19 | ### [0.0.20](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.19...v0.0.20) (2020-08-03) 20 | 21 | ### [0.0.19](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.18...v0.0.19) (2020-08-02) 22 | 23 | ### [0.0.18](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.10...v0.0.18) (2020-07-30) 24 | 25 | ### [0.0.17](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.16...v0.0.17) (2020-07-30) 26 | 27 | ### [0.0.16](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.15...v0.0.16) (2020-07-30) 28 | 29 | ### [0.0.15](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.14...v0.0.15) (2020-07-30) 30 | 31 | ### [0.0.14](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.10...v0.0.14) (2020-07-30) 32 | 33 | ### [0.0.13](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.12...v0.0.13) (2020-07-29) 34 | 35 | ### [0.0.12](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.10...v0.0.12) (2020-07-29) 36 | 37 | ### [0.0.11](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.10...v0.0.11) (2020-07-29) 38 | 39 | ### [0.0.10](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.9...v0.0.10) (2020-07-27) 40 | 41 | ### [0.0.9](https://github.com/breeku/trackmania2020-api-node/compare/v0.0.8...v0.0.9) (2020-07-27) 42 | 43 | ### 0.0.8 (2020-07-27) 44 | 45 | ### 0.0.7 (2020-07-27) 46 | 47 | ### 0.0.6 (2020-07-27) 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 breeku 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trackmania 2020 api node 2 | 3 | [![codecov](https://codecov.io/gh/breeku/trackmania-api-node/branch/master/graph/badge.svg)](https://codecov.io/gh/breeku/trackmania-api-node) 4 | 5 | ### [Converting this repo to a npm module](https://github.com/The-Firexx/trackmania2020apidocumentation) 6 | 7 | > "This API is not intended to be used outside of the game 8 | > 9 | > Be aware that Nadeo can change the API without even warning us, since the API is intended only to be used in game, so don't except any support for them, and don't try to make anything serious out of this 10 | > 11 | > Also, don't abuse of this API, has they have the right to lock your account since, for them, it's someone that is trying to hack or just trying to give bad performance to the servers" 12 | 13 | # Usage 14 | 15 | ## **[Docs](https://trackmania-api-node.netlify.app)** 16 | 17 | ### Nodejs 18 | 19 | `npm i trackmania-api-node` 20 | 21 | ```javascript 22 | const { loginUbi, loginTrackmaniaUbi, getTrophyCount } = require('trackmania-api-node') 23 | 24 | const login = async credentials => { 25 | try { 26 | const { ticket } = await loginUbi(credentials) // login to ubi, level 0 27 | return await loginTrackmaniaUbi(ticket) // login to trackmania, level 1 28 | } catch (e) { 29 | // axios error 30 | console.log(e.toJSON()) 31 | } 32 | } 33 | 34 | const getTrophies = async loggedIn => { 35 | const { accessToken, accountId, username } = loggedIn 36 | try { 37 | const trophyCount = await getTrophyCount(accessToken, accountId) 38 | console.log(username + ' trophies:') 39 | console.log(trophyCount) 40 | } catch (e) { 41 | // axios error 42 | console.log(e.toJSON()) 43 | } 44 | } 45 | 46 | ;(async () => { 47 | const credentials = Buffer.from('email' + ':' + 'password').toString('base64') 48 | const loggedIn = await login(credentials) 49 | if (loggedIn) await getTrophies(loggedIn) 50 | })() 51 | ``` 52 | 53 | ```javascript 54 | user trophies: 55 | { 56 | accountId: 'censored', 57 | points: 12345, 58 | t1Count: 70, 59 | t2Count: 200, 60 | t3Count: 80, 61 | t4Count: 2, 62 | t5Count: 5, 63 | t6Count: 1, 64 | t7Count: 0, 65 | t8Count: 0, 66 | t9Count: 0, 67 | timestamp: '2020-07-25T15:50:50+00:00' 68 | } 69 | ``` 70 | 71 | # Contributing 72 | 73 | [**Guide**](https://github.com/breeku/trackmania-api-node/blob/master/.github/contributing/guide.md) 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trackmania-api-node", 3 | "version": "0.0.27", 4 | "description": "npm module for Trackmania 2020 api", 5 | "main": "build/main/index.js", 6 | "typings": "build/main/index.d.ts", 7 | "module": "build/module/index.js", 8 | "repository": "https://github.com/breeku/trackmania2020-api-node", 9 | "license": "MIT", 10 | "keywords": [ 11 | "trackmania" 12 | ], 13 | "scripts": { 14 | "describe": "npm-scripts-info", 15 | "build": "run-s clean && run-p build:*", 16 | "build:main": "tsc -p tsconfig.json", 17 | "build:module": "tsc -p tsconfig.module.json", 18 | "fix": "run-s fix:*", 19 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 20 | "fix:tslint": "tslint --fix --project .", 21 | "test": "run-s build test:lint test:unit", 22 | "test:ci": "run-s build:main test:lint test:unit-ci", 23 | "test:lint": "tslint --project . && prettier --write \"src/**/*.ts\"", 24 | "test:unit": "run-s config:create test:verbose config:reset", 25 | "test:unit-ci": "run-s config:create test:silent config:reset", 26 | "test:verbose": "nyc --silent ava --verbose", 27 | "test:silent": "nyc --silent ava", 28 | "dev": "run-s clean build:main && run-p \"build:main -- -w\\", 29 | "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"nyc --silent ava --verbose --watch\"", 30 | "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", 31 | "cov:html": "nyc report --reporter=html", 32 | "cov:send": "nyc report --reporter=lcov && codecov", 33 | "cov:actions": "run-s test:ci && nyc report --reporter=lcov", 34 | "cov:check": "nyc report && nyc check-coverage --lines 100 --functions 100 --branches 100", 35 | "cov:typescript": "typescript-coverage-report", 36 | "doc": "run-s doc:html && open-cli build/docs/index.html", 37 | "doc:html": "typedoc src/ --exclude **/*.spec.ts --includeDeclarations --excludeExternals --stripInternal --target ES6 --mode file --out build/docs", 38 | "doc:json": "typedoc src/ --exclude **/*.spec.ts --includeDeclarations --excludeExternals --stripInternal --target ES6 --mode file --json build/docs/typedoc.json", 39 | "doc:publish": "gh-pages -m \"[ci skip] Updates\" -d build/docs", 40 | "version": "standard-version", 41 | "reset": "git clean -dfx && git reset --hard && npm i", 42 | "clean": "trash build test", 43 | "prepare-release": "run-s reset test cov:check doc:html version doc:publish", 44 | "ts-coverage": "typescript-coverage-report", 45 | "config:create": "node test-helper CREATE && npm run build:main", 46 | "config:reset": "node test-helper RESET && npm run build:main", 47 | "config:check": "node test-helper CHECK" 48 | }, 49 | "scripts-info": { 50 | "info": "Display information about the package scripts", 51 | "build": "Clean and rebuild the project", 52 | "fix": "Try to automatically fix any linting problems", 53 | "test": "Lint and unit test the project", 54 | "dev": "Watch and rebuild the project on save", 55 | "watch": "Watch and rebuild the project on save, then rerun relevant tests", 56 | "cov": "Rebuild, run tests, then create and open the coverage report", 57 | "doc": "Generate HTML API documentation and open it in a browser", 58 | "doc:json": "Generate API documentation in typedoc JSON format", 59 | "version": "Bump package.json version, update CHANGELOG.md, tag release", 60 | "reset": "Delete all untracked files and reset the repo to the last commit", 61 | "prepare-release": "One-step: clean, build, test, publish docs, and prep a release", 62 | "test file": "npx ava build/main/**/FILE.spec.js --verbose", 63 | "test by title": "npx ava build/main/**/*.spec.js --match='FIRST*LAST'", 64 | "config": "Create required json for tests" 65 | }, 66 | "engines": { 67 | "node": ">=8.9" 68 | }, 69 | "dependencies": { 70 | "axios": "^0.19.2", 71 | "jwt-decode": "^2.2.0" 72 | }, 73 | "devDependencies": { 74 | "@bitjson/npm-scripts-info": "^1.0.0", 75 | "@istanbuljs/nyc-config-typescript": "^1.0.1", 76 | "@types/jwt-decode": "^2.2.1", 77 | "ava": "3.10.1", 78 | "codecov": "^3.7.2", 79 | "cz-conventional-changelog": "^3.2.0", 80 | "dotenv": "^8.2.0", 81 | "envfile": "^6.11.0", 82 | "gh-pages": "^3.1.0", 83 | "husky": "^4.2.5", 84 | "npm-run-all": "^4.1.5", 85 | "nyc": "^15.1.0", 86 | "open-cli": "^6.0.1", 87 | "prettier": "^2.0.5", 88 | "standard-version": "^8.0.2", 89 | "trash-cli": "^3.0.0", 90 | "tslint": "^6.1.2", 91 | "tslint-config-prettier": "^1.18.0", 92 | "tslint-immutable": "^6.0.1", 93 | "typedoc": "^0.17.0-3", 94 | "typescript": "^3.9.7", 95 | "typescript-coverage-report": "^0.1.3" 96 | }, 97 | "ava": { 98 | "failFast": true, 99 | "files": [ 100 | "build/main/**/*.spec.js" 101 | ], 102 | "ignoredByWatcher": [ 103 | "src/**/*.ts", 104 | "build/main/**/*.d.*", 105 | "build/main/**/*.spec.d.*" 106 | ] 107 | }, 108 | "config": { 109 | "commitizen": { 110 | "path": "cz-conventional-changelog" 111 | } 112 | }, 113 | "nyc": { 114 | "extends": "@istanbuljs/nyc-config-typescript", 115 | "exclude": [ 116 | "**/*.spec.js" 117 | ] 118 | }, 119 | "husky": { 120 | "hooks": { 121 | "pre-commit": "npm run config:check", 122 | "pre-push": "npm run config:check" 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/config/test.json: -------------------------------------------------------------------------------- 1 | {"ticket":null,"lv1accessToken":null,"lv2accessToken":null,"accountId":null} -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/main' 2 | export * from './lib/api/auth' 3 | export * from './lib/api/prod-trackmania' 4 | export * from './lib/api/live-services' 5 | export * from './lib/api/ubi-services' 6 | -------------------------------------------------------------------------------- /src/lib/api/auth.spec.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | import anyTest, { TestInterface } from 'ava' 5 | 6 | import { loginUbi, loginTrackmaniaUbi, loginTrackmaniaNadeo, refreshTokens } from './auth' 7 | 8 | const test = anyTest as TestInterface<{ credentials: string }> 9 | 10 | test.before(async t => { 11 | const email = process.env.EMAIL 12 | const password = process.env.PASSWORD 13 | 14 | t.context.credentials = Buffer.from(email + ':' + password).toString('base64') 15 | }) 16 | 17 | test('login from level 0 to level 2, and refresh tokens', async t => { 18 | try { 19 | const { ticket } = await loginUbi(t.context.credentials) 20 | const { accessToken } = await loginTrackmaniaUbi(ticket) 21 | const { refreshToken } = await loginTrackmaniaNadeo( 22 | accessToken, 23 | 'NadeoLiveServices', 24 | ) 25 | const refreshedTokens = await refreshTokens(refreshToken) 26 | t.assert(refreshedTokens) 27 | } catch (err) { 28 | t.fail(JSON.stringify(err.response.data)) 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/lib/api/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import jwt_decode from 'jwt-decode' 4 | 5 | import { urls, setHeaders } from '../main' 6 | 7 | /** 8 | * Login to Ubisoft (level 0) 9 | * 10 | * @param {string} base64 - Base64 encoded email:password 11 | * 12 | */ 13 | export const loginUbi = async (base64: string): Promise => { 14 | const headers = setHeaders(base64, 'basic') 15 | 16 | const response = await axios({ 17 | url: urls.auth.ubisoft, 18 | method: 'post', 19 | headers, 20 | }) 21 | 22 | return response['data'] 23 | } 24 | 25 | /** 26 | * Login to Trackmania Ubisoft (level 1) 27 | * 28 | * @param {string} Ticket from loginUbi 29 | * 30 | */ 31 | export const loginTrackmaniaUbi = async (ticket: string): Promise => { 32 | const headers = setHeaders(ticket, 'ubi') 33 | 34 | const response = await axios({ 35 | url: urls.auth.trackmaniaUbi, 36 | method: 'POST', 37 | headers, 38 | }) 39 | const tokens: Itokens = response['data'] 40 | const decoded: jwt = jwt_decode(tokens['accessToken']) 41 | const result = { ...tokens, accountId: decoded.sub, username: decoded.aun } 42 | 43 | return result 44 | } 45 | 46 | /** 47 | * Login to Trackmania Nadeo (level 2) 48 | * 49 | * @param {string} Access token from loginTrackmaniaUbi 50 | * @param {string} Target API, NadeoLiveServices or NadeoClubServices 51 | * 52 | */ 53 | export const loginTrackmaniaNadeo = async ( 54 | accessToken: string, 55 | targetAPI: string, 56 | ): Promise => { 57 | const headers = setHeaders(accessToken, 'nadeo') 58 | 59 | const response = await axios({ 60 | url: urls.auth.trackmaniaNadeo, 61 | method: 'POST', 62 | data: JSON.stringify({ audience: targetAPI }), 63 | headers, 64 | }) 65 | const tokens: Itokens = response['data'] 66 | const decoded: jwt = jwt_decode(tokens['accessToken']) 67 | const result = { ...tokens, accountId: decoded.sub, username: decoded.aun } 68 | 69 | return result 70 | } 71 | 72 | /** 73 | * Refresh tokens 74 | * 75 | * @param {string} Refresh token from loginTrackmaniaUbi 76 | * 77 | */ 78 | export const refreshTokens = async (refreshToken: string): Promise => { 79 | const headers = setHeaders(refreshToken, 'nadeo') 80 | 81 | const response = await axios({ 82 | url: urls.auth.refreshToken, 83 | method: 'POST', 84 | headers, 85 | }) 86 | 87 | return response['data'] 88 | } 89 | 90 | export interface IloginUbi { 91 | platformType: string 92 | ticket: string 93 | twoFactorAuthenticationTicket: boolean 94 | profileId: string 95 | userId: string 96 | nameOnPlatform: string 97 | environment: string 98 | expiration: string 99 | spaceId: string 100 | clientIp: string 101 | clientIpCountry: string 102 | serverTime: string 103 | sessionId: string 104 | sessionKey: string 105 | rememberMeTicket: null | string 106 | } 107 | 108 | export interface IloginTrackmania { 109 | accessToken: string 110 | refreshToken: string 111 | accountId: string 112 | username: string 113 | } 114 | 115 | export interface Itokens { 116 | accessToken: string 117 | refreshToken: string 118 | } 119 | 120 | /** @internal */ 121 | type jwt = { 122 | [key: string]: string 123 | } 124 | -------------------------------------------------------------------------------- /src/lib/api/live-services.spec.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | import anyTest, { TestInterface } from 'ava' 5 | 6 | import { 7 | getSeasons, 8 | getTOTDs, 9 | getClubCampaigns, 10 | getMyGroupRecords, 11 | getMyPositionGroup, 12 | getTopPlayersGroup, 13 | getTopPlayersMap, 14 | getTopGroupPlayersMap, 15 | getSurroundingPlayersMap, 16 | getClubRooms, 17 | getArcadeRooms, 18 | getClubs, 19 | getClubMembers, 20 | getPlayerRankings, 21 | getLeaderboardsAroundScore, 22 | } from './live-services' 23 | 24 | import credentials from '../../config/test.json' 25 | 26 | const test = anyTest as TestInterface<{ 27 | account: { lv2liveAccessToken: string; accountId: string } 28 | }> 29 | 30 | test.before(async t => { 31 | const { accountId, lv2liveAccessToken } = (credentials as unknown) as { 32 | lv2liveAccessToken: null | string 33 | accountId: null | string 34 | } 35 | if (accountId && lv2liveAccessToken) 36 | t.context.account = { lv2liveAccessToken, accountId } 37 | }) 38 | 39 | test('Get all seasons', async t => { 40 | try { 41 | const response = await getSeasons(t.context.account.lv2liveAccessToken) 42 | t.assert(response) 43 | } catch (err) { 44 | t.fail(JSON.stringify(err.response.data)) 45 | } 46 | }) 47 | 48 | test('Get all TOTDs', async t => { 49 | try { 50 | const response = await getTOTDs(t.context.account.lv2liveAccessToken) 51 | t.assert(response) 52 | } catch (err) { 53 | t.fail(JSON.stringify(err.response.data)) 54 | } 55 | }) 56 | 57 | test('List club campaigns', async t => { 58 | try { 59 | const response = await getClubCampaigns(t.context.account.lv2liveAccessToken) 60 | t.assert(response) 61 | } catch (err) { 62 | t.fail(JSON.stringify(err.response.data)) 63 | } 64 | }) 65 | 66 | test('Get my group records', async t => { 67 | try { 68 | const response = await getMyGroupRecords( 69 | t.context.account.lv2liveAccessToken, 70 | '3987d489-03ae-4645-9903-8f7679c3a418', 71 | ) 72 | t.assert(response) 73 | } catch (err) { 74 | t.fail(JSON.stringify(err.response.data)) 75 | } 76 | }) 77 | 78 | test('Get my position in a group', async t => { 79 | try { 80 | const response = await getMyPositionGroup( 81 | t.context.account.lv2liveAccessToken, 82 | '3987d489-03ae-4645-9903-8f7679c3a418', 83 | ) 84 | t.assert(response) 85 | } catch (err) { 86 | t.fail(JSON.stringify(err.response.data)) 87 | } 88 | }) 89 | 90 | test('Get top players from a group', async t => { 91 | try { 92 | const response = await getTopPlayersGroup( 93 | t.context.account.lv2liveAccessToken, 94 | '3987d489-03ae-4645-9903-8f7679c3a418', 95 | ) 96 | t.assert(response) 97 | } catch (err) { 98 | t.fail(JSON.stringify(err.response.data)) 99 | } 100 | }) 101 | 102 | test('Get top players from a group and a map', async t => { 103 | try { 104 | const response = await getTopGroupPlayersMap( 105 | t.context.account.lv2liveAccessToken, 106 | '3987d489-03ae-4645-9903-8f7679c3a418', 107 | 'XJ_JEjWGoAexDWe8qfaOjEcq5l8', 108 | ) 109 | 110 | t.assert(response) 111 | } catch (err) { 112 | t.fail(JSON.stringify(err.response.data)) 113 | } 114 | }) 115 | 116 | test('Get leaderboards around a score', async t => { 117 | try { 118 | const response = await getLeaderboardsAroundScore( 119 | t.context.account.lv2liveAccessToken, 120 | '3987d489-03ae-4645-9903-8f7679c3a418', 121 | 'XJ_JEjWGoAexDWe8qfaOjEcq5l8', 122 | 19598, 123 | ) 124 | t.assert(response) 125 | } catch (err) { 126 | t.fail(JSON.stringify(err.response.data)) 127 | } 128 | }) 129 | 130 | test('Get top players from a map', async t => { 131 | try { 132 | const response = await getTopPlayersMap( 133 | t.context.account.lv2liveAccessToken, 134 | 'XJ_JEjWGoAexDWe8qfaOjEcq5l8', 135 | ) 136 | t.assert(response) 137 | } catch (err) { 138 | t.fail(JSON.stringify(err.response.data)) 139 | } 140 | }) 141 | 142 | test('Get surrounding players from a map', async t => { 143 | try { 144 | const response = await getSurroundingPlayersMap( 145 | t.context.account.lv2liveAccessToken, 146 | 'XJ_JEjWGoAexDWe8qfaOjEcq5l8', 147 | ) 148 | t.assert(response) 149 | } catch (err) { 150 | t.fail(JSON.stringify(err.response.data)) 151 | } 152 | }) 153 | 154 | test('Get club rooms', async t => { 155 | try { 156 | const response = await getClubRooms(t.context.account.lv2liveAccessToken) 157 | t.assert(response) 158 | } catch (err) { 159 | t.fail(JSON.stringify(err.response.data)) 160 | } 161 | }) 162 | 163 | test('Get arcade rooms', async t => { 164 | try { 165 | const response = await getArcadeRooms(t.context.account.lv2liveAccessToken) 166 | t.assert(response) 167 | } catch (err) { 168 | t.fail(JSON.stringify(err.response.data)) 169 | } 170 | }) 171 | 172 | test('Get clubs', async t => { 173 | try { 174 | const response = await getClubs(t.context.account.lv2liveAccessToken) 175 | t.assert(response) 176 | } catch (err) { 177 | t.fail(JSON.stringify(err.response.data)) 178 | } 179 | }) 180 | 181 | test('Get club members', async t => { 182 | try { 183 | const response = await getClubMembers(t.context.account.lv2liveAccessToken, 1) 184 | t.assert(response) 185 | } catch (err) { 186 | t.fail(JSON.stringify(err.response.data)) 187 | } 188 | }) 189 | 190 | test('Get player rankings', async t => { 191 | try { 192 | const response = await getPlayerRankings(t.context.account.lv2liveAccessToken, [ 193 | 'a9cbdeff-daa3-4bc2-998c-b2838183fb97', 194 | '531a9861-c024-4063-9b29-14601350b899', 195 | '2ed0997d-62bc-4a53-8c09-ffb793382ea2', 196 | ]) 197 | t.assert(response) 198 | } catch (err) { 199 | t.fail(JSON.stringify(err.response.data)) 200 | } 201 | }) 202 | -------------------------------------------------------------------------------- /src/lib/api/live-services.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { urls, setHeaders } from '../main' 4 | 5 | /** 6 | * Obtain all the seasons, including seasonsIds, groupIds, mapIds, etc. 7 | * 8 | * ## **Requires level 2 authentication** 9 | * 10 | * @category level 2 11 | * @param {string} accessToken - Access token 12 | * @param {number} offset - Offset (default = 0) 13 | * @param {number} length - Length (default = 1) 14 | * 15 | */ 16 | export const getSeasons = async ( 17 | accessToken: string, 18 | offset: number = 0, 19 | length: number = 1, 20 | ): Promise => { 21 | const headers = setHeaders(accessToken, 'nadeo') 22 | const response = await axios({ 23 | url: 24 | urls.liveServices + 25 | '/api/token/campaign/official?offset=' + 26 | offset + 27 | '&length=' + 28 | length, 29 | method: 'GET', 30 | headers, 31 | }) 32 | 33 | return response['data'] 34 | } 35 | 36 | /** 37 | * Obtain all the track of the days, with their respective mapUid and seasonUid 38 | * 39 | * ## **Requires level 2 authentication** 40 | * 41 | * @category level 2 42 | * @param {string} accessToken - Access token 43 | * @param {number} offset - Offset (default = 0) 44 | * @param {number} length - Length (default = 1) 45 | * 46 | */ 47 | export const getTOTDs = async ( 48 | accessToken: string, 49 | offset: number = 0, 50 | length: number = 1, 51 | ): Promise => { 52 | const headers = setHeaders(accessToken, 'nadeo') 53 | const response = await axios({ 54 | url: 55 | urls.liveServices + 56 | '/api/token/campaign/month?offset=' + 57 | offset + 58 | '&length=' + 59 | length, 60 | method: 'GET', 61 | headers, 62 | }) 63 | 64 | return response['data'] 65 | } 66 | 67 | /** 68 | * List clubs campaigns (used when we are in Solo Screen and looking for Club Campaigns) 69 | * 70 | * ## **Requires level 2 authentication** 71 | * 72 | * @category level 2 73 | * @param {string} accessToken - Access token 74 | * @param {number} offset - Offset (default = 0) 75 | * @param {number} length - Length (default = 75) 76 | * @param {string} sort - Sort (default = 'popularity') 77 | * @param {string} order - Order (default = 'DESC') 78 | * 79 | */ 80 | export const getClubCampaigns = async ( 81 | accessToken: string, 82 | offset: number = 0, 83 | length: number = 75, 84 | sort: string = 'popularity', 85 | order: string = 'DESC', 86 | ): Promise => { 87 | const headers = setHeaders(accessToken, 'nadeo') 88 | const response = await axios({ 89 | url: 90 | urls.liveServices + 91 | '/api/token/club/campaign?offset=' + 92 | offset + 93 | '&length=' + 94 | length + 95 | '&sort=' + 96 | sort + 97 | '&order=' + 98 | order, 99 | method: 'GET', 100 | headers, 101 | }) 102 | 103 | return response['data'] 104 | } 105 | 106 | /** 107 | * Returns your record in everymap of that group, and your position in each zone 108 | * 109 | * ## **Requires level 2 authentication** 110 | * 111 | * @category level 2 112 | * @param {string} accessToken - Access token 113 | * @param {string} groupUid - Group uid 114 | * 115 | */ 116 | export const getMyGroupRecords = async ( 117 | accessToken: string, 118 | groupUid: string, 119 | ): Promise => { 120 | const headers = setHeaders(accessToken, 'nadeo') 121 | const response = await axios({ 122 | url: urls.liveServices + '/api/token/leaderboard/group/' + groupUid + '/map', 123 | method: 'GET', 124 | headers, 125 | }) 126 | 127 | return response['data'] 128 | } 129 | 130 | /** 131 | * Get your position in a group (ex: overall ranking on Summer Season 2020 in world, country, etc.) 132 | * 133 | * ## **Requires level 2 authentication** 134 | * 135 | * @category level 2 136 | * @param {string} accessToken - Access token 137 | * @param {string} groupUid - Group uid 138 | * 139 | */ 140 | export const getMyPositionGroup = async ( 141 | accessToken: string, 142 | groupUid: string, 143 | ): Promise => { 144 | const headers = setHeaders(accessToken, 'nadeo') 145 | const response = await axios({ 146 | url: urls.liveServices + '/api/token/leaderboard/group/' + groupUid, 147 | method: 'GET', 148 | headers, 149 | }) 150 | 151 | return response['data'] 152 | } 153 | 154 | /** 155 | * Get the top leaders on a group (ex: top rankings on Summer Season 2020 in world, country, etc.) 156 | * 157 | * ## **Requires level 2 authentication** 158 | * 159 | * @category level 2 160 | * @param {string} accessToken - Access token 161 | * @param {string} groupUid - Group uid 162 | * 163 | */ 164 | export const getTopPlayersGroup = async ( 165 | accessToken: string, 166 | groupUid: string, 167 | ): Promise => { 168 | const headers = setHeaders(accessToken, 'nadeo') 169 | const response = await axios({ 170 | url: urls.liveServices + '/api/token/leaderboard/group/' + groupUid + '/top', 171 | method: 'GET', 172 | headers, 173 | }) 174 | 175 | return response['data'] 176 | } 177 | 178 | /** 179 | * Obtain the top players on a specific map from a specific group 180 | * 181 | * ## **Requires level 2 authentication** 182 | * 183 | * @category level 2 184 | * @param {string} accessToken - Access token 185 | * @param {string} groupUid - Group uid 186 | * @param {string} mapUid - Map uid 187 | * 188 | */ 189 | export const getTopGroupPlayersMap = async ( 190 | accessToken: string, 191 | groupUid: string, 192 | mapUid: string, 193 | ): Promise => { 194 | const headers = setHeaders(accessToken, 'nadeo') 195 | const response = await axios({ 196 | url: 197 | urls.liveServices + 198 | '/api/token/leaderboard/group/' + 199 | groupUid + 200 | '/map/' + 201 | mapUid + 202 | '/top', 203 | method: 'GET', 204 | headers, 205 | }) 206 | 207 | return response['data'] 208 | } 209 | 210 | /** 211 | * Obtain the leaderboards around a score 212 | * 213 | * ## **Requires level 2 authentication** 214 | * 215 | * @category level 2 216 | * @param {string} accessToken - Access token 217 | * @param {string} groupUid - Group uid 218 | * @param {string} mapUid - Map uid 219 | * @param {number} score - Score 220 | * 221 | */ 222 | export const getLeaderboardsAroundScore = async ( 223 | accessToken: string, 224 | groupUid: string, 225 | mapUid: string, 226 | score: number, 227 | ): Promise => { 228 | const headers = setHeaders(accessToken, 'nadeo') 229 | const response = await axios({ 230 | url: 231 | urls.liveServices + 232 | '/api/token/leaderboard/group/' + 233 | groupUid + 234 | '/map/' + 235 | mapUid + 236 | '/surround/0/50?score=' + 237 | score, 238 | method: 'GET', 239 | headers, 240 | }) 241 | 242 | return response['data'] 243 | } 244 | 245 | /** 246 | * Get the top players of a map, with no restriction of a group like the others endpoints 247 | * 248 | * ## **Requires level 2 authentication** 249 | * 250 | * @category level 2 251 | * @param {string} accessToken - Access token 252 | * @param {string} mapUid - Map uid 253 | * 254 | */ 255 | export const getTopPlayersMap = async ( 256 | accessToken: string, 257 | mapUid: string, 258 | ): Promise => { 259 | const headers = setHeaders(accessToken, 'nadeo') 260 | const response = await axios({ 261 | url: 262 | urls.liveServices + 263 | '/api/token/leaderboard/group/Personal_Best/map/' + 264 | mapUid + 265 | '/top', 266 | method: 'GET', 267 | headers, 268 | }) 269 | 270 | return response['data'] 271 | } 272 | 273 | /** 274 | * Get the surrounding players around your score on a map, with no restriction of a group like the others endpoints (this is used in that little leaderboard in game) 275 | * 276 | * ## **Requires level 2 authentication** 277 | * 278 | * @category level 2 279 | * @param {string} accessToken - Access token 280 | * @param {string} mapUid - Map uid 281 | * 282 | */ 283 | export const getSurroundingPlayersMap = async ( 284 | accessToken: string, 285 | mapUid: string, 286 | ): Promise => { 287 | const headers = setHeaders(accessToken, 'nadeo') 288 | const response = await axios({ 289 | url: 290 | urls.liveServices + 291 | '/api/token/leaderboard/group/Personal_Best/map/' + 292 | mapUid + 293 | '/surround/1/1', 294 | method: 'GET', 295 | headers, 296 | }) 297 | 298 | return response['data'] 299 | } 300 | 301 | /** 302 | * This is used to obtain the clubs in the Live section of the game 303 | * 304 | * ## **Requires level 2 authentication** 305 | * 306 | * @category level 2 307 | * @param {string} accessToken - Access token 308 | * @param {number} offset - Offset (default = 0) 309 | * @param {number} length - Length (default = 75) 310 | * @param {string} sort - Sort (default = 'popularity') 311 | * @param {string} order - Order (default = 'DESC') 312 | * 313 | */ 314 | export const getClubRooms = async ( 315 | accessToken: string, 316 | offset: number = 0, 317 | length: number = 75, 318 | sort: string = 'popularity', 319 | order: string = 'DESC', 320 | ): Promise => { 321 | const headers = setHeaders(accessToken, 'nadeo') 322 | const response = await axios({ 323 | url: 324 | urls.liveServices + 325 | '/api/token/club/room?offset=' + 326 | offset + 327 | '&length=' + 328 | length + 329 | '&sort=' + 330 | sort + 331 | '&order=' + 332 | order, 333 | method: 'GET', 334 | headers, 335 | }) 336 | 337 | return response['data'] 338 | } 339 | 340 | /** 341 | * Obtain the information about the current arcade room and the next room 342 | * 343 | * ## **Requires level 2 authentication** 344 | * 345 | * @category level 2 346 | * @param {string} accessToken - Access token 347 | * 348 | */ 349 | export const getArcadeRooms = async (accessToken: string): Promise => { 350 | const headers = setHeaders(accessToken, 'nadeo') 351 | const response = await axios({ 352 | url: urls.liveServices + '/api/token/channel/officialhard', 353 | method: 'GET', 354 | headers, 355 | }) 356 | 357 | return response['data'] 358 | } 359 | 360 | /** 361 | * List all the clubs in the club section 362 | * 363 | * ## **Requires level 2 authentication** 364 | * 365 | * @category level 2 366 | * @param {string} accessToken - Access token 367 | * @param {number} offset - Offset (default = 0) 368 | * @param {number} length Length (default = 90) 369 | * 370 | */ 371 | export const getClubs = async ( 372 | accessToken: string, 373 | offset: number = 0, 374 | length: number = 90, 375 | ): Promise => { 376 | const headers = setHeaders(accessToken, 'nadeo') 377 | const response = await axios({ 378 | url: 379 | urls.liveServices + 380 | '/api/token/club/mine?offset=' + 381 | offset + 382 | '&length=' + 383 | length, 384 | method: 'GET', 385 | headers, 386 | }) 387 | 388 | return response['data'] 389 | } 390 | 391 | /** 392 | * Obtain all the information about the members of a club 393 | * 394 | * ## **Requires level 2 authentication** 395 | * 396 | * @category level 2 397 | * @param {string} accessToken - Access token 398 | * @param {number} clubId - Clubid 399 | * @param {number} offset - Offset (default = 0) 400 | * @param {number} length - Length (default = 27) 401 | * 402 | */ 403 | export const getClubMembers = async ( 404 | accessToken: string, 405 | clubId: number, 406 | offset: number = 0, 407 | length: number = 27, 408 | ): Promise => { 409 | const headers = setHeaders(accessToken, 'nadeo') 410 | const response = await axios({ 411 | url: 412 | urls.liveServices + 413 | '/api/token/club/' + 414 | clubId + 415 | '/member?offset=' + 416 | offset + 417 | '&length=' + 418 | length, 419 | method: 'GET', 420 | headers, 421 | }) 422 | 423 | return response['data'] 424 | } 425 | 426 | /** 427 | * Obtain player rankings 428 | * 429 | * ## **Requires level 2 authentication** 430 | * 431 | * @category level 2 432 | * @param {string} accessToken - Access token 433 | * @param {string[]} accountIds - Account ids 434 | * 435 | */ 436 | export const getPlayerRankings = async (accessToken: string, accountIds: string[]) => { 437 | const obj = accountIds.map(id => { 438 | return { accountId: id } 439 | }) 440 | const headers = setHeaders(accessToken, 'nadeo') 441 | const response = await axios({ 442 | url: urls.liveServices + '/api/token/leaderboard/trophy/player', 443 | method: 'POST', 444 | data: { 445 | listPlayer: obj, 446 | }, 447 | headers, 448 | }) 449 | 450 | return response['data'] 451 | } 452 | 453 | export interface IallSeasons { 454 | campaignList: campaign[] 455 | itemCount: number 456 | } 457 | 458 | type campaign = { 459 | id: number 460 | seasonUid: string 461 | name: string 462 | color: string 463 | useCase: number 464 | clubId: unknown 465 | leaderboardGroupUid: string 466 | publicationTimestamp: number 467 | publishedDate: number 468 | year: number 469 | week: number 470 | day: number 471 | monthYear: number 472 | month: number 473 | monthDay: number 474 | published: boolean 475 | playlist: playlist[] 476 | latestSeasons: latestSeason[] 477 | categories: category[] 478 | media: media 479 | } 480 | 481 | type playlist = { 482 | id: number 483 | position: number 484 | mapUid: string 485 | } 486 | type latestSeason = { 487 | uid: string 488 | name: string 489 | startTimestamp: number 490 | startDate: number 491 | endTimestamp: number 492 | endDate: number 493 | relativeStart: number 494 | relativeEnd: number 495 | campaignId: number 496 | active: boolean 497 | } 498 | 499 | type category = { 500 | position: number 501 | length: number 502 | name: string 503 | } 504 | 505 | type media = { 506 | buttonBackgroundUrl: string 507 | buttonForegroundUrl: string 508 | popUpBackgroundUrl: string 509 | popUpImageUrl: string 510 | liveButtonBackgroundUrl: string 511 | } 512 | 513 | export interface ITOTDs { 514 | monthList: month[] 515 | itemCount: number 516 | } 517 | 518 | type month = { 519 | year: number 520 | month: number 521 | lastDay: number 522 | days: day[] 523 | media: media 524 | } 525 | 526 | type day = { 527 | campaignId: number 528 | mapUid: string 529 | day: number 530 | monthDay: number 531 | seasonUid: string 532 | relativeStart: number 533 | relativeEnd: number 534 | leaderboardGroup: unknown 535 | } 536 | 537 | export interface IclubCampaigns { 538 | clubCampaignList: clubCampaign[] 539 | maxPage: number 540 | itemCount: number 541 | } 542 | 543 | type clubCampaign = { 544 | clubId: number 545 | clubIconUrl: string 546 | clubDecalUrl: string 547 | campaignId: number 548 | publicationTimestamp: number 549 | publishedOn: number 550 | creationTimestamp: number 551 | activityId: number 552 | mediaUrl: string 553 | name: string 554 | campaign: campaign 555 | popularityLevel: number 556 | } 557 | 558 | export interface IgroupRecords { 559 | [key: string]: record 560 | } 561 | 562 | type record = { 563 | groupUid: string 564 | mapUid: string 565 | score: number 566 | zones: zone[] 567 | } 568 | 569 | type zone = { 570 | zoneId: string 571 | zoneName: string 572 | ranking: ranking 573 | } 574 | 575 | type ranking = { 576 | position: number 577 | length: number 578 | } 579 | 580 | export interface IpositionGroup { 581 | groupUid: string 582 | sp: number 583 | zones: zone[] 584 | } 585 | 586 | export interface IgroupTopPlayers { 587 | groupUid: string 588 | tops: tops[] 589 | } 590 | 591 | type tops = { 592 | zoneId: string 593 | zoneName: string 594 | top: top[] 595 | } 596 | 597 | type top = { 598 | accountId: string 599 | zoneId: string 600 | zoneName: string 601 | position: number 602 | sp?: number 603 | score?: number 604 | } 605 | 606 | export interface ImapTopPlayer { 607 | groupUid: string 608 | mapUid: string 609 | tops: tops[] 610 | } 611 | 612 | export interface IclubRooms { 613 | clubRoomList: clubRoom[] 614 | maxPage: number 615 | itemCount: number 616 | } 617 | 618 | type clubRoom = { 619 | id: number 620 | clubId: number 621 | nadeo: boolean 622 | roomId: number 623 | campaignId: unknown 624 | playerServerLogin: unknown 625 | activityId: number 626 | mediaUrl: string 627 | name: string 628 | room: room 629 | popularityLevel: number 630 | creationTimestamp: number 631 | } 632 | 633 | type room = { 634 | id: number 635 | name: string 636 | region: unknown 637 | serverAccountId: string 638 | maxPlayers: number 639 | playerCount: number 640 | maps: string[] 641 | script: string 642 | scriptSettings: scriptSettings 643 | } 644 | 645 | type scriptSettings = { 646 | S_ForceLapsNb: { key: string; value: string; type: string } 647 | S_DecoImageUrl_Screen16x9: { 648 | key: string 649 | value: string 650 | type: string 651 | } 652 | } 653 | 654 | export interface IarcadeRooms { 655 | uid: string 656 | name: string 657 | playerCount: number 658 | currentTimeSlot: timeSlot 659 | nextTimeSlot: timeSlot 660 | } 661 | 662 | type timeSlot = { 663 | startTimestamp: number 664 | endTimestamp: number 665 | name: string 666 | maps: string[] 667 | currentMap: string 668 | relativeStart: number 669 | relativeEnd: number 670 | mediaUrl: string 671 | } 672 | 673 | export interface Iclubs { 674 | clubList: unknown[] 675 | maxPage: number 676 | clubCount: number 677 | } 678 | 679 | export interface IclubMembers { 680 | clubMemberList: clubMember[] 681 | maxPage: number 682 | itemCount: number 683 | } 684 | 685 | type clubMember = { 686 | accountId: string 687 | role: string 688 | creationTimestamp: number 689 | creationDate: number 690 | vip: boolean 691 | } 692 | 693 | export interface IleaderboardsAroundScore { 694 | groupUid: string 695 | mapUid: string 696 | levels: level[] 697 | } 698 | 699 | type level = { 700 | zoneId: string 701 | zoneName: string 702 | level: top[] 703 | } 704 | -------------------------------------------------------------------------------- /src/lib/api/matchmaking.spec.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | import anyTest, { TestInterface } from 'ava' 5 | 6 | import { getMatchmakingRanks } from './matchmaking' 7 | 8 | import credentials from '../../config/test.json' 9 | 10 | const test = anyTest as TestInterface<{ 11 | account: { lv2clubAccessToken: string; accountId: string } 12 | }> 13 | 14 | test.before(async t => { 15 | const { accountId, lv2clubAccessToken } = (credentials as unknown) as { 16 | lv2clubAccessToken: null | string 17 | accountId: null | string 18 | } 19 | 20 | if (lv2clubAccessToken && accountId) 21 | t.context.account = { lv2clubAccessToken, accountId } 22 | }) 23 | 24 | test('Get matchmaking rank', async t => { 25 | try { 26 | const response = await getMatchmakingRanks(t.context.account.lv2clubAccessToken, [ 27 | 'a9cbdeff-daa3-4bc2-998c-b2838183fb97', 28 | '2ed0997d-62bc-4a53-8c09-ffb793382ea2', 29 | ]) 30 | t.assert(response) 31 | } catch (err) { 32 | console.error(err) 33 | t.fail(JSON.stringify(err.response.data)) 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/lib/api/matchmaking.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { urls, setHeaders } from '../main' 4 | 5 | /** 6 | * Get matchmaking rank of player(s) 7 | * 8 | * ## **Requires level 2 authentication with audience: NadeoClubServices** 9 | * 10 | * @category level 2 11 | * @param {string} accessToken - Access token 12 | * @param {string[]} profileIds - Profile ids 13 | * 14 | */ 15 | export const getMatchmakingRanks = async ( 16 | accessToken: string, 17 | profileIds: string[], 18 | ): Promise => { 19 | const str = profileIds 20 | .map((x, i) => { 21 | if (i !== profileIds.length - 1) { 22 | return x + '&players%5b%5d=' 23 | } else { 24 | return x 25 | } 26 | }) 27 | .join('') 28 | 29 | const headers = setHeaders(accessToken, 'nadeo') 30 | const response = await axios({ 31 | url: urls.matchmaking + 'leaderboard/players?players%5b%5d=' + str, 32 | method: 'GET', 33 | headers, 34 | }) 35 | 36 | return response['data'] 37 | } 38 | 39 | type result = { 40 | player: string 41 | rank: number 42 | score: number 43 | } 44 | 45 | export interface IMatchmakingRanks { 46 | matchmaking_id: number 47 | cardinal: number 48 | results: result[] 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/api/prod-trackmania.spec.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | import anyTest, { TestInterface } from 'ava' 5 | 6 | import { 7 | getClientConfig, 8 | getZones, 9 | getAccountZone, 10 | getTrophies, 11 | getTrophyCount, 12 | getTrophyHistory, 13 | getSeason, 14 | getServer, 15 | getMapRecords, 16 | getProfiles, 17 | getMaps, 18 | } from './prod-trackmania' 19 | 20 | import credentials from '../../config/test.json' 21 | 22 | const test = anyTest as TestInterface<{ 23 | account: { lv1accessToken: string; accountId: string } 24 | }> 25 | 26 | test.before(async t => { 27 | const { accountId, lv1accessToken } = (credentials as unknown) as { 28 | lv1accessToken: null | string 29 | accountId: null | string 30 | } 31 | 32 | if (lv1accessToken && accountId) t.context.account = { lv1accessToken, accountId } 33 | }) 34 | 35 | test('Get client config', async t => { 36 | try { 37 | const response = await getClientConfig() 38 | t.assert(response) 39 | } catch (err) { 40 | t.fail(JSON.stringify(err.response.data)) 41 | } 42 | }) 43 | 44 | test('Get zones', async t => { 45 | try { 46 | const response = await getZones(t.context.account.lv1accessToken) 47 | t.assert(response) 48 | } catch (err) { 49 | t.fail(JSON.stringify(err.response.data)) 50 | } 51 | }) 52 | 53 | test('Get account zone', async t => { 54 | try { 55 | const response = await getAccountZone( 56 | t.context.account.lv1accessToken, 57 | t.context.account.accountId, 58 | ) 59 | t.assert(response) 60 | } catch (err) { 61 | t.fail(JSON.stringify(err.response.data)) 62 | } 63 | }) 64 | 65 | test('Get trophies', async t => { 66 | try { 67 | const response = await getTrophies(t.context.account.lv1accessToken) 68 | t.assert(response) 69 | } catch (err) { 70 | t.fail(JSON.stringify(err.response.data)) 71 | } 72 | }) 73 | 74 | test('Get trophy count', async t => { 75 | try { 76 | const response = await getTrophyCount( 77 | t.context.account.lv1accessToken, 78 | t.context.account.accountId, 79 | ) 80 | t.assert(response) 81 | } catch (err) { 82 | t.fail(JSON.stringify(err.response.data)) 83 | } 84 | }) 85 | 86 | test('Get trophy history', async t => { 87 | try { 88 | const response = await getTrophyHistory( 89 | t.context.account.lv1accessToken, 90 | '7dc8d3e3-ccf0-4eb7-bbaa-e8752116ac33', 91 | 1, 92 | 0, 93 | 35, 94 | ) 95 | t.assert(response) 96 | } catch (err) { 97 | t.fail(JSON.stringify(err.response.data)) 98 | } 99 | }) 100 | 101 | test('Get season by ID', async t => { 102 | try { 103 | const response = await getSeason( 104 | t.context.account.lv1accessToken, 105 | '3987d489-03ae-4645-9903-8f7679c3a418', 106 | ) 107 | t.assert(response) 108 | } catch (err) { 109 | t.fail(JSON.stringify(err.response.data)) 110 | } 111 | }) 112 | 113 | test.skip('Get server by UID', async t => { 114 | // TODO: get servers then get server by uid 115 | try { 116 | const response = await getServer( 117 | t.context.account.lv1accessToken, 118 | 'bc251924-d267-4702-b526-9ed4b950d729', 119 | ) 120 | t.assert(response) 121 | } catch (err) { 122 | t.fail(JSON.stringify(err.response.data)) 123 | } 124 | }) 125 | 126 | test('Get account map records', async t => { 127 | try { 128 | const response = await getMapRecords( 129 | t.context.account.lv1accessToken, 130 | '7dc8d3e3-ccf0-4eb7-bbaa-e8752116ac33', 131 | '27cb67e3-f8bc-4971-ab22-f74055ca6b37', 132 | ) 133 | t.assert(response) 134 | } catch (err) { 135 | t.fail(JSON.stringify(err.response.data)) 136 | } 137 | }) 138 | 139 | test('Get profile ids', async t => { 140 | try { 141 | const response = await getProfiles(t.context.account.lv1accessToken, [ 142 | 'a9cbdeff-daa3-4bc2-998c-b2838183fb97', 143 | '531a9861-c024-4063-9b29-14601350b899', 144 | '2ed0997d-62bc-4a53-8c09-ffb793382ea2', 145 | '58278714-fbe5-4bb1-960c-3ad278bb7ecc', 146 | 'aa4e375f-d23e-4915-8d53-8b3307e06764', 147 | 'b67bedd1-7d2f-4861-86c4-dae8c1583ace', 148 | 'f6a1ceb1-1928-4043-9df8-2c5465e65eaa', 149 | '95abee92-1174-45e3-8967-bc46d2e6afe3', 150 | ]) 151 | t.assert(response) 152 | } catch (err) { 153 | t.fail(JSON.stringify(err.response.data)) 154 | } 155 | }) 156 | 157 | test('Get maps', async t => { 158 | try { 159 | const response = await getMaps(t.context.account.lv1accessToken, [ 160 | 'rHonuj4sZKXkq3dbtafrs25ENPg', 161 | '8bTOMNceJrsZdDD2UvJhGsRwnQg', 162 | ]) 163 | t.assert(response) 164 | } catch (err) { 165 | t.fail(JSON.stringify(err.response.data)) 166 | } 167 | }) 168 | -------------------------------------------------------------------------------- /src/lib/api/prod-trackmania.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { urls, setHeaders } from '../main' 4 | 5 | /** 6 | * Get configuration of a client 7 | * 8 | * ## **Requires no authentication** 9 | * 10 | * @category Level 0 11 | * 12 | */ 13 | export const getClientConfig = async (): Promise => { 14 | const response = await axios({ 15 | url: urls.prodTrackmania + '/client/config', 16 | method: 'GET', 17 | }) 18 | 19 | return response['data'] 20 | } 21 | 22 | /** 23 | * Get all the IDs from all the zones for internal use and to be able to call endpoints using this IDs 24 | * 25 | * ## **Requires level 2 authentication** 26 | * 27 | * @category level 1 28 | * @param {string} accessToken - Access token 29 | * 30 | */ 31 | export const getZones = async (accessToken: string): Promise => { 32 | const headers = setHeaders(accessToken, 'nadeo') 33 | const response = await axios({ 34 | url: urls.prodTrackmania + '/zones', 35 | method: 'GET', 36 | headers, 37 | }) 38 | 39 | return response['data'] 40 | } 41 | 42 | /** 43 | * Get account zone 44 | * 45 | * ## **Requires level 2 authentication** 46 | * 47 | * @category level 1 48 | * @param {string} accessToken - Access token 49 | * @param {string} accountId - Account ID 50 | * 51 | */ 52 | export const getAccountZone = async ( 53 | accessToken: string, 54 | accountId: string, 55 | ): Promise => { 56 | const headers = setHeaders(accessToken, 'nadeo') 57 | const response = await axios({ 58 | url: urls.prodTrackmania + '/accounts/' + accountId + '/zone', 59 | method: 'GET', 60 | headers, 61 | }) 62 | 63 | return response['data'] 64 | } 65 | 66 | /** 67 | * Get Trophies 68 | * 69 | * ## **Requires level 2 authentication** 70 | * 71 | * @category level 1 72 | * @param {string} accessToken - Access token 73 | * 74 | */ 75 | export const getTrophies = async (accessToken: string): Promise => { 76 | const headers = setHeaders(accessToken, 'nadeo') 77 | const response = await axios({ 78 | url: urls.prodTrackmania + '/trophies/settings', 79 | method: 'GET', 80 | headers, 81 | }) 82 | 83 | return response['data'] 84 | } 85 | 86 | /** 87 | * Get Trophy count 88 | * 89 | * ## **Requires level 2 authentication** 90 | * 91 | * @category level 1 92 | * @param {string} accessToken - Access token 93 | * @param {string} accountId - Account ID 94 | * 95 | */ 96 | export const getTrophyCount = async ( 97 | accessToken: string, 98 | accountId: string, 99 | ): Promise => { 100 | const headers = setHeaders(accessToken, 'nadeo') 101 | const response = await axios({ 102 | url: urls.prodTrackmania + '/accounts/' + accountId + '/trophies/lastYearSummary', 103 | method: 'GET', 104 | headers, 105 | }) 106 | 107 | return response['data'] 108 | } 109 | /** 110 | * Get trophy history 111 | * 112 | * ## **Requires level 1 authentication** 113 | * 114 | * @category level 1 115 | * @param {string} accessToken - Access token 116 | * @param {string} accountId - Account ID 117 | * @param {number} trophyType - Trophy type (1/2/3/4/5/6/7/8/9) 118 | * @param {number} offset - Offset (default = 0) 119 | * @param {number} count - Count (default = 35) 120 | * 121 | */ 122 | export const getTrophyHistory = async ( 123 | accessToken: string, 124 | accountId: string, 125 | trophyType: number, 126 | offset: number = 0, 127 | count: number = 35, 128 | ): Promise => { 129 | const headers = setHeaders(accessToken, 'nadeo') 130 | const response = await axios({ 131 | url: 132 | urls.prodTrackmania + 133 | '/accounts/' + 134 | accountId + 135 | '/trophies/?trophyType=' + 136 | trophyType + 137 | '&offset=' + 138 | offset + 139 | '&count=' + 140 | count, 141 | method: 'GET', 142 | headers, 143 | }) 144 | 145 | return response['data'] 146 | } 147 | 148 | /** 149 | * Get info about a season, with all the details, included info about map ids 150 | * 151 | * ## **Requires level 2 authentication** 152 | * 153 | * @category level 1 154 | * @param {string} accessToken - Access token 155 | * @param {string} uuid - The seasons uuid 156 | * 157 | */ 158 | export const getSeason = async (accessToken: string, uuid: string): Promise => { 159 | const headers = setHeaders(accessToken, 'nadeo') 160 | const response = await axios({ 161 | url: urls.prodTrackmania + '/seasons/' + uuid, 162 | method: 'GET', 163 | headers, 164 | }) 165 | 166 | return response['data'] 167 | } 168 | 169 | /** 170 | * Get info about a server 171 | * 172 | * ## **Requires level 2 authentication** 173 | * 174 | * @category level 1 175 | * @param {string} accessToken - Access token 176 | * @param {string} uid - Server uid 177 | * 178 | */ 179 | export const getServer = async (accessToken: string, uid: string): Promise => { 180 | const headers = setHeaders(accessToken, 'nadeo') 181 | const response = await axios({ 182 | url: urls.prodTrackmania + '/servers/' + uid, 183 | method: 'GET', 184 | headers, 185 | }) 186 | 187 | return response['data'] 188 | } 189 | 190 | /** 191 | * Get map records for a account 192 | * 193 | * ## **Requires level 2 authentication** 194 | * 195 | * @category level 1 196 | * @param {string} accessToken - Access token 197 | * @param {string} accountId - Account id 198 | * @param {string} mapId - Map id, optional. Leave out to get all records 199 | */ 200 | export const getMapRecords = async ( 201 | accessToken: string, 202 | accountId: string, 203 | mapId?: string, 204 | ): Promise => { 205 | const headers = setHeaders(accessToken, 'nadeo') 206 | const response = await axios({ 207 | url: 208 | urls.prodTrackmania + 209 | '/mapRecords/?accountIdList=' + 210 | accountId + 211 | (mapId ? '&addPersonalBest=false&mapIdList=' + mapId : ''), 212 | method: 'GET', 213 | headers, 214 | }) 215 | 216 | return response['data'] 217 | } 218 | 219 | /** 220 | * Get web identity based on account ids 221 | * 222 | * ## **Requires level 2 authentication** 223 | * 224 | * @category level 1 225 | * @param {string} accessToken - Access token 226 | * @param {string[]} accountIds - account ids 227 | */ 228 | export const getProfiles = async ( 229 | accessToken: string, 230 | accountIds: string[], 231 | ): Promise => { 232 | const str = accountIds 233 | .map((x, i) => { 234 | if (i !== accountIds.length - 1) { 235 | return x + '%2c' 236 | } else { 237 | return x 238 | } 239 | }) 240 | .join('') 241 | 242 | const headers = setHeaders(accessToken, 'nadeo') 243 | const response = await axios({ 244 | url: urls.prodTrackmania + '/webidentities/?accountIdList=' + str, 245 | method: 'GET', 246 | headers, 247 | }) 248 | 249 | return response['data'] 250 | } 251 | 252 | /** 253 | * Get info about a map 254 | * 255 | * ## **Requires level 2 authentication** 256 | * 257 | * @category level 1 258 | * @param {string} accessToken - Access token 259 | * @param {string[]} mapUids - mapUids 260 | */ 261 | export const getMaps = async ( 262 | accessToken: string, 263 | mapUids: string[], 264 | ): Promise => { 265 | const str = mapUids 266 | .map((x, i) => { 267 | if (i !== mapUids.length - 1) { 268 | return x + '%2c' 269 | } else { 270 | return x 271 | } 272 | }) 273 | .join('') 274 | 275 | const headers = setHeaders(accessToken, 'nadeo') 276 | const response = await axios({ 277 | url: urls.prodTrackmania + '/maps/?mapUidList=' + str, 278 | method: 'GET', 279 | headers, 280 | }) 281 | 282 | return response['data'] 283 | } 284 | 285 | export interface ImapRecords { 286 | accountId: string 287 | filename: string 288 | gameMode: string 289 | gameModeCustomData: string 290 | mapId: string 291 | medal: number 292 | recordScore: recordScore 293 | removed: boolean 294 | scopeId: unknown 295 | scope: string 296 | timestamp: string 297 | url: string 298 | } 299 | 300 | type recordScore = { 301 | respawnCount: number 302 | score: number 303 | time: number 304 | } 305 | 306 | export interface Iseason { 307 | creationTimestamp: string 308 | creatorId: string 309 | endTimestamp: string 310 | gameMode: string 311 | gameModeCustomData: string 312 | isOfficial: boolean 313 | name: string 314 | recordScore: string 315 | seasonId: string 316 | seasonMapList: seasonMap[] 317 | } 318 | 319 | type seasonMap = { 320 | mapId: string 321 | timestamp: string 322 | } 323 | 324 | export interface ItrophyCount { 325 | accountId: string 326 | points: number 327 | t1Count: number 328 | t2Count: number 329 | t3Count: number 330 | t4Count: number 331 | t5Count: number 332 | t6Count: number 333 | t7Count: number 334 | t8Count: number 335 | t9Count: number 336 | timestamp: string 337 | } 338 | 339 | type TrophyAchievementInfo = { 340 | trophyAchievementId: string 341 | trophyAchievementType: string 342 | trophySoloMedalAchievementType: string 343 | duration?: number 344 | gameMode: string 345 | gameModeCustomData: string 346 | isOfficial?: boolean 347 | serverId: string 348 | } 349 | 350 | type TrophyGainDetails = { 351 | level: number 352 | previousLevel: number 353 | rank?: number 354 | trophyRanking?: number 355 | } 356 | 357 | type trophyHistoryData = { 358 | accountId: string 359 | t1Count: number 360 | t2Count: number 361 | t3Count: number 362 | t4Count: number 363 | t5Count: number 364 | t6Count: number 365 | t7Count: number 366 | t8Count: number 367 | t9Count: number 368 | timestamp: Date 369 | trophyAchievementInfo: TrophyAchievementInfo 370 | trophyGainDetails: TrophyGainDetails 371 | } 372 | 373 | export interface ItrophyHistory { 374 | count: number 375 | data: trophyHistoryData[] 376 | offset: number 377 | totalCount: number 378 | } 379 | 380 | export interface Itrophies { 381 | gain: { 382 | Solo: { 383 | SoloMedal: { 384 | ClubOfficial: { 385 | allBronze: allBronze 386 | allSilver: allSilver 387 | allGold: allGold 388 | allAuthor: allAuthor 389 | } 390 | ClubUnofficial: { 391 | allSilver: allSilver 392 | allGold: allGold 393 | allAuthor: allAuthor 394 | } 395 | SoloAll: { 396 | allAuthor: allAuthorTiers // A guess 397 | } 398 | SoloBlack: { 399 | allBronze: allBronze 400 | allSilver: allSilver 401 | allGold: allGold 402 | allAuthor: allAuthor 403 | } 404 | SoloBlue: { 405 | allBronze: allBronze 406 | allSilver: allSilver 407 | allGold: allGold 408 | allAuthor: allAuthor 409 | } 410 | SoloGreen: { 411 | allBronze: allBronze 412 | allSilver: allSilver 413 | allGold: allGold 414 | allAuthor: allAuthor 415 | } 416 | SoloRed: { 417 | allBronze: allBronze 418 | allSilver: allSilver 419 | allGold: allGold 420 | allAuthor: allAuthor 421 | } 422 | SoloWhite: { 423 | allBronze: allBronze 424 | allSilver: allSilver 425 | allGold: allGold 426 | allAuthor: allAuthor 427 | } 428 | TrackOfTheDay: { 429 | allGold: allGold 430 | allAuthor: allAuthor 431 | } 432 | } 433 | } 434 | } 435 | } 436 | 437 | type allBronze = { t1Count: number } 438 | type allSilver = { t2Count: number } 439 | type allGold = { t3Count: number } 440 | type allAuthor = { t4Count: number } 441 | type allAuthorTiers = { 442 | t5Count: number 443 | t6Count?: number 444 | t7Count?: number 445 | t8Count?: number 446 | t9Count?: number 447 | } 448 | 449 | export interface IaccountZone { 450 | accountId: string 451 | timestamp: string 452 | zoneId: string 453 | } 454 | 455 | export interface Izones { 456 | icon: string 457 | name: string 458 | parentId: string 459 | zoneId: string 460 | } 461 | 462 | export interface IclientConfig { 463 | keys: clientKey[] 464 | settings: clientSettings 465 | } 466 | 467 | type clientKey = { 468 | id: number 469 | timeout: number 470 | key: string 471 | } 472 | 473 | type clientSettings = { 474 | KillSwitch_ProfileChunk: number 475 | KillSwitch_TitleConfig: number 476 | KillSwitch_TitleLadder: number 477 | KillSwitch_TitlePolicy: number 478 | KillSwitch_TitleProfileChunk: number 479 | KillSwitch_Xp: number 480 | AdsClearCacheOnExit: number 481 | AdsMandatory: number 482 | MapRecordResetTimestamp: number 483 | ClientIP: string 484 | } 485 | 486 | export interface Iserver { 487 | accountId: string 488 | gameMode: string 489 | gameModeCustomData: string 490 | ip: string 491 | isPrivate: boolean 492 | login: string 493 | name: string 494 | playerCount: number 495 | playerCountMax: number 496 | port: number 497 | timestamp: string 498 | titleId: string 499 | } 500 | 501 | export interface IwebIdentity { 502 | accountId: string 503 | provider: string 504 | uid: string 505 | timestamp: string 506 | } 507 | 508 | export interface Imaps { 509 | author: string 510 | authorScore: number 511 | bronzeScore: number 512 | collectionName: string 513 | environment: string 514 | filename: string 515 | goldScore: number 516 | isPlayable: boolean 517 | mapId: string 518 | mapUid: string 519 | name: string 520 | silverScore: number 521 | submitter: string 522 | timestamp: string 523 | fileUrl: string 524 | thumbnailUrl: string 525 | } 526 | -------------------------------------------------------------------------------- /src/lib/api/ubi-services.spec.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | 4 | import anyTest, { TestInterface } from 'ava' 5 | 6 | import { getProfilesById } from './ubi-services' 7 | 8 | import credentials from '../../config/test.json' 9 | 10 | const test = anyTest as TestInterface<{ 11 | account: { ticket: string } 12 | }> 13 | 14 | test.before(async t => { 15 | const { ticket } = credentials as { ticket: null | string } 16 | if (ticket) t.context.account = { ticket } 17 | }) 18 | 19 | test.only('Get profiles', async t => { 20 | try { 21 | const response = await getProfilesById(t.context.account.ticket, [ 22 | '0a2daffa-b588-4d99-bc65-9873b2c9ae6b', 23 | '2ebf7150-5c14-4bb7-b5b2-7631ea68f889', 24 | '4497b71f-3bcc-4d44-87c8-a61dcb1cd1ab', 25 | ]) 26 | t.assert(response) 27 | } catch (err) { 28 | t.fail(JSON.stringify(err.response.data)) 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /src/lib/api/ubi-services.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | import { setHeaders } from '../main' 4 | 5 | /** 6 | * Get more info about profiles 7 | * 8 | * ## **Requires level 1 authentication** 9 | * 10 | * @category level 1 11 | * @param {string} accessToken - Ticket 12 | * @param {string[]} profileIds - Profile ids 13 | * 14 | */ 15 | export const getProfilesById = async ( 16 | accessToken: string, 17 | profileIds: string[], 18 | ): Promise => { 19 | const headers = setHeaders(accessToken, 'ubi') 20 | const response = await axios({ 21 | url: 22 | 'https://public-ubiservices.ubi.com/v3/profiles?profileId=' + 23 | profileIds.join(), 24 | method: 'GET', 25 | headers, 26 | }) 27 | 28 | return response['data'] 29 | } 30 | 31 | export interface Iprofiles { 32 | profiles: profile[] 33 | } 34 | 35 | type profile = { 36 | profileId: string 37 | userId: string 38 | platformType: string 39 | idOnPlatform: string 40 | nameOnPlatform: string 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/main.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export const urls = { 3 | auth: { 4 | ubisoft: 'https://public-ubiservices.ubi.com/v3/profiles/sessions', 5 | trackmaniaUbi: 6 | 'https://prod.trackmania.core.nadeo.online/v2/authentication/token/ubiservices', 7 | trackmaniaNadeo: 8 | 'https://prod.trackmania.core.nadeo.online/v2/authentication/token/nadeoservices', 9 | refreshToken: 10 | 'https://prod.trackmania.core.nadeo.online/v2/authentication/token/refresh', 11 | }, 12 | prodTrackmania: 'https://prod.trackmania.core.nadeo.online', 13 | liveServices: 'https://live-services.trackmania.nadeo.live', 14 | matchmaking: 'https://matchmaking.trackmania.nadeo.club/api/matchmaking/2/', 15 | } 16 | 17 | /** @internal */ 18 | const defaultHeaders = { 19 | 'Content-Type': 'application/json', 20 | 'Ubi-AppId': '86263886-327a-4328-ac69-527f0d20a237', 21 | 'Ubi-RequestedPlatformType': 'uplay', 22 | } 23 | 24 | /** @internal */ 25 | export const setHeaders = (auth: string, type: string) => 26 | type === 'basic' 27 | ? { ...defaultHeaders, Authorization: 'Basic ' + auth } 28 | : type === 'ubi' 29 | ? { ...defaultHeaders, Authorization: 'ubi_v1 t=' + auth } 30 | : type === 'nadeo' && { ...defaultHeaders, Authorization: 'nadeo_v1 t=' + auth } 31 | -------------------------------------------------------------------------------- /test-helper.js: -------------------------------------------------------------------------------- 1 | const { 2 | loginUbi, 3 | loginTrackmaniaUbi, 4 | loginTrackmaniaNadeo, 5 | } = require('./build/main/lib/api/auth.js') 6 | require('dotenv').config() 7 | const { promises: fs } = require('fs') 8 | 9 | const login = async base64 => { 10 | try { 11 | const { ticket } = await loginUbi(base64) // login to ubi, level 0 12 | const ubiTokens = await loginTrackmaniaUbi(ticket) // login to trackmania, level 1 13 | const nadeoLiveTokens = await loginTrackmaniaNadeo( 14 | ubiTokens.accessToken, 15 | 'NadeoLiveServices', 16 | ) // login to trackmania nadeoliveservices, level 2 17 | const nadeoClubTokens = await loginTrackmaniaNadeo( 18 | ubiTokens.accessToken, 19 | 'NadeoClubServices', 20 | ) 21 | console.log('logged in') 22 | return { 23 | ticket, 24 | lv1accessToken: ubiTokens.accessToken, 25 | lv2liveAccessToken: nadeoLiveTokens.accessToken, 26 | lv2clubAccessToken: nadeoClubTokens.accessToken, 27 | accountId: nadeoLiveTokens.accountId, 28 | } 29 | } catch (err) { 30 | console.error(err) 31 | } 32 | } 33 | 34 | ;(async () => { 35 | const mode = process.argv[2] 36 | console.log(mode) 37 | if (mode === 'CREATE') { 38 | const email = process.env.EMAIL 39 | const password = process.env.PASSWORD 40 | const base64 = Buffer.from(email + ':' + password).toString('base64') 41 | 42 | const credentials = JSON.stringify(await login(base64)) 43 | 44 | await fs.writeFile('./src/config/test.json', credentials) 45 | console.log('writed file') 46 | } else if (mode === 'RESET') { 47 | await fs.writeFile( 48 | './src/config/test.json', 49 | JSON.stringify({ 50 | ticket: null, 51 | lv1accessToken: null, 52 | lv2liveAccessToken: null, 53 | lv2clubAccessToken: null, 54 | accountId: null, 55 | }), 56 | ) 57 | console.log('reset file') 58 | } else if (mode === 'CHECK') { 59 | // TODO: check also build 60 | const data = JSON.parse(await fs.readFile('./src/config/test.json', 'utf8')) 61 | if ( 62 | data.ticket || 63 | data.lv1accessToken || 64 | data.lv2refreshToken || 65 | data.accountId 66 | ) { 67 | console.error('Config was not reset. do npm run config:reset') 68 | process.exit(1) 69 | } else { 70 | console.log('File is reset') 71 | } 72 | } else { 73 | console.error('Unknown command') 74 | } 75 | })() 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "outDir": "build/main", 5 | "rootDir": "src", 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 11 | "resolveJsonModule": true, 12 | 13 | "strict": true /* Enable all strict type-checking options. */, 14 | 15 | /* Strict Type-Checking Options */ 16 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 17 | // "strictNullChecks": true /* Enable strict null checks. */, 18 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 19 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 20 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 21 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 22 | 23 | /* Additional Checks */ 24 | "noUnusedLocals": true /* Report errors on unused locals. */, 25 | "noUnusedParameters": true /* Report errors on unused parameters. */, 26 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 27 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 28 | 29 | /* Debugging Options */ 30 | "traceResolution": false /* Report module resolution log messages. */, 31 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 32 | "listFiles": false /* Print names of files part of the compilation. */, 33 | "pretty": true /* Stylize errors and messages using color and context. */, 34 | 35 | /* Experimental Options */ 36 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 37 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 38 | 39 | "lib": ["es2017"], 40 | "types": ["node"], 41 | "typeRoots": ["node_modules/@types", "src/types"] 42 | }, 43 | "include": ["src/**/*.ts"], 44 | "exclude": ["node_modules/**"], 45 | "compileOnSave": false 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "build/module", 6 | "module": "esnext" 7 | }, 8 | "exclude": ["node_modules/**", "**/*.spec.ts", "config/"] 9 | } 10 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier", "tslint-immutable"], 3 | "rules": { 4 | "interface-name": [false, "always-prefix"], 5 | "no-console": [true, "log"], 6 | // TODO: allow devDependencies only in **/*.spec.ts files: 7 | // waiting on https://github.com/palantir/tslint/pull/3708 8 | "no-implicit-dependencies": [true, "dev"], 9 | 10 | /* tslint-immutable rules */ 11 | // Recommended built-in rules 12 | "no-var-keyword": true, 13 | "no-parameter-reassignment": true, 14 | "typedef": [true, "call-signature"], 15 | 16 | // Immutability rules 17 | "readonly-keyword": false, 18 | "readonly-array": false, 19 | "no-let": true, 20 | "no-object-mutation": [true, { "ignore-prefix": "t" }], 21 | "no-delete": true, 22 | "no-method-signature": true, 23 | 24 | // Functional style rules 25 | "no-this": true, 26 | "no-class": true, 27 | "no-mixed-interface": true, 28 | "no-string-literal": false, 29 | "no-expression-statement": false, 30 | 31 | "no-if-statement": false 32 | /* end tslint-immutable rules */ 33 | } 34 | } 35 | --------------------------------------------------------------------------------