├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── commitlint.config.cjs ├── package.json ├── pnpm-lock.yaml ├── src ├── __playground.ts ├── achievement │ ├── getAchievementUnlocks.test.ts │ ├── getAchievementUnlocks.ts │ ├── index.ts │ └── models │ │ ├── achievement-type.model.ts │ │ ├── achievement-unlock-entity.model.ts │ │ ├── get-achievement-unlocks-response.model.ts │ │ └── index.ts ├── comment │ ├── getComments.test.ts │ ├── getComments.ts │ ├── index.ts │ └── models │ │ ├── comment-entity.model.ts │ │ ├── comment-response.model.ts │ │ ├── get-comments-response.model.ts │ │ └── index.ts ├── console │ ├── getConsoleIds.test.ts │ ├── getConsoleIds.ts │ ├── getGameList.test.ts │ ├── getGameList.ts │ ├── index.ts │ └── models │ │ ├── fetched-system.model.ts │ │ ├── game-list.model.ts │ │ ├── get-console-ids-response.model.ts │ │ ├── get-game-list-response.model.ts │ │ └── index.ts ├── feed │ ├── getAchievementOfTheWeek.test.ts │ ├── getAchievementOfTheWeek.ts │ ├── getActiveClaims.test.ts │ ├── getActiveClaims.ts │ ├── getClaims.test.ts │ ├── getClaims.ts │ ├── getRecentGameAwards.test.ts │ ├── getRecentGameAwards.ts │ ├── getTopTenUsers.test.ts │ ├── getTopTenUsers.ts │ ├── index.ts │ └── models │ │ ├── achievement-of-the-week.model.ts │ │ ├── claim-set-type.enum.ts │ │ ├── claim-status.enum.ts │ │ ├── claim-type.enum.ts │ │ ├── get-achievement-of-the-week-response.model.ts │ │ ├── get-recent-game-awards-response.model.ts │ │ ├── get-set-claims-response.model.ts │ │ ├── get-top-ten-users-response.model.ts │ │ ├── index.ts │ │ ├── recent-game-awards.model.ts │ │ ├── set-claim.model.ts │ │ ├── top-ten-users-entity.model.ts │ │ └── top-ten-users.model.ts ├── game │ ├── getAchievementCount.test.ts │ ├── getAchievementCount.ts │ ├── getAchievementDistribution.test.ts │ ├── getAchievementDistribution.ts │ ├── getGame.test.ts │ ├── getGame.ts │ ├── getGameExtended.test.ts │ ├── getGameExtended.ts │ ├── getGameHashes.test.ts │ ├── getGameHashes.ts │ ├── getGameRankAndScore.test.ts │ ├── getGameRankAndScore.ts │ ├── getGameRating.test.ts │ ├── getGameRating.ts │ ├── index.ts │ └── models │ │ ├── achievement-count.model.ts │ │ ├── achievement-distribution-flags.enum.ts │ │ ├── game-extended-achievement-entity.model.ts │ │ ├── game-extended-claim-entity.model.ts │ │ ├── game-extended.model.ts │ │ ├── game-hashes.model.ts │ │ ├── game-rank-and-score-entity.model.ts │ │ ├── game-rating.model.ts │ │ ├── game.model.ts │ │ ├── get-achievement-count-response.model.ts │ │ ├── get-achievement-distribution-response.model.ts │ │ ├── get-game-extended-response.model.ts │ │ ├── get-game-hashes-response.model.ts │ │ ├── get-game-rank-and-score-response.model.ts │ │ ├── get-game-rating-response.model.ts │ │ ├── get-game-response.model.ts │ │ └── index.ts ├── index.ts ├── set-version.js ├── ticket │ ├── getTicketData.test.ts │ ├── getTicketData.ts │ ├── index.ts │ └── models │ │ ├── achievement-ticket-stats-response.model.ts │ │ ├── achievement-ticket-stats.model.ts │ │ ├── game-ticket-stats.model.ts │ │ ├── game-tickets-response.model.ts │ │ ├── index.ts │ │ ├── most-ticketed-games-response.model.ts │ │ ├── most-ticketed-games.model.ts │ │ ├── recent-tickets-response.model.ts │ │ ├── recent-tickets.model.ts │ │ ├── response-ticket-entity.model.ts │ │ ├── ticket-entity.model.ts │ │ ├── tickets-by-user-response.model.ts │ │ └── user-ticket-stats.model.ts ├── user │ ├── getAchievementsEarnedBetween.test.ts │ ├── getAchievementsEarnedBetween.ts │ ├── getAchievementsEarnedOnDay.test.ts │ ├── getAchievementsEarnedOnDay.ts │ ├── getGameInfoAndUserProgress.test.ts │ ├── getGameInfoAndUserProgress.ts │ ├── getUserAwards.test.ts │ ├── getUserAwards.ts │ ├── getUserClaims.test.ts │ ├── getUserClaims.ts │ ├── getUserCompletedGames.test.ts │ ├── getUserCompletedGames.ts │ ├── getUserCompletionProgress.test.ts │ ├── getUserCompletionProgress.ts │ ├── getUserGameRankAndScore.test.ts │ ├── getUserGameRankAndScore.ts │ ├── getUserPoints.test.ts │ ├── getUserPoints.ts │ ├── getUserProfile.test.ts │ ├── getUserProfile.ts │ ├── getUserProgress.test.ts │ ├── getUserProgress.ts │ ├── getUserRecentAchievements.test.ts │ ├── getUserRecentAchievements.ts │ ├── getUserRecentlyPlayedGames.test.ts │ ├── getUserRecentlyPlayedGames.ts │ ├── getUserSummary.test.ts │ ├── getUserSummary.ts │ ├── getUserWantToPlayList.test.ts │ ├── getUserWantToPlayList.ts │ ├── index.ts │ └── models │ │ ├── award-type.model.ts │ │ ├── dated-user-achievement.model.ts │ │ ├── dated-user-achievements-response.model.ts │ │ ├── game-info-and-user-progress.model.ts │ │ ├── get-game-info-and-user-progress-response.model.ts │ │ ├── get-user-awards-response.model.ts │ │ ├── get-user-completed-games-response.model.ts │ │ ├── get-user-completion-progress-response.model.ts │ │ ├── get-user-game-rank-and-score-response.model.ts │ │ ├── get-user-points-response.model.ts │ │ ├── get-user-profile-response.model.ts │ │ ├── get-user-progress-response.model.ts │ │ ├── get-user-recent-achievements-response.model.ts │ │ ├── get-user-recently-played-games-response.model.ts │ │ ├── get-user-summary-response.model.ts │ │ ├── get-user-want-to-play-list-response.model.ts │ │ ├── index.ts │ │ ├── user-awards.model.ts │ │ ├── user-claims-response.model.ts │ │ ├── user-claims.model.ts │ │ ├── user-completed-games.model.ts │ │ ├── user-completion-progress-entity.model.ts │ │ ├── user-completion-progress.model.ts │ │ ├── user-game-rank-and-score.model.ts │ │ ├── user-points.model.ts │ │ ├── user-profile.model.ts │ │ ├── user-progress.model.ts │ │ ├── user-recent-achievement.model.ts │ │ ├── user-recently-played-games.model.ts │ │ ├── user-summary.model.ts │ │ └── user-want-to-play-list.model.ts └── utils │ ├── internal │ ├── apiBaseUrl.ts │ ├── buildRequestUrl.test.ts │ ├── buildRequestUrl.ts │ ├── call.test.ts │ ├── call.ts │ ├── id.model.ts │ ├── index.ts │ ├── serializeProperties.test.ts │ └── serializeProperties.ts │ └── public │ ├── buildAuthorization.test.ts │ ├── buildAuthorization.ts │ ├── index.ts │ └── models │ ├── auth-object.model.ts │ ├── award-kind.model.ts │ └── index.ts ├── tsconfig.json ├── vitest.config.ts └── vitest.polyfills.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/dist/* 3 | **/website/node_modules/* 4 | **/website/.docusaurus/* 5 | **/website/build/* -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | ecmaVersion: 2020, 4 | sourceType: "module", 5 | }, 6 | env: { 7 | browser: true, 8 | node: true, 9 | es6: true, 10 | }, 11 | parser: "@typescript-eslint/parser", 12 | plugins: ["@typescript-eslint", "simple-import-sort", "import"], 13 | extends: [ 14 | "eslint:recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:sonarjs/recommended", 17 | "plugin:unicorn/recommended", 18 | "prettier", 19 | ], 20 | rules: { 21 | "@typescript-eslint/consistent-type-imports": "error", 22 | "@typescript-eslint/explicit-module-boundary-types": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-non-null-assertion": "off", 25 | "@typescript-eslint/no-var-requires": "off", 26 | 27 | "import/first": "error", 28 | "import/newline-after-import": "error", 29 | "import/no-duplicates": "error", 30 | 31 | curly: "error", 32 | "object-shorthand": "error", 33 | "one-var": ["error", "never"], 34 | eqeqeq: "error", 35 | 36 | "simple-import-sort/exports": "error", 37 | "simple-import-sort/imports": "error", 38 | 39 | "sonarjs/no-unused-collection": "off", 40 | 41 | "unicorn/filename-case": "off", 42 | "unicorn/no-array-callback-reference": "off", 43 | "unicorn/no-array-for-each": "warn", 44 | "unicorn/no-array-reduce": "off", 45 | "unicorn/no-null": "off", 46 | "unicorn/prefer-module": "off", 47 | "unicorn/prefer-node-protocol": "off", 48 | "unicorn/prefer-spread": "off", 49 | "unicorn/prefer-switch": "off", 50 | "unicorn/prevent-abbreviations": "off", 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/RetroAchievements/api-js/discussions/new?category=q-a 5 | about: Ask the community for help 6 | - name: Request a feature 7 | url: https://github.com/RetroAchievements/api-js/discussions/new?category=ideas 8 | about: Share ideas for new features 9 | - name: Report a bug 10 | url: https://github.com/RetroAchievements/api-js/issues/new 11 | about: Report a reproducable bug 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | jobs: 11 | build: 12 | name: Build, lint, and test on Node ${{ matrix.node }} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node: ["20.x"] 17 | 18 | steps: 19 | - name: Checkout repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup pnpm 23 | uses: pnpm/action-setup@v4 24 | 25 | - name: Use Node ${{ matrix.node }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node }} 29 | cache: "pnpm" 30 | 31 | - name: Install 32 | run: pnpm install 33 | 34 | - name: Lint 35 | run: pnpm lint 36 | 37 | - name: Test 38 | run: pnpm test 39 | 40 | - name: Build 41 | run: pnpm build 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | .env.local 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | .parcel-cache 81 | 82 | # Next.js build output 83 | .next 84 | out 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # yarn v2 115 | .yarn/cache 116 | .yarn/unplugged 117 | .yarn/build-state.yml 118 | .yarn/install-state.gz 119 | .pnp.* 120 | 121 | # Docusaurus 122 | website/node_modules 123 | website/.docusaurus 124 | website/build 125 | 126 | # VitePress 127 | .temp 128 | .vite_opt_cache 129 | cache 130 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm pretty-quick --staged && pnpm lint 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | website/.docusaurus 4 | website/build 5 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | Please review and abide by the [Code of Conduct](https://docs.retroachievements.org/Users-Code-of-Conduct/). 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RetroAchievements.org 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | The RAWeb team takes the security of the RetroAchievements platform seriously. If you discover a security issue, we appreciate your help in disclosing it to us in a responsible manner. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Here's how you can report a security issue: 8 | 9 | 1. **Do not report security issues in GitHub issues, discussions, or on Discord.** 10 | 2. **Send a direct on-site message.** If you believe you've found a security vulnerability, [please send an on-site message to RAdmin](https://retroachievements.org/user/RAdmin). 11 | 12 | In your report, please include: 13 | 14 | - A brief description of the issue. 15 | - Steps to reproduce the issue, if possible. 16 | - Potential impact of the issue. 17 | 18 | We will acknowledge your report within 48 hours of receiving it. We appreciate your help as we strive to keep our site as secure as possible. Thank you. 19 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('@commitlint/types').UserConfig} */ 2 | module.exports = { 3 | extends: ["@commitlint/config-conventional"], 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@retroachievements/api", 3 | "type": "module", 4 | "source": "src/index.ts", 5 | "description": "A well-tested library that lets you get achievement, user, and game data from RetroAchievements.", 6 | "keywords": [ 7 | "retroachievements", 8 | "achievements", 9 | "trophies", 10 | "raweb", 11 | "retro gaming" 12 | ], 13 | "version": "2.4.0", 14 | "typings": "dist/index.d.ts", 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "require": "./dist/api.cjs", 19 | "default": "./dist/api.modern.js" 20 | } 21 | }, 22 | "main": "./dist/api.cjs", 23 | "types": "./dist/index.d.ts", 24 | "module": "./dist/api.module.js", 25 | "unpkg": "./dist/api.umd.js", 26 | "license": "MIT", 27 | "files": [ 28 | "dist", 29 | "src" 30 | ], 31 | "scripts": { 32 | "dev": "esrun --watch src/__playground.ts", 33 | "prebuild": "node src/set-version.js", 34 | "build": "microbundle", 35 | "prepare": "microbundle && husky install", 36 | "format": "prettier --write . '**/*.{json,md,js,ts,tsx}'", 37 | "format:write": "prettier --write . '**/*.{json,md,js,ts,tsx}'", 38 | "format:check": "prettier --check . '**/*.{json,md,js,ts,tsx}'", 39 | "lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .", 40 | "lint:fix": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx . --fix", 41 | "test": "vitest run", 42 | "test:watch": "vitest", 43 | "test:coverage": "vitest run --coverage", 44 | "verify": "pnpm format:check && pnpm lint && pnpm test:coverage && pnpm build", 45 | "start": "microbundle watch" 46 | }, 47 | "devDependencies": { 48 | "@commitlint/cli": "^17.4.2", 49 | "@commitlint/config-conventional": "^17.4.2", 50 | "@digitak/esrun": "^3.2.19", 51 | "@tsconfig/recommended": "^1.0.2", 52 | "@typescript-eslint/eslint-plugin": "^6.9.1", 53 | "@typescript-eslint/parser": "^6.9.1", 54 | "@vitest/coverage-v8": "^0.34.6", 55 | "cz-conventional-changelog": "^3.3.0", 56 | "dotenv-flow-cli": "^1.0.0", 57 | "eslint": "^8.53.0", 58 | "eslint-config-prettier": "^8.6.0", 59 | "eslint-import-resolver-typescript": "^3.6.1", 60 | "eslint-plugin-import": "^2.29.0", 61 | "eslint-plugin-simple-import-sort": "^10.0.0", 62 | "eslint-plugin-sonarjs": "^0.23.0", 63 | "eslint-plugin-unicorn": "^49.0.0", 64 | "husky": "9.0.11", 65 | "microbundle": "^0.15.1", 66 | "msw": "^2.0.3", 67 | "prettier": "2.8.3", 68 | "pretty-quick": "3.1.3", 69 | "tslib": "^2.6.2", 70 | "type-fest": "^4.6.0", 71 | "typescript": "^5.2.2", 72 | "undici": "^5.27.2", 73 | "vite": "^4.5.0", 74 | "vitest": "^0.34.6" 75 | }, 76 | "config": { 77 | "commitizen": { 78 | "path": "./node_modules/cz-conventional-changelog" 79 | } 80 | }, 81 | "engines": { 82 | "node": ">=16" 83 | }, 84 | "release": { 85 | "branches": [ 86 | "main", 87 | "next" 88 | ] 89 | }, 90 | "repository": { 91 | "type": "git", 92 | "url": "git+https://github.com/RetroAchievements/api-js.git" 93 | }, 94 | "bugs": { 95 | "url": "https://github.com/RetroAchievements/api-js/issues" 96 | }, 97 | "homepage": "https://github.com/RetroAchievements/api-js#readme", 98 | "author": "RAWeb Team", 99 | "packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b" 100 | } 101 | -------------------------------------------------------------------------------- /src/__playground.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PLEASE ONLY COMMIT CHANGES TO THIS FILE IF YOU WANT THEM 3 | * TO DIRECTLY IMPACT EVERY DEV WORKING ON THE PROJECT. 4 | * 5 | * Use this file to test and experiment with changes to the project. 6 | * If changes to this file land in a PR, you probably did something wrong. 7 | */ 8 | 9 | // --- 10 | 11 | /** 12 | * "./index" is the library's single public-facing export. 13 | * In other words, if you're not able to import what you want 14 | * to use from "./index", no one who uses the package will be 15 | * able to either. 16 | */ 17 | import { buildAuthorization, getAchievementCount } from "./index"; 18 | 19 | // MODIFY THESE VALUES. 20 | const username = "myUsername"; 21 | const webApiKey = "myWebApiKey"; 22 | 23 | const main = async () => { 24 | console.log("🚀 @retroachievements/api playground is running.\n"); 25 | 26 | // -- Start testing stuff here -- 27 | 28 | if (username === "myUsername" || webApiKey === "myWebApiKey") { 29 | console.error( 30 | "⛔️ ERROR: In __playground.ts, modify the username and webApiKey variables to match your RA credentials.\n" 31 | ); 32 | } 33 | 34 | const authorization = buildAuthorization({ username, webApiKey }); 35 | 36 | const achievementCount = await getAchievementCount(authorization, { 37 | gameId: 14_402, 38 | }); 39 | console.log(achievementCount); 40 | }; 41 | 42 | main(); 43 | -------------------------------------------------------------------------------- /src/achievement/getAchievementUnlocks.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getAchievementUnlocks } from "./getAchievementUnlocks"; 7 | import type { GetAchievementUnlocksResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getAchievementUnlocks", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getAchievementUnlocks).toBeDefined(); 20 | }); 21 | 22 | it("retrieves metadata about unlocks for a target achievement", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetAchievementUnlocksResponse = { 30 | Achievement: { 31 | ID: "1", 32 | Title: "Ring Collector", 33 | Description: "Collect 100 Rings!", 34 | Points: "5", 35 | TrueRatio: "6", 36 | Author: "Scott", 37 | DateCreated: "2012-11-02 00:03:12", 38 | DateModified: "2022-06-11 16:52:35", 39 | }, 40 | Console: { ID: "1", Title: "Mega Drive" }, 41 | Game: { ID: "1", Title: "Sonic the Hedgehog" }, 42 | UnlocksCount: 9524, 43 | TotalPlayers: 21_710, 44 | Unlocks: [ 45 | { 46 | User: "Tiotroll2022", 47 | RAPoints: "348", 48 | RASoftcorePoints: "363", 49 | DateAwarded: "2023-01-29 21:45:41", 50 | HardcoreMode: "0", 51 | }, 52 | ], 53 | }; 54 | 55 | let requestUrl = ""; 56 | 57 | server.use( 58 | http.get(`${apiBaseUrl}/API_GetAchievementUnlocks.php`, (info) => { 59 | requestUrl = info.request.url; 60 | return HttpResponse.json(mockResponse); 61 | }) 62 | ); 63 | 64 | // ACT 65 | const response = await getAchievementUnlocks(authorization, { 66 | achievementId: 18_000, 67 | count: 1, 68 | offset: 1, 69 | }); 70 | 71 | // ASSERT 72 | expect(requestUrl).toContain("a=18000"); 73 | expect(requestUrl).toContain("o=1"); 74 | expect(requestUrl).toContain("c=1"); 75 | 76 | expect(response).toEqual({ 77 | achievement: { 78 | id: 1, 79 | title: "Ring Collector", 80 | description: "Collect 100 Rings!", 81 | points: 5, 82 | trueRatio: 6, 83 | author: "Scott", 84 | dateCreated: "2012-11-02 00:03:12", 85 | dateModified: "2022-06-11 16:52:35", 86 | }, 87 | console: { id: 1, title: "Mega Drive" }, 88 | game: { id: 1, title: "Sonic the Hedgehog" }, 89 | unlocksCount: 9524, 90 | totalPlayers: 21_710, 91 | unlocks: [ 92 | { 93 | user: "Tiotroll2022", 94 | raPoints: 348, 95 | raSoftcorePoints: 363, 96 | dateAwarded: "2023-01-29 21:45:41", 97 | hardcoreMode: false, 98 | }, 99 | ], 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/achievement/getAchievementUnlocks.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { 10 | AchievementUnlocksMetadata, 11 | GetAchievementUnlocksResponse, 12 | } from "./models"; 13 | 14 | /** 15 | * A call to this function will retrieve a list of users who 16 | * have earned a given achievement, targeted by the achievement's ID. 17 | * 18 | * @param authorization An object containing your username and webApiKey. 19 | * This can be constructed with `buildAuthorization()`. 20 | * 21 | * @param payload.achievementId The target achievement we want to 22 | * retrieve the unlocks list for. If unknown, this can be found 23 | * by navigating to the achievement's page on the RetroAchievements.org 24 | * website. eg: https://retroachievements.org/achievement/13876 has an 25 | * ID of 13876. 26 | * 27 | * @param payload.offset Defaults to 0. The number of entries to skip. 28 | * 29 | * @param payload.count Defaults to 50, has a max of 500. 30 | * 31 | * @example 32 | * ``` 33 | * const achievementUnlocks = await getAchievementUnlocks( 34 | * authorization, 35 | * { achievementId: 13876 } 36 | * ); 37 | * ``` 38 | * 39 | * @returns An array containing metadata about unlocks for 40 | * the target achievement. 41 | * ``` 42 | * [ 43 | * { 44 | * user: 'Podgicus0305', 45 | * raPoints: 15544, 46 | * dateAwarded: '2022-07-12 19:06:34', 47 | * hardcoreMode: true 48 | * } 49 | * ] 50 | * ``` 51 | */ 52 | export const getAchievementUnlocks = async ( 53 | authorization: AuthObject, 54 | payload: { achievementId: ID; offset?: number; count?: number } 55 | ): Promise => { 56 | const { achievementId, offset, count } = payload; 57 | 58 | const queryParams: Record = { a: achievementId }; 59 | 60 | if (offset) { 61 | queryParams.o = offset; 62 | } 63 | 64 | if (count) { 65 | queryParams.c = count; 66 | } 67 | 68 | const url = buildRequestUrl( 69 | apiBaseUrl, 70 | "/API_GetAchievementUnlocks.php", 71 | authorization, 72 | queryParams 73 | ); 74 | 75 | const rawResponse = await call({ url }); 76 | 77 | return serializeProperties(rawResponse, { 78 | shouldCastToNumbers: [ 79 | "ID", 80 | "Points", 81 | "TrueRatio", 82 | "RAPoints", 83 | "RASoftcorePoints", 84 | ], 85 | shouldMapToBooleans: ["HardcoreMode"], 86 | }); 87 | }; 88 | -------------------------------------------------------------------------------- /src/achievement/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getAchievementUnlocks"; 2 | export * from "./models"; 3 | -------------------------------------------------------------------------------- /src/achievement/models/achievement-type.model.ts: -------------------------------------------------------------------------------- 1 | export type AchievementType = 2 | | "progression" 3 | | "win_condition" 4 | | "missable" 5 | | null; 6 | -------------------------------------------------------------------------------- /src/achievement/models/achievement-unlock-entity.model.ts: -------------------------------------------------------------------------------- 1 | export interface AchievementUnlockEntity { 2 | user: string; 3 | raPoints: number; 4 | raSoftcorePoints: number; 5 | dateAwarded: string; 6 | hardcoreMode: boolean; 7 | } 8 | 9 | export interface AchievementUnlocksMetadata { 10 | achievement: { 11 | id: number; 12 | title: string; 13 | description: string; 14 | points: number; 15 | trueRatio: number; 16 | author: string; 17 | dateCreated: string; 18 | dateModified: string; 19 | }; 20 | 21 | console: { id: number; title: string }; 22 | game: { id: number; title: string }; 23 | unlocksCount: number; 24 | totalPlayers: number; 25 | unlocks: AchievementUnlockEntity[]; 26 | } 27 | -------------------------------------------------------------------------------- /src/achievement/models/get-achievement-unlocks-response.model.ts: -------------------------------------------------------------------------------- 1 | interface AchievementUnlocksResponseEntity { 2 | User: string; 3 | RAPoints: string; 4 | RASoftcorePoints: string; 5 | DateAwarded: string; 6 | HardcoreMode: string; 7 | } 8 | 9 | export interface GetAchievementUnlocksResponse { 10 | Achievement: { 11 | ID: string; 12 | Title: string; 13 | Description: string; 14 | Points: string; 15 | TrueRatio: string; 16 | Author: string; 17 | DateCreated: string; 18 | DateModified: string; 19 | }; 20 | Console: { ID: string; Title: string }; 21 | Game: { ID: string; Title: string }; 22 | UnlocksCount: number; 23 | TotalPlayers: number; 24 | Unlocks: AchievementUnlocksResponseEntity[]; 25 | } 26 | -------------------------------------------------------------------------------- /src/achievement/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./achievement-type.model"; 2 | export * from "./achievement-unlock-entity.model"; 3 | export * from "./get-achievement-unlocks-response.model"; 4 | -------------------------------------------------------------------------------- /src/comment/getComments.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { CommentsResponse, GetCommentsResponse } from "./models"; 10 | 11 | const kindMap: Record<"game" | "achievement" | "user", number> = { 12 | game: 1, 13 | achievement: 2, 14 | user: 3, 15 | }; 16 | 17 | /** 18 | * A call to this function will retrieve a list of comments on a particular target. 19 | * 20 | * @param authorization An object containing your username and webApiKey. 21 | * This can be constructed with `buildAuthorization()`. 22 | * 23 | * @param payload.identifier The identifier to retrieve. For user walls, this will 24 | * be a string (the username), and for game and achievement walls, this will be a 25 | * the ID of the object in question. 26 | * 27 | * @param payload.kind What kind of identifier was used. This can be "game", 28 | * "achievement", or "user". 29 | * 30 | * @param payload.offset Defaults to 0. The number of entries to skip. 31 | * 32 | * @param payload.count Defaults to 50, has a max of 500. 33 | * 34 | * @example 35 | * ``` 36 | * // Retrieving game/achievement comments 37 | * const gameWallComments = await getComments( 38 | * authorization, 39 | * { identifier: 20294, kind: 'game', count: 4, offset: 0 }, 40 | * ); 41 | * 42 | * // Retrieving comments on a user's wall 43 | * const userWallComments = await getComments( 44 | * authorization, 45 | * { identifier: "xelnia", count: 4, offset: 0 }, 46 | * ); 47 | * ``` 48 | * 49 | * @returns An object containing the amount of comments retrieved, 50 | * the total comments, and an array of the comments themselves. 51 | * ``` 52 | * { 53 | * count: 4, 54 | * total: 4, 55 | * results: [ 56 | * { 57 | * user: "PlayTester", 58 | * submitted: "2024-07-31T11:22:23.000000Z", 59 | * commentText: "Comment 1" 60 | * }, 61 | * // ... 62 | * ] 63 | * } 64 | * ``` 65 | */ 66 | export const getComments = async ( 67 | authorization: AuthObject, 68 | payload: { 69 | identifier: ID; 70 | kind?: "game" | "achievement" | "user"; 71 | offset?: number; 72 | count?: number; 73 | } 74 | ): Promise => { 75 | const { identifier, kind, offset, count } = payload; 76 | 77 | const queryParams: Record = { i: identifier }; 78 | 79 | if (kind) { 80 | queryParams.t = kindMap[kind]; 81 | } else if (typeof identifier === "number") { 82 | throw new TypeError( 83 | "'kind' must be specified when looking up an achievement or game." 84 | ); 85 | } 86 | 87 | if (offset) { 88 | queryParams.o = offset; 89 | } 90 | 91 | if (count) { 92 | queryParams.c = count; 93 | } 94 | 95 | const url = buildRequestUrl( 96 | apiBaseUrl, 97 | "/API_GetComments.php", 98 | authorization, 99 | queryParams 100 | ); 101 | 102 | const rawResponse = await call({ url }); 103 | 104 | return serializeProperties(rawResponse, { 105 | shouldCastToNumbers: ["Count", "Total"], 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /src/comment/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getComments"; 2 | export * from "./models"; 3 | -------------------------------------------------------------------------------- /src/comment/models/comment-entity.model.ts: -------------------------------------------------------------------------------- 1 | export interface CommentEntity { 2 | user: string; 3 | submitted: string; 4 | commentText: string; 5 | } 6 | 7 | export type Comment = CommentEntity; 8 | -------------------------------------------------------------------------------- /src/comment/models/comment-response.model.ts: -------------------------------------------------------------------------------- 1 | import type { CommentEntity } from "./comment-entity.model"; 2 | 3 | export interface CommentsResponse { 4 | count: number; 5 | total: number; 6 | results: CommentEntity[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/comment/models/get-comments-response.model.ts: -------------------------------------------------------------------------------- 1 | interface RawCommentsResponseEntity { 2 | Count: number; 3 | Total: number; 4 | Results: RawComment[]; 5 | } 6 | 7 | interface RawComment { 8 | User: string; 9 | Submitted: string; 10 | CommentText: string; 11 | } 12 | 13 | export type GetCommentsResponse = RawCommentsResponseEntity; 14 | -------------------------------------------------------------------------------- /src/comment/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./comment-entity.model"; 2 | export * from "./comment-response.model"; 3 | export * from "./get-comments-response.model"; 4 | -------------------------------------------------------------------------------- /src/console/getConsoleIds.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getConsoleIds } from "./getConsoleIds"; 9 | import type { FetchedSystem, GetConsoleIdsResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getConsoleIds", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getConsoleIds).toBeDefined(); 22 | }); 23 | 24 | it("retrieves a list of console IDs and their names and cleans properties", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetConsoleIdsResponse = [ 32 | { 33 | ID: "1", 34 | Name: "Mega Drive", 35 | IconURL: 36 | "https://static.retroachievements.org/assets/images/system/md.png", 37 | Active: true, 38 | IsGameSystem: true, 39 | }, 40 | { 41 | ID: "2", 42 | Name: "Nintendo 64", 43 | IconURL: 44 | "https://static.retroachievements.org/assets/images/system/n64.png", 45 | Active: true, 46 | IsGameSystem: true, 47 | }, 48 | { 49 | ID: "3", 50 | Name: "SNES", 51 | IconURL: 52 | "https://static.retroachievements.org/assets/images/system/snes.png", 53 | Active: false, 54 | IsGameSystem: false, 55 | }, 56 | ]; 57 | 58 | server.use( 59 | http.get(`${apiBaseUrl}/API_GetConsoleIDs.php`, () => 60 | HttpResponse.json(mockResponse) 61 | ) 62 | ); 63 | 64 | // ACT 65 | const response = await getConsoleIds(authorization, { 66 | shouldOnlyRetrieveActiveSystems: true, 67 | shouldOnlyRetrieveGameSystems: true, 68 | }); 69 | 70 | // ASSERT 71 | const expectedResponse: FetchedSystem[] = [ 72 | { 73 | id: 1, 74 | name: "Mega Drive", 75 | iconUrl: 76 | "https://static.retroachievements.org/assets/images/system/md.png", 77 | active: true, 78 | isGameSystem: true, 79 | }, 80 | { 81 | id: 2, 82 | name: "Nintendo 64", 83 | iconUrl: 84 | "https://static.retroachievements.org/assets/images/system/n64.png", 85 | active: true, 86 | isGameSystem: true, 87 | }, 88 | { 89 | id: 3, 90 | name: "SNES", 91 | iconUrl: 92 | "https://static.retroachievements.org/assets/images/system/snes.png", 93 | active: false, 94 | isGameSystem: false, 95 | }, 96 | ]; 97 | 98 | expect(response).toEqual(expectedResponse); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/console/getConsoleIds.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { FetchedSystem, GetConsoleIdsResponse } from "./models"; 9 | 10 | /** 11 | * A call to this function will retrieve the complete list 12 | * of console ID and name pairs on the RetroAchievements.org 13 | * platform. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.shouldOnlyRetrieveActiveSystems If true, only systems that 19 | * officially support achievements will be returned. 20 | * 21 | * @param payload.shouldOnlyRetrieveGameSystems If true, events and hubs will 22 | * not be returned. 23 | * 24 | * @example 25 | * ``` 26 | * const consoleIds = await getConsoleIds(authorization); 27 | * ``` 28 | * 29 | * @returns An array containing a complete list of console ID 30 | * and name pairs for RetroAchievements.org. 31 | * ```json 32 | * { 33 | * id: "1", 34 | * name: "Mega Drive", 35 | * iconUrl: "https://static.retroachievements.org/assets/images/system/md.png", 36 | * active: true, 37 | * isGameSystem: true 38 | * } 39 | * ``` 40 | */ 41 | export const getConsoleIds = async ( 42 | authorization: AuthObject, 43 | payload?: { 44 | shouldOnlyRetrieveActiveSystems: boolean; 45 | shouldOnlyRetrieveGameSystems: boolean; 46 | } 47 | ): Promise => { 48 | let callPayload: Record | undefined; 49 | 50 | if (payload?.shouldOnlyRetrieveActiveSystems) { 51 | callPayload = { ...callPayload, a: 1 }; 52 | } 53 | if (payload?.shouldOnlyRetrieveGameSystems) { 54 | callPayload = { ...callPayload, g: 1 }; 55 | } 56 | 57 | const url = buildRequestUrl( 58 | apiBaseUrl, 59 | "/API_GetConsoleIDs.php", 60 | authorization, 61 | callPayload 62 | ); 63 | 64 | const rawResponse = await call({ url }); 65 | 66 | return serializeProperties(rawResponse, { 67 | shouldCastToNumbers: ["ID"], 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /src/console/getGameList.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getGameList } from "./getGameList"; 9 | import type { GetGameListResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getGameList", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getGameList).toBeDefined(); 22 | }); 23 | 24 | it("retrieves a list of games and cleans their properties", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetGameListResponse = [ 32 | { 33 | Title: "Elemental Master", 34 | ID: "4247", 35 | ConsoleID: "1", 36 | ConsoleName: "Mega Drive", 37 | ImageIcon: "/Images/048245.png", 38 | NumAchievements: 44, 39 | NumLeaderboards: 0, 40 | Points: 500, 41 | DateModified: "2021-12-09 17:05:39", 42 | ForumTopicID: 1972, 43 | Hashes: [ 44 | "32e1a15161ef1f070b023738353bde51", 45 | "9b04970a603ace521c7cca2acaf69804", 46 | ], 47 | }, 48 | ]; 49 | 50 | server.use( 51 | http.get(`${apiBaseUrl}/API_GetGameList.php`, () => 52 | HttpResponse.json(mockResponse) 53 | ) 54 | ); 55 | 56 | // ACT 57 | const response = await getGameList(authorization, { 58 | consoleId: 1, 59 | shouldRetrieveGameHashes: true, 60 | }); 61 | 62 | // ASSERT 63 | expect(response).toEqual([ 64 | { 65 | title: "Elemental Master", 66 | id: 4247, 67 | consoleId: 1, 68 | consoleName: "Mega Drive", 69 | imageIcon: "/Images/048245.png", 70 | numAchievements: 44, 71 | numLeaderboards: 0, 72 | points: 500, 73 | dateModified: "2021-12-09 17:05:39", 74 | forumTopicId: 1972, 75 | hashes: [ 76 | "32e1a15161ef1f070b023738353bde51", 77 | "9b04970a603ace521c7cca2acaf69804", 78 | ], 79 | }, 80 | ]); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/console/getGameList.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { GameList, GetGameListResponse } from "./models"; 10 | /** 11 | * A call to this function will retrieve the complete list 12 | * of games for a specified console on the RetroAchievements.org 13 | * platform. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.consoleId The unique console ID to retrieve a list of 19 | * games from. The list of consoleIds can be retrieved using the `getConsoleIds()` 20 | * function provided by this library. 21 | * 22 | * @param payload.shouldOnlyRetrieveGamesWithAchievements If truthy, will not 23 | * return games that do not have achievements. 24 | * 25 | * @param payload.shouldRetrieveGameHashes If truthy, will return valid 26 | * hashes for game ROMs in an array attached to each game in the list. 27 | * 28 | * @example 29 | * ``` 30 | * const gameList = await getGameList( 31 | * authorization, 32 | * { consoleId: 1, shouldOnlyRetrieveGamesWithAchievements: true } 33 | * ); 34 | * ``` 35 | * 36 | * @returns An array containing a list of games for a given consoleId. 37 | * ``` 38 | * [ 39 | * { 40 | * title: "Elemental Master", 41 | * id: 4247, 42 | * consoleId: 1, 43 | * consoleName: "Mega Drive", 44 | * imageIcon: "/Images/048245.png", 45 | * numAchievements: 44, 46 | * numLeaderboards: 0, 47 | * points: 500, 48 | * dateModified: "2021-12-09 17:05:39", 49 | * forumTopicId: 1972, 50 | * hashes: ["32e1a15161ef1f070b023738353bde51"] 51 | * } 52 | * ] 53 | * ``` 54 | */ 55 | export const getGameList = async ( 56 | authorization: AuthObject, 57 | payload: { 58 | consoleId: ID; 59 | shouldOnlyRetrieveGamesWithAchievements?: boolean; 60 | shouldRetrieveGameHashes?: boolean; 61 | } 62 | ): Promise => { 63 | const { 64 | consoleId, 65 | shouldOnlyRetrieveGamesWithAchievements, 66 | shouldRetrieveGameHashes, 67 | } = payload; 68 | 69 | let callPayload: Record = { i: consoleId }; 70 | 71 | if (shouldOnlyRetrieveGamesWithAchievements !== undefined) { 72 | callPayload = { 73 | ...callPayload, 74 | f: shouldOnlyRetrieveGamesWithAchievements ? 1 : 0, 75 | }; 76 | } 77 | 78 | if (shouldRetrieveGameHashes) { 79 | callPayload = { ...callPayload, h: shouldRetrieveGameHashes ? 1 : 0 }; 80 | } 81 | 82 | const url = buildRequestUrl( 83 | apiBaseUrl, 84 | "/API_GetGameList.php", 85 | authorization, 86 | callPayload 87 | ); 88 | 89 | const rawResponse = await call({ url }); 90 | 91 | return serializeProperties(rawResponse, { 92 | shouldCastToNumbers: ["ID", "ConsoleID"], 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /src/console/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getConsoleIds"; 2 | export * from "./getGameList"; 3 | export * from "./models"; 4 | -------------------------------------------------------------------------------- /src/console/models/fetched-system.model.ts: -------------------------------------------------------------------------------- 1 | export interface FetchedSystem { 2 | id: number; 3 | name: string; 4 | iconUrl: string; 5 | active: boolean; 6 | isGameSystem: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/console/models/game-list.model.ts: -------------------------------------------------------------------------------- 1 | interface GameEntity { 2 | title: string; 3 | id: number; 4 | consoleId: number; 5 | consoleName: string; 6 | imageIcon: string; 7 | numAchievements: number; 8 | numLeaderboards: number; 9 | points: number; 10 | dateModified: string; 11 | forumTopicId: number; 12 | 13 | hashes?: string[]; 14 | } 15 | 16 | export type GameList = GameEntity[]; 17 | -------------------------------------------------------------------------------- /src/console/models/get-console-ids-response.model.ts: -------------------------------------------------------------------------------- 1 | export type GetConsoleIdsResponse = readonly { 2 | ID: string; 3 | Name: string; 4 | IconURL: string; 5 | Active: boolean; 6 | IsGameSystem: boolean; 7 | }[]; 8 | -------------------------------------------------------------------------------- /src/console/models/get-game-list-response.model.ts: -------------------------------------------------------------------------------- 1 | interface RawGameListEntity { 2 | Title: string; 3 | ID: string; 4 | ConsoleID: string; 5 | ConsoleName: string; 6 | ImageIcon: string; 7 | NumAchievements: number; 8 | NumLeaderboards: number; 9 | Points: number; 10 | DateModified: string; 11 | ForumTopicID: number; 12 | 13 | Hashes?: string[]; 14 | } 15 | 16 | export type GetGameListResponse = RawGameListEntity[]; 17 | -------------------------------------------------------------------------------- /src/console/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fetched-system.model"; 2 | export * from "./game-list.model"; 3 | export * from "./get-console-ids-response.model"; 4 | export * from "./get-game-list-response.model"; 5 | -------------------------------------------------------------------------------- /src/feed/getAchievementOfTheWeek.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | AchievementOfTheWeek, 10 | GetAchievementOfTheWeekResponse, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve comprehensive 15 | * metadata about the current Achievement of the Week. 16 | * 17 | * @param authorization An object containing your username and webApiKey. 18 | * This can be constructed with `buildAuthorization()`. 19 | * 20 | * @example 21 | * ``` 22 | * const achievementOfTheWeek = await getAchievementOfTheWeek( 23 | * authorization 24 | * ); 25 | * ``` 26 | * 27 | * @returns An object containing comprehensive metadata 28 | * about the current Achievement of the Week. 29 | * ``` 30 | * { 31 | * achievement: { 32 | * id: "165062", 33 | * title: "The True Hero", 34 | * description: "Receive any Ending as Han [Normal or Hard]", 35 | * points: "10", 36 | * trueRatio: "22", 37 | * author: "BigWeedSmokerMan", 38 | * dateCreated: "2021-08-08 17:47:46", 39 | * dateModified: "2021-08-09 12:20:05", 40 | * badgeName: "185805", 41 | * badgeUrl: "/Badge/185805.png" 42 | * }, 43 | * console: { id: "39", title: "Saturn" }, 44 | * forumTopic: { id: "14767" }, 45 | * game: { id: "14513", title: "Guardian Heroes" }, 46 | * startAt: "2022-10-10 00:00:00", 47 | * totalPlayers: "219", 48 | * unlocks: [ 49 | * { 50 | * user: "Tirbaba2", 51 | * raPoints: "72", 52 | * raSoftcorePoints: "72", 53 | * dateAwarded: "2022-10-10 01:42:19", 54 | * hardcoreMode: "1" 55 | * } 56 | * ], 57 | * unlocksCount: "40" 58 | * } 59 | * ``` 60 | */ 61 | export const getAchievementOfTheWeek = async ( 62 | authorization: AuthObject 63 | ): Promise => { 64 | const url = buildRequestUrl( 65 | apiBaseUrl, 66 | "/API_GetAchievementOfTheWeek.php", 67 | authorization 68 | ); 69 | 70 | const rawResponse = await call({ url }); 71 | 72 | return serializeProperties(rawResponse, { 73 | shouldCastToNumbers: [ 74 | "ID", 75 | "Points", 76 | "TrueRatio", 77 | "TotalPlayers", 78 | "RAPoints", 79 | "RASoftcorePoints", 80 | "UnlocksCount", 81 | ], 82 | shouldMapToBooleans: ["HardcoreMode"], 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /src/feed/getActiveClaims.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getActiveClaims } from "./getActiveClaims"; 9 | import type { GetSetClaimsResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getActiveClaims", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getActiveClaims).toBeDefined(); 22 | }); 23 | 24 | it("retrieves metadata about current active claims", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetSetClaimsResponse = [ 32 | { 33 | ID: 7043, 34 | User: "siouxerskate", 35 | GameID: 3726, 36 | GameTitle: "Tactics Ogre: Let Us Cling Together", 37 | GameIcon: "/Images/049640.png", 38 | ConsoleName: "PlayStation Portable", 39 | ConsoleID: 1, 40 | ClaimType: 0, 41 | SetType: 0, 42 | Status: 0, 43 | Extension: 0, 44 | Special: 0, 45 | Created: "2022-10-03 20:29:45", 46 | DoneTime: "2023-01-03 20:29:45", 47 | Updated: "2022-10-03 20:29:45", 48 | MinutesLeft: 112_285, 49 | UserIsJrDev: 1, 50 | }, 51 | ]; 52 | 53 | server.use( 54 | http.get(`${apiBaseUrl}/API_GetActiveClaims.php`, () => 55 | HttpResponse.json(mockResponse) 56 | ) 57 | ); 58 | 59 | // ACT 60 | const response = await getActiveClaims(authorization); 61 | 62 | // ASSERT 63 | expect(response).toEqual([ 64 | { 65 | id: 7043, 66 | user: "siouxerskate", 67 | gameId: 3726, 68 | gameTitle: "Tactics Ogre: Let Us Cling Together", 69 | gameIcon: "/Images/049640.png", 70 | consoleName: "PlayStation Portable", 71 | consoleId: 1, 72 | claimType: 0, 73 | setType: 0, 74 | status: 0, 75 | extension: 0, 76 | special: 0, 77 | created: "2022-10-03 20:29:45", 78 | doneTime: "2023-01-03 20:29:45", 79 | updated: "2022-10-03 20:29:45", 80 | minutesLeft: 112_285, 81 | userIsJrDev: true, 82 | }, 83 | ]); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/feed/getActiveClaims.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { GetSetClaimsResponse, SetClaim } from "./models"; 9 | 10 | /** 11 | * A call to this function returns information about all 12 | * (1000 max) active set claims. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @example 18 | * ``` 19 | * const activeClaims = await getActiveClaims(authorization); 20 | * ``` 21 | * 22 | * @returns An array containing metadata about all active claims. 23 | * ``` 24 | * [ 25 | * { 26 | * id: 7044, 27 | * user: "blendedsea", 28 | * gameId: 19212, 29 | * gameTitle: "SpongeBob SquarePants: Battle for Bikini Bottom", 30 | * gameIcon: "/Images/059776.png", 31 | * consoleName: "PlayStation 2", 32 | * consoleId: 22, 33 | * claimType: 0, 34 | * setType: 0, 35 | * status: 0, 36 | * extension: 0, 37 | * special: 0, 38 | * created: "2022-10-04 00:25:06", 39 | * doneTime: "2023-01-04 00:25:06", 40 | * updated: "2022-10-04 00:25:06", 41 | * minutesLeft: 112523, 42 | * userIsJrDev: false 43 | * } 44 | * ] 45 | * ``` 46 | */ 47 | export const getActiveClaims = async ( 48 | authorization: AuthObject 49 | ): Promise => { 50 | const url = buildRequestUrl( 51 | apiBaseUrl, 52 | "/API_GetActiveClaims.php", 53 | authorization 54 | ); 55 | 56 | const rawResponse = await call({ url }); 57 | 58 | return serializeProperties(rawResponse, { 59 | shouldMapToBooleans: ["UserIsJrDev"], 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/feed/getClaims.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getClaims } from "./getClaims"; 9 | import type { GetSetClaimsResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getClaims", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getClaims).toBeDefined(); 22 | }); 23 | 24 | it("retrieves metadata about a requested kind of claims", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetSetClaimsResponse = [ 32 | { 33 | ID: 7043, 34 | User: "siouxerskate", 35 | GameID: 3726, 36 | GameTitle: "Tactics Ogre: Let Us Cling Together", 37 | GameIcon: "/Images/049640.png", 38 | ConsoleName: "PlayStation Portable", 39 | ConsoleID: 1, 40 | ClaimType: 0, 41 | SetType: 0, 42 | Status: 0, 43 | Extension: 0, 44 | Special: 0, 45 | Created: "2022-10-03 20:29:45", 46 | DoneTime: "2023-01-03 20:29:45", 47 | Updated: "2022-10-03 20:29:45", 48 | MinutesLeft: 112_285, 49 | UserIsJrDev: 1, 50 | }, 51 | ]; 52 | 53 | server.use( 54 | http.get(`${apiBaseUrl}/API_GetClaims.php`, () => 55 | HttpResponse.json(mockResponse) 56 | ) 57 | ); 58 | 59 | // ACT 60 | const response = await getClaims(authorization, { claimKind: "completed" }); 61 | 62 | // ASSERT 63 | expect(response).toEqual([ 64 | { 65 | id: 7043, 66 | user: "siouxerskate", 67 | gameId: 3726, 68 | gameTitle: "Tactics Ogre: Let Us Cling Together", 69 | gameIcon: "/Images/049640.png", 70 | consoleName: "PlayStation Portable", 71 | consoleId: 1, 72 | claimType: 0, 73 | setType: 0, 74 | status: 0, 75 | extension: 0, 76 | special: 0, 77 | created: "2022-10-03 20:29:45", 78 | doneTime: "2023-01-03 20:29:45", 79 | updated: "2022-10-03 20:29:45", 80 | minutesLeft: 112_285, 81 | userIsJrDev: true, 82 | }, 83 | ]); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /src/feed/getClaims.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { GetSetClaimsResponse, SetClaim } from "./models"; 9 | 10 | type ClaimKind = "completed" | "dropped" | "expired"; 11 | 12 | export const getClaims = async ( 13 | authorization: AuthObject, 14 | payload: { claimKind: ClaimKind } 15 | ): Promise => { 16 | const { claimKind } = payload; 17 | 18 | const url = buildRequestUrl(apiBaseUrl, "/API_GetClaims.php", authorization, { 19 | k: claimKindValueMap[claimKind], 20 | }); 21 | 22 | const rawResponse = await call({ url }); 23 | 24 | return serializeProperties(rawResponse, { 25 | shouldMapToBooleans: ["UserIsJrDev"], 26 | }); 27 | }; 28 | 29 | const claimKindValueMap: Record = { 30 | completed: "1", 31 | dropped: "2", 32 | expired: "3", 33 | }; 34 | -------------------------------------------------------------------------------- /src/feed/getRecentGameAwards.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getRecentGameAwards } from "./getRecentGameAwards"; 7 | import type { GetRecentGameAwardsResponse, RecentGameAwards } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getRecentGameAwards", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getRecentGameAwards).toBeDefined(); 20 | }); 21 | 22 | it("retrieves metadata about all recently-earned game awards on the site", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetRecentGameAwardsResponse = { 30 | Count: 1, 31 | Total: 1, 32 | Results: [ 33 | { 34 | User: "renanbrj", 35 | AwardKind: "mastered", 36 | AwardDate: "2022-01-01T23:48:04+00:00", 37 | GameID: 14_284, 38 | GameTitle: "Batman Returns", 39 | ConsoleID: 15, 40 | ConsoleName: "Game Gear", 41 | }, 42 | ], 43 | }; 44 | 45 | server.use( 46 | http.get(`${apiBaseUrl}/API_GetRecentGameAwards.php`, () => 47 | HttpResponse.json(mockResponse) 48 | ) 49 | ); 50 | 51 | // ACT 52 | const response = await getRecentGameAwards(authorization, { 53 | startDate: "2025-01-05", 54 | offset: 10, 55 | count: 10, 56 | desiredAwardKinds: ["completed"], 57 | }); 58 | 59 | const expectedResponse: RecentGameAwards = { 60 | count: 1, 61 | total: 1, 62 | results: [ 63 | { 64 | user: "renanbrj", 65 | awardKind: "mastered", 66 | awardDate: "2022-01-01T23:48:04+00:00", 67 | gameId: 14_284, 68 | gameTitle: "Batman Returns", 69 | consoleId: 15, 70 | consoleName: "Game Gear", 71 | }, 72 | ], 73 | }; 74 | 75 | // ASSERT 76 | expect(response).toEqual(expectedResponse); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/feed/getRecentGameAwards.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject, AwardKind } from "../utils/public"; 8 | import type { GetRecentGameAwardsResponse, RecentGameAwards } from "./models"; 9 | 10 | /** 11 | * A call to this function will retrieve all recently granted game 12 | * awards across the site's userbase. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @param payload.startDate The date to fetch awards from. 18 | * 19 | * @param payload.offset Optional. Defaults to 0. 20 | * 21 | * @param payload.count Optional. Defaults to 25. 22 | * 23 | * @param payload.desiredAwardKinds Optional. Defaults to all. Accepts "beaten-softcore", "beaten-hardcore", "completed", and/or "mastered". 24 | * 25 | * @example 26 | * ``` 27 | * const recentGameAwards = await getRecentGameAwards( 28 | * authorization, 29 | * ); 30 | * ``` 31 | * 32 | * @returns An object containing metadata about all recently granted game 33 | * awards across the site's userbase 34 | * ``` 35 | * { 36 | * count: 1, 37 | * total: 1, 38 | * results: [ 39 | * { 40 | * user: "renanbrj", 41 | * awardKind: "mastered", 42 | * awardDate: "2022-01-01T23:48:04+00:00", 43 | * gameId: 14_284, 44 | * gameTitle: "Batman Returns", 45 | * consoleId: 15, 46 | * consoleName: "Game Gear", 47 | * }, 48 | * ], 49 | * } 50 | * ``` 51 | */ 52 | export const getRecentGameAwards = async ( 53 | authorization: AuthObject, 54 | payload?: Partial<{ 55 | startDate: string; 56 | offset: number; 57 | count: number; 58 | desiredAwardKinds: AwardKind[]; 59 | }> 60 | ): Promise => { 61 | const queryParams: Record = {}; 62 | if (payload?.startDate) { 63 | queryParams.d = payload.startDate; 64 | } 65 | if (payload?.offset) { 66 | queryParams.o = payload.offset; 67 | } 68 | if (payload?.count) { 69 | queryParams.c = payload.count; 70 | } 71 | if (payload?.desiredAwardKinds) { 72 | queryParams.k = payload.desiredAwardKinds.join(","); 73 | } 74 | 75 | const url = buildRequestUrl( 76 | apiBaseUrl, 77 | "/API_GetRecentGameAwards.php", 78 | authorization, 79 | queryParams 80 | ); 81 | 82 | const rawResponse = await call({ url }); 83 | 84 | return serializeProperties(rawResponse); 85 | }; 86 | -------------------------------------------------------------------------------- /src/feed/getTopTenUsers.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getTopTenUsers } from "./getTopTenUsers"; 7 | import type { GetTopTenUsersResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getTopTenUsers", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getTopTenUsers).toBeDefined(); 20 | }); 21 | 22 | it("retrieves metadata about the current top ten users on the site", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetTopTenUsersResponse = [ 30 | { "1": "MaxMilyin", "2": "346289", "3": "995092" }, 31 | { "1": "HippopotamusRex", "2": "312118", "3": "1151351" }, 32 | { "1": "Sarconius", "2": "257862", "3": "1181770" }, 33 | { "1": "guineu", "2": "241623", "3": "672597" }, 34 | { "1": "Andrey199650", "2": "240101", "3": "567522" }, 35 | { "1": "Wendigo", "2": "227903", "3": "1099685" }, 36 | { "1": "donutweegee", "2": "204701", "3": "587221" }, 37 | { "1": "AmericanNinja", "2": "202980", "3": "567618" }, 38 | { "1": "Infernum", "2": "202171", "3": "689967" }, 39 | { "1": "FabricioPrie", "2": "196974", "3": "450436" }, 40 | ]; 41 | 42 | server.use( 43 | http.get(`${apiBaseUrl}/API_GetTopTenUsers.php`, () => 44 | HttpResponse.json(mockResponse) 45 | ) 46 | ); 47 | 48 | // ACT 49 | const response = await getTopTenUsers(authorization); 50 | 51 | // ASSERT 52 | expect(response).toEqual([ 53 | { 54 | username: "MaxMilyin", 55 | totalPoints: 346_289, 56 | totalRatioPoints: 995_092, 57 | }, 58 | { 59 | username: "HippopotamusRex", 60 | totalPoints: 312_118, 61 | totalRatioPoints: 1_151_351, 62 | }, 63 | { 64 | username: "Sarconius", 65 | totalPoints: 257_862, 66 | totalRatioPoints: 1_181_770, 67 | }, 68 | { username: "guineu", totalPoints: 241_623, totalRatioPoints: 672_597 }, 69 | { 70 | username: "Andrey199650", 71 | totalPoints: 240_101, 72 | totalRatioPoints: 567_522, 73 | }, 74 | { 75 | username: "Wendigo", 76 | totalPoints: 227_903, 77 | totalRatioPoints: 1_099_685, 78 | }, 79 | { 80 | username: "donutweegee", 81 | totalPoints: 204_701, 82 | totalRatioPoints: 587_221, 83 | }, 84 | { 85 | username: "AmericanNinja", 86 | totalPoints: 202_980, 87 | totalRatioPoints: 567_618, 88 | }, 89 | { 90 | username: "Infernum", 91 | totalPoints: 202_171, 92 | totalRatioPoints: 689_967, 93 | }, 94 | { 95 | username: "FabricioPrie", 96 | totalPoints: 196_974, 97 | totalRatioPoints: 450_436, 98 | }, 99 | ]); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/feed/getTopTenUsers.ts: -------------------------------------------------------------------------------- 1 | import { apiBaseUrl, buildRequestUrl, call } from "../utils/internal"; 2 | import type { AuthObject } from "../utils/public"; 3 | import type { 4 | GetTopTenUsersResponse, 5 | TopTenUsers, 6 | TopTenUsersEntity, 7 | } from "./models"; 8 | 9 | /** 10 | * A call to this function will retrieve the current top ten users 11 | * on the site. 12 | * 13 | * @param authorization An object containing your username and webApiKey. 14 | * This can be constructed with `buildAuthorization()`. 15 | * 16 | * @example 17 | * ``` 18 | * const topTenUsers = await getTopTenUsers(authorization); 19 | * ``` 20 | * 21 | * @returns An array containing the list of top ten users. 22 | * ```json 23 | * [ 24 | * { username: "MockUser", totalPoints: 350000, totalRatioPoints: 995000 }, 25 | * { username: "MockUser2", totalPoints: 345000, totalRatioPoints: 994000 }, 26 | * // ... 27 | * ] 28 | * ``` 29 | */ 30 | export const getTopTenUsers = async ( 31 | authorization: AuthObject 32 | ): Promise => { 33 | const url = buildRequestUrl( 34 | apiBaseUrl, 35 | "/API_GetTopTenUsers.php", 36 | authorization 37 | ); 38 | 39 | const rawTopTenUsers = await call({ url }); 40 | 41 | const sanitizedTopTenUsers: TopTenUsersEntity[] = []; 42 | for (const rawUser of rawTopTenUsers) { 43 | sanitizedTopTenUsers.push({ 44 | username: rawUser["1"], 45 | totalPoints: Number(rawUser["2"]), 46 | totalRatioPoints: Number(rawUser["3"]), 47 | }); 48 | } 49 | 50 | return sanitizedTopTenUsers; 51 | }; 52 | -------------------------------------------------------------------------------- /src/feed/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getAchievementOfTheWeek"; 2 | export * from "./getActiveClaims"; 3 | export * from "./getClaims"; 4 | export * from "./getRecentGameAwards"; 5 | export * from "./getTopTenUsers"; 6 | export * from "./models"; 7 | -------------------------------------------------------------------------------- /src/feed/models/achievement-of-the-week.model.ts: -------------------------------------------------------------------------------- 1 | export interface AchievementOfTheWeek { 2 | achievement: { 3 | id: number; 4 | title: string; 5 | description: string; 6 | points: number; 7 | trueRatio: number; 8 | author: string; 9 | dateCreated: string; 10 | dateModified: string; 11 | badgeName: string; 12 | badgeUrl: string; 13 | }; 14 | 15 | console: { id: number; title: string }; 16 | forumTopic: { id: number }; 17 | game: { id: number; title: string }; 18 | startAt: string; 19 | totalPlayers: number; 20 | 21 | unlocks: Array<{ 22 | user: string; 23 | raPoints: number; 24 | raSoftcorePoints: number; 25 | dateAwarded: string; 26 | hardcoreMode: boolean; 27 | }>; 28 | 29 | unlocksCount: number; 30 | } 31 | -------------------------------------------------------------------------------- /src/feed/models/claim-set-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ClaimSetType { 2 | NewSet = 0, 3 | Revision = 1, 4 | } 5 | -------------------------------------------------------------------------------- /src/feed/models/claim-status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ClaimStatus { 2 | Active = 0, 3 | Complete = 1, 4 | Dropped = 2, 5 | } 6 | -------------------------------------------------------------------------------- /src/feed/models/claim-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ClaimType { 2 | Primary = 0, 3 | Collaboration = 1, 4 | } 5 | -------------------------------------------------------------------------------- /src/feed/models/get-achievement-of-the-week-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GetAchievementOfTheWeekResponse { 2 | Achievement: { 3 | ID: string; 4 | Title: string; 5 | Description: string; 6 | Points: string; 7 | TrueRatio: string; 8 | Author: string; 9 | DateCreated: string; 10 | DateModified: string; 11 | BadgeName: string; 12 | BadgeURL: string; 13 | }; 14 | 15 | Console: { ID: string; Title: string }; 16 | ForumTopic: { ID: string }; 17 | Game: { ID: string; Title: string }; 18 | StartAt: string; 19 | TotalPlayers: string; 20 | 21 | Unlocks: Array<{ 22 | User: string; 23 | RAPoints: string; 24 | RASoftcorePoints: string; 25 | DateAwarded: string; 26 | HardcoreMode: string; 27 | }>; 28 | 29 | UnlocksCount: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/feed/models/get-recent-game-awards-response.model.ts: -------------------------------------------------------------------------------- 1 | import type { AwardKind } from "../../utils/public"; 2 | 3 | export interface GetRecentGameAwardsResponse { 4 | Count: number; 5 | Total: number; 6 | Results: Array<{ 7 | User: string; 8 | AwardKind: AwardKind; 9 | AwardDate: string; 10 | GameID: number; 11 | GameTitle: string; 12 | ConsoleID: number; 13 | ConsoleName: string; 14 | }>; 15 | } 16 | -------------------------------------------------------------------------------- /src/feed/models/get-set-claims-response.model.ts: -------------------------------------------------------------------------------- 1 | interface SetClaimResponseEntity { 2 | ID: number; 3 | User: string; 4 | GameID: number; 5 | GameTitle: string; 6 | GameIcon: string; 7 | ConsoleName: string; 8 | ConsoleID: number; 9 | ClaimType: number; 10 | SetType: number; 11 | Status: number; 12 | Extension: number; 13 | Special: number; 14 | Created: string; 15 | DoneTime: string; 16 | Updated: string; 17 | MinutesLeft: number; 18 | UserIsJrDev: 0 | 1; 19 | } 20 | 21 | export type GetSetClaimsResponse = SetClaimResponseEntity[]; 22 | -------------------------------------------------------------------------------- /src/feed/models/get-top-ten-users-response.model.ts: -------------------------------------------------------------------------------- 1 | interface TopTenUsersResponseEntity { 2 | /** Username */ 3 | "1": string; 4 | 5 | /** Total points earned by the user */ 6 | "2": string; 7 | 8 | /** Total ratio (white) points earned by the user */ 9 | "3": string; 10 | } 11 | 12 | export type GetTopTenUsersResponse = TopTenUsersResponseEntity[]; 13 | -------------------------------------------------------------------------------- /src/feed/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./achievement-of-the-week.model"; 2 | export * from "./claim-set-type.enum"; 3 | export * from "./claim-status.enum"; 4 | export * from "./claim-type.enum"; 5 | export * from "./get-achievement-of-the-week-response.model"; 6 | export * from "./get-recent-game-awards-response.model"; 7 | export * from "./get-set-claims-response.model"; 8 | export * from "./get-top-ten-users-response.model"; 9 | export * from "./recent-game-awards.model"; 10 | export * from "./set-claim.model"; 11 | export * from "./top-ten-users.model"; 12 | export * from "./top-ten-users-entity.model"; 13 | -------------------------------------------------------------------------------- /src/feed/models/recent-game-awards.model.ts: -------------------------------------------------------------------------------- 1 | import type { AwardKind } from "../../utils/public"; 2 | 3 | export interface RecentGameAwards { 4 | count: number; 5 | total: number; 6 | results: Array<{ 7 | user: string; 8 | awardKind: AwardKind; 9 | awardDate: string; 10 | gameId: number; 11 | gameTitle: string; 12 | consoleId: number; 13 | consoleName: string; 14 | }>; 15 | } 16 | -------------------------------------------------------------------------------- /src/feed/models/set-claim.model.ts: -------------------------------------------------------------------------------- 1 | import type { ClaimSetType } from "./claim-set-type.enum"; 2 | import type { ClaimStatus } from "./claim-status.enum"; 3 | import type { ClaimType } from "./claim-type.enum"; 4 | 5 | export interface SetClaim { 6 | /** Unique ID of the claim. */ 7 | id: number; 8 | 9 | /** User who made the claim. */ 10 | user: string; 11 | 12 | /** ID of the claimed game. */ 13 | gameId: number; 14 | 15 | /** Title of the claimed game. */ 16 | gameTitle: string; 17 | 18 | /** Site-relative path of the game's icon image. */ 19 | gameIcon: string; 20 | 21 | /** Console name of the claimed game. */ 22 | consoleName: string; 23 | 24 | /** Console ID of the claimed game. */ 25 | consoleId: number; 26 | 27 | /** Whether the claim is primary or a collaboration. */ 28 | claimType: ClaimType; 29 | 30 | /** Whether the claim is for a new set or a revision. */ 31 | setType: ClaimSetType; 32 | 33 | /** Whether the claim is active, complete, or dropped. */ 34 | status: ClaimStatus; 35 | 36 | /** Number of times the claim has been extended. */ 37 | extension: number; 38 | 39 | /** Flag indicating a special type of claim. */ 40 | special: number; 41 | 42 | /** Date the claim was made. */ 43 | created: string; 44 | 45 | /** 46 | * Date the claim is done. This is an expiration 47 | * date for active claims, completion date for complete 48 | * claims, and dropped date for dropped claims. 49 | */ 50 | doneTime: string; 51 | 52 | /** Date the claim was updated. */ 53 | updated: string; 54 | 55 | /** Time in minutes left until the claim expires. */ 56 | minutesLeft: number; 57 | 58 | /** True if the user is a junior dev. */ 59 | userIsJrDev: boolean; 60 | } 61 | -------------------------------------------------------------------------------- /src/feed/models/top-ten-users-entity.model.ts: -------------------------------------------------------------------------------- 1 | export interface TopTenUsersEntity { 2 | username: string; 3 | totalPoints: number; 4 | totalRatioPoints: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/feed/models/top-ten-users.model.ts: -------------------------------------------------------------------------------- 1 | import type { TopTenUsersEntity } from "./top-ten-users-entity.model"; 2 | 3 | export type TopTenUsers = TopTenUsersEntity[]; 4 | -------------------------------------------------------------------------------- /src/game/getAchievementCount.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getAchievementCount } from "./getAchievementCount"; 7 | import type { GetAchievementCountResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getAchievementCount", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getAchievementCount).toBeDefined(); 20 | }); 21 | 22 | it("given a game ID, retrieves the list of achievement IDs associated with the game and cleans properties", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetAchievementCountResponse = { 30 | GameID: 8, 31 | AchievementIDs: [1, 2, 3, 4, 5], 32 | }; 33 | 34 | server.use( 35 | http.get(`${apiBaseUrl}/API_GetAchievementCount.php`, () => 36 | HttpResponse.json(mockResponse) 37 | ) 38 | ); 39 | 40 | // ACT 41 | const response = await getAchievementCount(authorization, { gameId: 8 }); 42 | 43 | // ASSERT 44 | expect(response).toEqual({ 45 | gameId: 8, 46 | achievementIds: [1, 2, 3, 4, 5], 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/game/getAchievementCount.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { AchievementCount, GetAchievementCountResponse } from "./models"; 10 | 11 | /** 12 | * A call to this function will retrieve the list of 13 | * achievement IDs for a game, targeted by game ID. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.gameId The unique game ID. If you are unsure, open the 19 | * game's page on the RetroAchievements.org website. For example, Dragster's 20 | * URL is https://retroachievements.org/game/14402. We can see from the 21 | * URL that the game ID is "14402". 22 | * 23 | * @example 24 | * ``` 25 | * const achievementCount = await getAchievementCount( 26 | * authorization, 27 | * { gameId: 14402 } 28 | * ); 29 | * ``` 30 | * 31 | * @returns An object containing a gameId and a list of 32 | * achievementIds. 33 | * ``` 34 | * { gameId: 14402, achievementIds: [1,2,3,4,5] } 35 | * ``` 36 | */ 37 | export const getAchievementCount = async ( 38 | authorization: AuthObject, 39 | payload: { gameId: ID } 40 | ): Promise => { 41 | const { gameId } = payload; 42 | 43 | const url = buildRequestUrl( 44 | apiBaseUrl, 45 | "/API_GetAchievementCount.php", 46 | authorization, 47 | { i: gameId } 48 | ); 49 | 50 | const rawResponse = await call({ url }); 51 | 52 | return serializeProperties(rawResponse); 53 | }; 54 | -------------------------------------------------------------------------------- /src/game/getAchievementDistribution.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { apiBaseUrl, buildRequestUrl, call } from "../utils/internal"; 3 | import type { AuthObject } from "../utils/public"; 4 | import type { 5 | AchievementDistributionFlags, 6 | GetAchievementDistributionResponse, 7 | } from "./models"; 8 | 9 | /** 10 | * A call to this function will retrieve a dictionary 11 | * of the number of players who have earned a specific 12 | * number of achievements for a given game ID. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @param payload.gameId The unique game ID. If you are unsure, open the 18 | * game's page on the RetroAchievements.org website. For example, Dragster's 19 | * URL is https://retroachievements.org/game/14402. We can see from the 20 | * URL that the game ID is "14402". 21 | * 22 | * @param payload.flags Optional. By default, only official achievement 23 | * tallies are returned in the response. Import the `AchievementDistributionFlags` 24 | * enum for possible values. This lets you see the count of players who have 25 | * unlocked unofficial achievements. 26 | * 27 | * @param payload.hardcore Optional. By default, set to false, with both 28 | * softcore and hardcore tallies returned in the response. If this option 29 | * is set to true, only hardcore unlocks will be included in the totals. 30 | * 31 | * @example 32 | * ``` 33 | * const achievementDistribution = await getAchievementDistribution( 34 | * authorization, 35 | * { gameId: 14402, hardcore: true } 36 | * ); 37 | * ``` 38 | * 39 | * @returns A dictionary where the keys represent the earned achievement 40 | * count and the values represent the number of players who have unlocked 41 | * that many achievements. 42 | * ``` 43 | * { 44 | * '1': 64, 45 | * '2': 19, 46 | * '3': 11, 47 | * '4': 18, 48 | * '5': 25, 49 | * '6': 20, 50 | * '7': 26, 51 | * '8': 29, 52 | * '9': 54, 53 | * '10': 17, 54 | * '11': 29, 55 | * '12': 4 56 | * } 57 | * ``` 58 | */ 59 | export const getAchievementDistribution = async ( 60 | authorization: AuthObject, 61 | payload: { 62 | gameId: ID; 63 | flags?: AchievementDistributionFlags; 64 | hardcore?: boolean; 65 | } 66 | ) => { 67 | const { gameId, flags, hardcore } = payload; 68 | 69 | const queryParams: Record = { i: gameId }; 70 | 71 | if (flags !== undefined) { 72 | queryParams["f"] = flags; 73 | } 74 | 75 | if (hardcore !== undefined) { 76 | queryParams["h"] = hardcore === true ? 1 : 0; 77 | } 78 | 79 | const url = buildRequestUrl( 80 | apiBaseUrl, 81 | "/API_GetAchievementDistribution.php", 82 | authorization, 83 | queryParams 84 | ); 85 | 86 | return await call({ 87 | url, 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /src/game/getGame.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getGame } from "./getGame"; 9 | import type { GetGameResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getGame", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getGame).toBeDefined(); 22 | }); 23 | 24 | it("given a game ID, retrieves basic metadata about the game", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetGameResponse = { 32 | ID: "14402", 33 | Title: "Dragster", 34 | ForumTopicID: "9145", 35 | ConsoleID: "25", 36 | ConsoleName: "Atari 2600", 37 | Flags: "0", 38 | ImageIcon: "/Images/026368.png", 39 | GameIcon: "/Images/026368.png", 40 | ImageTitle: "/Images/026366.png", 41 | ImageIngame: "/Images/026367.png", 42 | ImageBoxArt: "/Images/026365.png", 43 | Publisher: "Activision ", 44 | Developer: "David Crane", 45 | Genre: "Racing", 46 | Released: "1980", 47 | GameTitle: "Dragster", 48 | Console: "Atari 2600", 49 | }; 50 | 51 | server.use( 52 | http.get(`${apiBaseUrl}/API_GetGame.php`, () => 53 | HttpResponse.json(mockResponse) 54 | ) 55 | ); 56 | 57 | // ACT 58 | const response = await getGame(authorization, { gameId: 14_402 }); 59 | 60 | // ASSERT 61 | expect(response).toEqual({ 62 | id: 14_402, 63 | title: "Dragster", 64 | forumTopicId: 9145, 65 | consoleId: 25, 66 | consoleName: "Atari 2600", 67 | flags: 0, 68 | imageIcon: "/Images/026368.png", 69 | gameIcon: "/Images/026368.png", 70 | imageTitle: "/Images/026366.png", 71 | imageIngame: "/Images/026367.png", 72 | imageBoxArt: "/Images/026365.png", 73 | publisher: "Activision ", 74 | developer: "David Crane", 75 | genre: "Racing", 76 | released: "1980", 77 | gameTitle: "Dragster", 78 | console: "Atari 2600", 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/game/getGame.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { Game, GetGameResponse } from "./models"; 10 | 11 | /** 12 | * A call to this function will retrieve basic metadata about 13 | * a game, targeted via its unique ID. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.gameId The unique game ID. If you are unsure, open the 19 | * game's page on the RetroAchievements.org website. For example, Dragster's 20 | * URL is https://retroachievements.org/game/14402. We can see from the 21 | * URL that the game ID is "14402". 22 | * 23 | * @example 24 | * ``` 25 | * const game = await getGame( 26 | * authorization, 27 | * { gameId: 14402 } 28 | * ); 29 | * ``` 30 | * 31 | * @returns An object containing basic metadata about a target game. 32 | * ```json 33 | * { 34 | * id: 14402, 35 | * title: "Dragster", 36 | * forumTopicId: 9145, 37 | * consoleId: 25, 38 | * consoleName: "Atari 2600", 39 | * flags: 0, 40 | * imageIcon: "/Images/026368.png", 41 | * gameIcon: "/Images/026368.png", 42 | * imageTitle: "/Images/026366.png", 43 | * imageIngame: "/Images/026367.png", 44 | * imageBoxArt: "/Images/026365.png", 45 | * publisher: "Activision", 46 | * developer: "David Crane", 47 | * genre: "Racing", 48 | * released: "1980", 49 | * gameTitle: "Dragster", 50 | * console: "Atari 2600" 51 | * } 52 | * ``` 53 | */ 54 | export const getGame = async ( 55 | authorization: AuthObject, 56 | payload: { gameId: ID } 57 | ): Promise => { 58 | const { gameId } = payload; 59 | 60 | const url = buildRequestUrl(apiBaseUrl, "/API_GetGame.php", authorization, { 61 | i: gameId, 62 | }); 63 | 64 | const rawResponse = await call({ url }); 65 | 66 | return serializeProperties(rawResponse, { 67 | shouldCastToNumbers: ["ID", "ForumTopicID", "ConsoleID", "Flags"], 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /src/game/getGameExtended.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { GameExtended, GetGameExtendedResponse } from "./models"; 10 | 11 | /** 12 | * A call to this function will retrieve extended metadata 13 | * about a game, targeted via its unique ID. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.gameId The unique game ID. If you are unsure, open the 19 | * game's page on the RetroAchievements.org website. For example, Dragster's 20 | * URL is https://retroachievements.org/game/14402. We can see from the 21 | * URL that the game ID is "14402". 22 | * 23 | * @example 24 | * ``` 25 | * const gameExtended = await getGameExtended( 26 | * authorization, 27 | * { gameId: 14402 } 28 | * ); 29 | * ``` 30 | * 31 | * @returns An object containing extended metadata about a target game. 32 | * ```json 33 | * { 34 | * id: 14402, 35 | * title: "Dragster", 36 | * consoleId: 25, 37 | * forumTopicId: 9145, 38 | * flags: 0, 39 | * imageIcon: "/Images/026368.png", 40 | * imageTitle: "/Images/026366.png", 41 | * imageIngame: "/Images/026367.png", 42 | * imageBoxArt: "/Images/026365.png", 43 | * publisher: "Activision", 44 | * developer: "David Crane", 45 | * genre: "Racing", 46 | * released: "1980", 47 | * isFinal: false, 48 | * consoleName: "Atari 2600", 49 | * richPresencePatch: "2b92fa1bf9635c303b3b7f8feea3ed3c", 50 | * numAchievements: 12, 51 | * numDistinctPlayersCasual: 454, 52 | * numDistinctPlayersHardcore: 323, 53 | * claims: [], 54 | * achievements: { 55 | * '79434': { 56 | * id: 79434, 57 | * numAwarded: 338, 58 | * numAwardedHardcore: 253, 59 | * title: "Novice Dragster Driver 1", 60 | * description: "Complete your very first race in game 1.", 61 | * points: 1, 62 | * trueRatio: 1, 63 | * author: "Boldewin", 64 | * dateModified: "2019-08-01 19:03:46", 65 | * dateCreated: "2019-07-31 18:49:57", 66 | * badgeName: "85541", 67 | * displayOrder: 0, 68 | * memAddr: "f5c41fa0b5fa0d5fbb8a74c598f18582" 69 | * } 70 | * } 71 | * } 72 | * ``` 73 | */ 74 | export const getGameExtended = async ( 75 | authorization: AuthObject, 76 | payload: { gameId: ID; isRequestingUnofficialAchievements?: boolean } 77 | ): Promise => { 78 | const { gameId, isRequestingUnofficialAchievements } = payload; 79 | 80 | const params: Record = { 81 | i: gameId, 82 | }; 83 | 84 | if (isRequestingUnofficialAchievements) { 85 | params["f"] = 5; 86 | } 87 | 88 | const url = buildRequestUrl( 89 | apiBaseUrl, 90 | "/API_GetGameExtended.php", 91 | authorization, 92 | params 93 | ); 94 | 95 | const rawResponse = await call({ url }); 96 | 97 | return serializeProperties(rawResponse, { 98 | shouldCastToNumbers: [ 99 | "ID", 100 | "NumAwarded", 101 | "NumAwardedHardcore", 102 | "Points", 103 | "TrueRatio", 104 | "DisplayOrder", 105 | "NumDistinctPlayersCasual", 106 | "NumDistinctPlayersHardcore", 107 | ], 108 | }); 109 | }; 110 | -------------------------------------------------------------------------------- /src/game/getGameHashes.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getGameHashes } from "./getGameHashes"; 9 | import type { GetGameHashesResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getGameHashes", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getGameHashes).toBeDefined(); 22 | }); 23 | 24 | it("given a game ID, retrieves a list of linked hashes", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetGameHashesResponse = { 32 | Results: [ 33 | { 34 | Name: "Sonic The Hedgehog (USA, Europe) (Ru) (NewGame).md", 35 | MD5: "1b1d9ac862c387367e904036114c4825", 36 | Labels: ["nointro", "rapatches"], 37 | PatchUrl: 38 | "https://github.com/RetroAchievements/RAPatches/raw/main/MD/Translation/Russian/1-Sonic1-Russian.zip", 39 | }, 40 | { 41 | Name: "Sonic The Hedgehog (USA, Europe).md", 42 | MD5: "1bc674be034e43c96b86487ac69d9293", 43 | Labels: ["nointro"], 44 | PatchUrl: null, 45 | }, 46 | ], 47 | }; 48 | 49 | server.use( 50 | http.get(`${apiBaseUrl}/API_GetGameHashes.php`, () => 51 | HttpResponse.json(mockResponse) 52 | ) 53 | ); 54 | 55 | // ACT 56 | const response = await getGameHashes(authorization, { gameId: 1 }); 57 | 58 | // ASSERT 59 | expect(response).toEqual({ 60 | results: [ 61 | { 62 | name: "Sonic The Hedgehog (USA, Europe) (Ru) (NewGame).md", 63 | md5: "1b1d9ac862c387367e904036114c4825", 64 | labels: ["nointro", "rapatches"], 65 | patchUrl: 66 | "https://github.com/RetroAchievements/RAPatches/raw/main/MD/Translation/Russian/1-Sonic1-Russian.zip", 67 | }, 68 | { 69 | name: "Sonic The Hedgehog (USA, Europe).md", 70 | md5: "1bc674be034e43c96b86487ac69d9293", 71 | labels: ["nointro"], 72 | patchUrl: null, 73 | }, 74 | ], 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/game/getGameHashes.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { GameHashes, GetGameHashesResponse } from "./models"; 10 | 11 | /** 12 | * A call to this function will retrieve a list of hashes linked to a game. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @param payload.gameId The unique game ID. If you are unsure, open the 18 | * game's page on the RetroAchievements.org website. For example, Dragster's 19 | * URL is https://retroachievements.org/game/14402. We can see from the 20 | * URL that the game ID is "14402". 21 | * 22 | * @example 23 | * ``` 24 | * const game = await getGameHashes( 25 | * authorization, 26 | * { gameId: 14402 } 27 | * ); 28 | * ``` 29 | * 30 | * @returns An object containing a list of game hashes. 31 | * ```json 32 | * { 33 | * "results": [ 34 | * { 35 | * "md5": "1b1d9ac862c387367e904036114c4825", 36 | * "name": "Sonic The Hedgehog (USA, Europe) (Ru) (NewGame).md", 37 | * "labels": ["nointro", "rapatches"], 38 | * "patchUrl": "https://github.com/RetroAchievements/RAPatches/raw/main/MD/Translation/Russian/1-Sonic1-Russian.zip" 39 | * }, 40 | * { 41 | * "md5": "1bc674be034e43c96b86487ac69d9293", 42 | * "name": "Sonic The Hedgehog (USA, Europe).md", 43 | * "labels": ["nointro"], 44 | * "patchUrl": null 45 | * } 46 | * ] 47 | * } 48 | * ``` 49 | */ 50 | export const getGameHashes = async ( 51 | authorization: AuthObject, 52 | payload: { gameId: ID } 53 | ): Promise => { 54 | const { gameId } = payload; 55 | 56 | const url = buildRequestUrl( 57 | apiBaseUrl, 58 | "/API_GetGameHashes.php", 59 | authorization, 60 | { i: gameId } 61 | ); 62 | 63 | const rawResponse = await call({ url }); 64 | 65 | return serializeProperties(rawResponse); 66 | }; 67 | -------------------------------------------------------------------------------- /src/game/getGameRankAndScore.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getGameRankAndScore } from "./getGameRankAndScore"; 9 | import type { GetGameRankAndScoreResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getGameRankAndScore", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getGameRankAndScore).toBeDefined(); 22 | }); 23 | 24 | it("given a game ID, retrieves metadata about latest masteries for a game", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetGameRankAndScoreResponse = [ 32 | { 33 | User: "Arekdias", 34 | TotalScore: "189", 35 | LastAward: "2020-10-10 22:43:32", 36 | Rank: 1, 37 | }, 38 | ]; 39 | 40 | server.use( 41 | http.get(`${apiBaseUrl}/API_GetGameRankAndScore.php`, () => 42 | HttpResponse.json(mockResponse) 43 | ) 44 | ); 45 | 46 | // ACT 47 | const response = await getGameRankAndScore(authorization, { 48 | gameId: 14_402, 49 | type: "high-scores", 50 | }); 51 | 52 | // ASSERT 53 | expect(response).toEqual([ 54 | { 55 | user: "Arekdias", 56 | totalScore: 189, 57 | lastAward: "2020-10-10 22:43:32", 58 | rank: 1, 59 | }, 60 | ]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/game/getGameRankAndScore.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { 10 | GameRankAndScoreEntity, 11 | GetGameRankAndScoreResponse, 12 | } from "./models"; 13 | 14 | /** 15 | * A call to this function will retrieve metadata about 16 | * either the latest masters for a game, or the highest 17 | * points earners for a game. The game is targeted via 18 | * its unique ID. 19 | * 20 | * @param authorization An object containing your username and webApiKey. 21 | * This can be constructed with `buildAuthorization()`. 22 | * 23 | * @param payload.gameId The unique game ID. If you are unsure, open the 24 | * game's page on the RetroAchievements.org website. For example, Dragster's 25 | * URL is https://retroachievements.org/game/14402. We can see from the 26 | * URL that the game ID is "14402". 27 | * 28 | * @param payload.type Can either be "latest-masters" or "high-scores". 29 | * 30 | * @example 31 | * ``` 32 | * const gameRankAndScore = await getGameRankAndScore( 33 | * authorization, 34 | * { gameId: 14402, type: "latest-masters" } 35 | * ); 36 | * ``` 37 | * 38 | * @returns An array containing a list of latest masters or 39 | * high score earners for a given game ID. 40 | * ```json 41 | * [ 42 | * { user: 'Arekdias', totalScore: 189, lastAward: '2020-10-10 22:43:32' } 43 | * ] 44 | * ``` 45 | */ 46 | export const getGameRankAndScore = async ( 47 | authorization: AuthObject, 48 | payload: { gameId: ID; type: "latest-masters" | "high-scores" } 49 | ): Promise => { 50 | const { gameId, type } = payload; 51 | 52 | const url = buildRequestUrl( 53 | apiBaseUrl, 54 | "/API_GetGameRankAndScore.php", 55 | authorization, 56 | { 57 | g: gameId, 58 | t: type === "latest-masters" ? 1 : 0, 59 | } 60 | ); 61 | 62 | const rawResponse = await call({ url }); 63 | 64 | return serializeProperties(rawResponse, { 65 | shouldCastToNumbers: ["TotalScore"], 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /src/game/getGameRating.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getGameRating } from "./getGameRating"; 7 | import type { GetGameRatingResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getGameRating", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getGameRating).toBeDefined(); 20 | }); 21 | 22 | it("given a game ID, retrieves metadata about how users have rated it", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetGameRatingResponse = { 30 | GameID: 14_402, 31 | Ratings: { 32 | Game: 3.1875, 33 | Achievements: 0, 34 | GameNumVotes: 16, 35 | AchievementsNumVotes: 0, 36 | }, 37 | }; 38 | 39 | server.use( 40 | http.get(`${apiBaseUrl}/API_GetGameRating.php`, () => 41 | HttpResponse.json(mockResponse) 42 | ) 43 | ); 44 | 45 | // ACT 46 | const response = await getGameRating(authorization, { gameId: 14_402 }); 47 | 48 | // ASSERT 49 | expect(response).toEqual({ 50 | gameId: 14_402, 51 | ratings: { 52 | game: 3.1875, 53 | achievements: 0, 54 | gameNumVotes: 16, 55 | achievementsNumVotes: 0, 56 | }, 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/game/getGameRating.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { GameRating, GetGameRatingResponse } from "./models"; 10 | 11 | /** 12 | * A call to this function will retrieve metadata about 13 | * how users have rated the game and its set. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.gameId The unique game ID. If you are unsure, open the 19 | * game's page on the RetroAchievements.org website. For example, Dragster's 20 | * URL is https://retroachievements.org/game/14402. We can see from the 21 | * URL that the game ID is "14402". 22 | * 23 | * @example 24 | * ``` 25 | * const gameRating = await getGameRating( 26 | * authorization, 27 | * { gameId: 14402 } 28 | * ); 29 | * ``` 30 | * 31 | * @returns An object with game rating metadata. 32 | * ```json 33 | * { 34 | * gameId: 14402, 35 | * ratings: { 36 | * game: 3.1875, 37 | * achievements: 0, 38 | * gameNumVotes: 16, 39 | * achievementsNumVotes: 0 40 | * } 41 | * } 42 | * ``` 43 | */ 44 | export const getGameRating = async ( 45 | authorization: AuthObject, 46 | payload: { gameId: ID } 47 | ): Promise => { 48 | const { gameId } = payload; 49 | 50 | const url = buildRequestUrl( 51 | apiBaseUrl, 52 | "/API_GetGameRating.php", 53 | authorization, 54 | { i: gameId } 55 | ); 56 | 57 | const rawResponse = await call({ url }); 58 | 59 | return serializeProperties(rawResponse); 60 | }; 61 | -------------------------------------------------------------------------------- /src/game/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getAchievementCount"; 2 | export * from "./getAchievementDistribution"; 3 | export * from "./getGame"; 4 | export * from "./getGameExtended"; 5 | export * from "./getGameHashes"; 6 | export * from "./getGameRankAndScore"; 7 | export * from "./getGameRating"; 8 | export * from "./models"; 9 | -------------------------------------------------------------------------------- /src/game/models/achievement-count.model.ts: -------------------------------------------------------------------------------- 1 | export interface AchievementCount { 2 | gameId: number; 3 | achievementIds: number[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/game/models/achievement-distribution-flags.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AchievementDistributionFlags { 2 | CoreAchievements = 3, 3 | UnofficialAchievements = 5, 4 | } 5 | -------------------------------------------------------------------------------- /src/game/models/game-extended-achievement-entity.model.ts: -------------------------------------------------------------------------------- 1 | import type { AchievementType } from "../../achievement"; 2 | 3 | export interface GameExtendedAchievementEntity { 4 | id: number; 5 | numAwarded: number; 6 | numAwardedHardcore: number; 7 | title: string; 8 | description: string; 9 | points: number; 10 | trueRatio: number; 11 | author: string; 12 | dateModified: string; 13 | dateCreated: string; 14 | badgeName: string; 15 | displayOrder: number; 16 | memAddr: string; 17 | type: AchievementType; 18 | } 19 | -------------------------------------------------------------------------------- /src/game/models/game-extended-claim-entity.model.ts: -------------------------------------------------------------------------------- 1 | export interface GameExtendedClaimEntity { 2 | user: string; 3 | setType: number; 4 | claimType: number; 5 | created: string; 6 | expiration: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/game/models/game-extended.model.ts: -------------------------------------------------------------------------------- 1 | import type { GameExtendedAchievementEntity } from "./game-extended-achievement-entity.model"; 2 | import type { GameExtendedClaimEntity } from "./game-extended-claim-entity.model"; 3 | 4 | export interface GameExtended { 5 | id: number; 6 | title: string; 7 | consoleId: number; 8 | forumTopicId: number; 9 | flags: number; 10 | imageIcon: string; 11 | imageTitle: string; 12 | imageIngame: string; 13 | imageBoxArt: string; 14 | publisher: string; 15 | developer: string; 16 | genre: string; 17 | released: string; 18 | isFinal: boolean; 19 | consoleName: string; 20 | richPresencePatch: string; 21 | numAchievements: number; 22 | numDistinctPlayersCasual: number; 23 | numDistinctPlayersHardcore: number; 24 | claims: GameExtendedClaimEntity[]; 25 | achievements: Record; 26 | } 27 | -------------------------------------------------------------------------------- /src/game/models/game-hashes.model.ts: -------------------------------------------------------------------------------- 1 | interface GameHash { 2 | md5: string; 3 | name: string; 4 | labels: string[]; 5 | patchUrl: string; 6 | } 7 | 8 | export interface GameHashes { 9 | results: GameHash[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/game/models/game-rank-and-score-entity.model.ts: -------------------------------------------------------------------------------- 1 | export interface GameRankAndScoreEntity { 2 | user: string; 3 | totalScore: number; 4 | lastAward: string; 5 | 6 | rank?: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/game/models/game-rating.model.ts: -------------------------------------------------------------------------------- 1 | export interface GameRating { 2 | gameId: number; 3 | ratings: { 4 | game: number; 5 | achievements: number; 6 | gameNumVotes: number; 7 | achievementsNumVotes: number; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/game/models/game.model.ts: -------------------------------------------------------------------------------- 1 | export interface Game { 2 | id: number; 3 | title: string; 4 | forumTopicId: number; 5 | consoleId: number; 6 | consoleName: string; 7 | flags: number; 8 | imageIcon: string; 9 | gameIcon: string; 10 | imageTitle: string; 11 | imageIngame: string; 12 | imageBoxArt: string; 13 | publisher: string; 14 | developer: string; 15 | genre: string; 16 | released: string; 17 | gameTitle: string; 18 | console: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/game/models/get-achievement-count-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GetAchievementCountResponse { 2 | GameID: number; 3 | AchievementIDs: number[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/game/models/get-achievement-distribution-response.model.ts: -------------------------------------------------------------------------------- 1 | export type GetAchievementDistributionResponse = Record<`${number}`, number>; 2 | -------------------------------------------------------------------------------- /src/game/models/get-game-extended-response.model.ts: -------------------------------------------------------------------------------- 1 | // NOTE: This cannot be a true extension of the `GetGameResponse` 2 | // interface because the return types for many of these fields 3 | // are different from the actual RA API. 4 | 5 | enum GameExtendedClaimType { 6 | Primary = "0", 7 | Collaboration = "1", 8 | } 9 | 10 | export interface GameExtendedRawAchievementEntity { 11 | ID: string; 12 | NumAwarded: string; 13 | NumAwardedHardcore: string; 14 | Title: string; 15 | Description: string; 16 | Points: string; 17 | TrueRatio: string; 18 | Author: string; 19 | DateModified: string; 20 | DateCreated: string; 21 | BadgeName: string; 22 | DisplayOrder: string; 23 | MemAddr: string; 24 | } 25 | 26 | interface GameExtendedRawClaimEntity { 27 | User: string; 28 | SetType: string; 29 | ClaimType: GameExtendedClaimType; 30 | Created: string; 31 | Expiration: string; 32 | } 33 | 34 | export interface GetGameExtendedResponse { 35 | ID: number; 36 | Title: string; 37 | ConsoleID: number; 38 | ForumTopicID: number; 39 | Flags: number; 40 | ImageIcon: string; 41 | ImageTitle: string; 42 | ImageIngame: string; 43 | ImageBoxArt: string; 44 | Publisher: string; 45 | Developer: string; 46 | Genre: string; 47 | Released: string; 48 | IsFinal: boolean; 49 | ConsoleName: string; 50 | RichPresencePatch: string; 51 | NumAchievements: number; 52 | NumDistinctPlayersCasual: string; 53 | NumDistinctPlayersHardcore: string; 54 | Claims: GameExtendedRawClaimEntity[]; 55 | Achievements: Record | []; 56 | } 57 | -------------------------------------------------------------------------------- /src/game/models/get-game-hashes-response.model.ts: -------------------------------------------------------------------------------- 1 | interface GameHashResult { 2 | MD5: string; 3 | Name: string; 4 | Labels: string[]; 5 | PatchUrl: string; 6 | } 7 | 8 | export interface GetGameHashesResponse { 9 | Results: GameHashResult[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/game/models/get-game-rank-and-score-response.model.ts: -------------------------------------------------------------------------------- 1 | interface RawGameRankAndScoreEntity { 2 | User: string; 3 | TotalScore: string; 4 | LastAward: string; 5 | Rank: number; 6 | } 7 | 8 | export type GetGameRankAndScoreResponse = RawGameRankAndScoreEntity[]; 9 | -------------------------------------------------------------------------------- /src/game/models/get-game-rating-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GetGameRatingResponse { 2 | GameID: number; 3 | Ratings: { 4 | Game: number; 5 | Achievements: number; 6 | GameNumVotes: number; 7 | AchievementsNumVotes: number; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/game/models/get-game-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GetGameResponse { 2 | ID: string; 3 | Title: string; 4 | ForumTopicID: string; 5 | ConsoleID: string; 6 | ConsoleName: string; 7 | Flags: string; 8 | ImageIcon: string; 9 | GameIcon: string; 10 | ImageTitle: string; 11 | ImageIngame: string; 12 | ImageBoxArt: string; 13 | Publisher: string; 14 | Developer: string; 15 | Genre: string; 16 | Released: string; 17 | GameTitle: string; 18 | Console: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/game/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./achievement-count.model"; 2 | export * from "./achievement-distribution-flags.enum"; 3 | export * from "./game.model"; 4 | export * from "./game-extended.model"; 5 | export * from "./game-extended-achievement-entity.model"; 6 | export * from "./game-extended-claim-entity.model"; 7 | export * from "./game-hashes.model"; 8 | export * from "./game-rank-and-score-entity.model"; 9 | export * from "./game-rating.model"; 10 | export * from "./get-achievement-count-response.model"; 11 | export * from "./get-achievement-distribution-response.model"; 12 | export * from "./get-game-extended-response.model"; 13 | export * from "./get-game-hashes-response.model"; 14 | export * from "./get-game-rank-and-score-response.model"; 15 | export * from "./get-game-rating-response.model"; 16 | export * from "./get-game-response.model"; 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // This file is the single public-facing API of the entire library. 2 | 3 | export * from "./achievement"; 4 | export * from "./comment"; 5 | export * from "./console"; 6 | export * from "./feed"; 7 | export * from "./game"; 8 | export * from "./ticket"; 9 | export * from "./user"; 10 | export * from "./utils/public"; 11 | -------------------------------------------------------------------------------- /src/set-version.js: -------------------------------------------------------------------------------- 1 | import packageJson from "../package.json" assert { type: "json" }; 2 | 3 | const { version } = packageJson; 4 | 5 | process.env.PACKAGE_VERSION = version; 6 | console.log(`ℹ️ Set User-Agent header version variable to ${version}`); 7 | -------------------------------------------------------------------------------- /src/ticket/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getTicketData"; 2 | export * from "./models"; 3 | -------------------------------------------------------------------------------- /src/ticket/models/achievement-ticket-stats-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface AchievementTicketStatsResponse { 2 | AchievementID: number; 3 | AchievementTitle: string; 4 | AchievementDescription: string; 5 | URL: string; 6 | OpenTickets: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/ticket/models/achievement-ticket-stats.model.ts: -------------------------------------------------------------------------------- 1 | export interface AchievementTicketStats { 2 | achievementId: number; 3 | achievementTitle: string; 4 | achievementDescription: string; 5 | url: string; 6 | openTickets: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/ticket/models/game-ticket-stats.model.ts: -------------------------------------------------------------------------------- 1 | import type { TicketEntity } from "./ticket-entity.model"; 2 | 3 | export interface GameTicketStats { 4 | gameId: number; 5 | gameTitle: string; 6 | consoleName: string; 7 | openTickets: number; 8 | url: string; 9 | 10 | tickets?: TicketEntity[]; 11 | } 12 | -------------------------------------------------------------------------------- /src/ticket/models/game-tickets-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GameTicketsResponse { 2 | GameID: number; 3 | GameTitle: string; 4 | ConsoleName: string; 5 | OpenTickets: number; 6 | URL: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/ticket/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./achievement-ticket-stats.model"; 2 | export * from "./achievement-ticket-stats-response.model"; 3 | export * from "./game-ticket-stats.model"; 4 | export * from "./game-tickets-response.model"; 5 | export * from "./most-ticketed-games.model"; 6 | export * from "./most-ticketed-games-response.model"; 7 | export * from "./recent-tickets.model"; 8 | export * from "./recent-tickets-response.model"; 9 | export * from "./response-ticket-entity.model"; 10 | export * from "./ticket-entity.model"; 11 | export * from "./tickets-by-user-response.model"; 12 | export * from "./user-ticket-stats.model"; 13 | -------------------------------------------------------------------------------- /src/ticket/models/most-ticketed-games-response.model.ts: -------------------------------------------------------------------------------- 1 | interface ReportedGameEntity { 2 | GameID: string; 3 | GameTitle: string; 4 | GameIcon: string; 5 | Console: string; 6 | OpenTickets: string; 7 | } 8 | 9 | export interface MostTicketedGamesResponse { 10 | MostReportedGames: ReportedGameEntity[]; 11 | URL: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/ticket/models/most-ticketed-games.model.ts: -------------------------------------------------------------------------------- 1 | export interface ReportedGameEntity { 2 | gameId: number; 3 | gameTitle: string; 4 | gameIcon: string; 5 | console: string; 6 | openTickets: number; 7 | } 8 | 9 | export interface MostTicketedGames { 10 | mostReportedGames: ReportedGameEntity[]; 11 | url: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/ticket/models/recent-tickets-response.model.ts: -------------------------------------------------------------------------------- 1 | import type { ResponseTicketEntity } from "./response-ticket-entity.model"; 2 | 3 | export interface RecentTicketsResponse { 4 | RecentTickets: ResponseTicketEntity[]; 5 | OpenTickets: number; 6 | URL: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/ticket/models/recent-tickets.model.ts: -------------------------------------------------------------------------------- 1 | import type { TicketEntity } from "./ticket-entity.model"; 2 | 3 | export interface RecentTickets { 4 | recentTickets: TicketEntity[]; 5 | openTickets: number; 6 | url: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/ticket/models/response-ticket-entity.model.ts: -------------------------------------------------------------------------------- 1 | export interface ResponseTicketEntity { 2 | ID: string; 3 | AchievementID: string; 4 | AchievementTitle: string; 5 | AchievementDesc: string; 6 | Points: string; 7 | BadgeName: string; 8 | AchievementAuthor: string; 9 | GameID: string; 10 | ConsoleName: string; 11 | GameTitle: string; 12 | GameIcon: string; 13 | ReportedAt: string; 14 | ReportType: string; 15 | ReportState: string; 16 | Hardcore: "0" | "1" | null; 17 | ReportNotes: string; 18 | ReportedBy: string; 19 | ResolvedAt: string | null; 20 | ResolvedBy: string | null; 21 | ReportStateDescription: string; 22 | ReportTypeDescription: string; 23 | 24 | URL?: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/ticket/models/ticket-entity.model.ts: -------------------------------------------------------------------------------- 1 | export interface TicketEntity { 2 | id: number; 3 | achievementId: number; 4 | achievementTitle: string; 5 | achievementDesc: string; 6 | points: number; 7 | badgeName: string; 8 | achievementAuthor: string; 9 | gameId: number; 10 | consoleName: string; 11 | gameTitle: string; 12 | gameIcon: string; 13 | reportedAt: string; 14 | reportType: number; 15 | reportState: number; 16 | hardcore: boolean | null; 17 | reportNotes: string; 18 | reportedBy: string; 19 | resolvedAt: string | null; 20 | resolvedBy: string | null; 21 | reportStateDescription: string; 22 | reportTypeDescription: string; 23 | url: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/ticket/models/tickets-by-user-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface TicketsByUserResponse { 2 | User: string; 3 | Open: number; 4 | Closed: number; 5 | Resolved: number; 6 | Total: number; 7 | URL: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/ticket/models/user-ticket-stats.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserTicketStats { 2 | user: string; 3 | open: number; 4 | closed: number; 5 | resolved: number; 6 | total: number; 7 | url: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/user/getAchievementsEarnedBetween.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getAchievementsEarnedBetween } from "./getAchievementsEarnedBetween"; 7 | import type { 8 | DatedUserAchievement, 9 | DatedUserAchievementsResponse, 10 | } from "./models"; 11 | 12 | const server = setupServer(); 13 | 14 | describe("Function: getAchievementsEarnedBetween", () => { 15 | // MSW Setup 16 | beforeAll(() => server.listen()); 17 | afterEach(() => server.resetHandlers()); 18 | afterAll(() => server.close()); 19 | 20 | it("is defined #sanity", () => { 21 | // ASSERT 22 | expect(getAchievementsEarnedBetween).toBeDefined(); 23 | }); 24 | 25 | it("retrieves a list of user achievements earned between a set of dates", async () => { 26 | // ARRANGE 27 | const authorization = buildAuthorization({ 28 | username: "mockUserName", 29 | webApiKey: "mockWebApiKey", 30 | }); 31 | 32 | const mockResponse: DatedUserAchievementsResponse = [ 33 | { 34 | Date: "2022-10-12 07:36:31", 35 | HardcoreMode: "1", 36 | AchievementID: "173356", 37 | Title: "Wind Beneath My Wings", 38 | Description: "Collect all objects in the Wings Category.", 39 | BadgeName: "193797", 40 | Points: "10", 41 | Author: "blendedsea", 42 | GameTitle: "Me & My Katamari", 43 | GameIcon: "/Images/047357.png", 44 | GameID: "3571", 45 | ConsoleName: "PlayStation Portable", 46 | CumulScore: 40, 47 | BadgeURL: "/Badge/193797.png", 48 | GameURL: "/game/3571", 49 | Type: "progression", 50 | }, 51 | ]; 52 | 53 | server.use( 54 | http.get(`${apiBaseUrl}/API_GetAchievementsEarnedBetween.php`, () => 55 | HttpResponse.json(mockResponse) 56 | ) 57 | ); 58 | 59 | // ACT 60 | const response = await getAchievementsEarnedBetween(authorization, { 61 | username: "xelnia", 62 | fromDate: new Date("2022-10-12"), 63 | toDate: new Date("2022-10-13"), 64 | }); 65 | 66 | const expectedResponse: DatedUserAchievement[] = [ 67 | { 68 | date: "2022-10-12 07:36:31", 69 | hardcoreMode: true, 70 | achievementId: 173_356, 71 | title: "Wind Beneath My Wings", 72 | description: "Collect all objects in the Wings Category.", 73 | badgeName: "193797", 74 | points: 10, 75 | author: "blendedsea", 76 | gameTitle: "Me & My Katamari", 77 | gameIcon: "/Images/047357.png", 78 | gameId: 3571, 79 | consoleName: "PlayStation Portable", 80 | cumulScore: 40, 81 | badgeUrl: "/Badge/193797.png", 82 | gameUrl: "/game/3571", 83 | type: "progression", 84 | }, 85 | ]; 86 | 87 | // ASSERT 88 | expect(response).toEqual(expectedResponse); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/user/getAchievementsEarnedBetween.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | DatedUserAchievement, 10 | DatedUserAchievementsResponse, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve a list of achievements 15 | * earned by a given user between two provided dates. 16 | * 17 | * @param authorization An object containing your username and webApiKey. 18 | * This can be constructed with `buildAuthorization()`. 19 | * 20 | * @param payload.username The user for which to retrieve the 21 | * list of achievements for. 22 | * 23 | * @param payload.fromDate A Date object specifying when 24 | * the list itself should begin. 25 | * 26 | * @param payload.toDate A Date object specifying when 27 | * the list itself should end. 28 | * 29 | * @example 30 | * ``` 31 | * const achievementsEarnedBetween = await getAchievementsEarnedBetween( 32 | * authorization, 33 | * { 34 | * username: "xelnia", 35 | * fromDate: new Date("2022-10-12"), 36 | * toDate: new Date("2022-10-13") 37 | * } 38 | * ); 39 | * ``` 40 | * 41 | * @returns An array containing metadata about the user 42 | * achievements earned during the specified date range. 43 | * ``` 44 | * [ 45 | * { 46 | * date: '2022-10-12 07:58:05', 47 | * hardcoreMode: true, 48 | * achievementId: 173315, 49 | * title: 'Your Puny Human Weapons', 50 | * description: 'Collect all objects in the Weapons Category.', 51 | * badgeName: '193756', 52 | * points: 10, 53 | * author: 'blendedsea', 54 | * gameTitle: 'Me & My Katamari', 55 | * gameIcon: '/Images/047357.png', 56 | * gameId: 3571, 57 | * consoleName: 'PlayStation Portable', 58 | * cumulScore: 120, 59 | * badgeUrl: '/Badge/193756.png', 60 | * gameUrl: '/game/3571', 61 | * type: 'progression' 62 | * } 63 | * ] 64 | * ``` 65 | */ 66 | export const getAchievementsEarnedBetween = async ( 67 | authorization: AuthObject, 68 | payload: { username: string; fromDate: Date; toDate: Date } 69 | ): Promise => { 70 | const { username, fromDate, toDate } = payload; 71 | 72 | const url = buildRequestUrl( 73 | apiBaseUrl, 74 | "/API_GetAchievementsEarnedBetween.php", 75 | authorization, 76 | { 77 | u: username, 78 | f: (fromDate.getTime() / 1000).toFixed(0), 79 | t: (toDate.getTime() / 1000).toFixed(0), 80 | } 81 | ); 82 | 83 | const rawResponse = await call({ url }); 84 | 85 | return serializeProperties(rawResponse, { 86 | shouldCastToNumbers: ["AchievementID", "Points", "GameID"], 87 | shouldMapToBooleans: ["HardcoreMode"], 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /src/user/getAchievementsEarnedOnDay.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getAchievementsEarnedOnDay } from "./getAchievementsEarnedOnDay"; 7 | import type { 8 | DatedUserAchievement, 9 | DatedUserAchievementsResponse, 10 | } from "./models"; 11 | 12 | const server = setupServer(); 13 | 14 | describe("Function: getAchievementsEarnedOnDay", () => { 15 | // MSW Setup 16 | beforeAll(() => server.listen()); 17 | afterEach(() => server.resetHandlers()); 18 | afterAll(() => server.close()); 19 | 20 | it("is defined #sanity", () => { 21 | // ASSERT 22 | expect(getAchievementsEarnedOnDay).toBeDefined(); 23 | }); 24 | 25 | it("retrieves a list of user achievements earned on a specified date", async () => { 26 | // ARRANGE 27 | const authorization = buildAuthorization({ 28 | username: "mockUserName", 29 | webApiKey: "mockWebApiKey", 30 | }); 31 | 32 | const mockResponse: DatedUserAchievementsResponse = [ 33 | { 34 | Date: "2022-10-12 07:36:31", 35 | HardcoreMode: "1", 36 | AchievementID: "173356", 37 | Title: "Wind Beneath My Wings", 38 | Description: "Collect all objects in the Wings Category.", 39 | BadgeName: "193797", 40 | Points: "10", 41 | Author: "blendedsea", 42 | GameTitle: "Me & My Katamari", 43 | GameIcon: "/Images/047357.png", 44 | GameID: "3571", 45 | ConsoleName: "PlayStation Portable", 46 | CumulScore: 40, 47 | BadgeURL: "/Badge/193797.png", 48 | GameURL: "/game/3571", 49 | Type: null, 50 | }, 51 | ]; 52 | 53 | server.use( 54 | http.get(`${apiBaseUrl}/API_GetAchievementsEarnedOnDay.php`, () => 55 | HttpResponse.json(mockResponse) 56 | ) 57 | ); 58 | 59 | // ACT 60 | const response = await getAchievementsEarnedOnDay(authorization, { 61 | username: "xelnia", 62 | onDate: new Date("2022-10-12"), 63 | }); 64 | 65 | const expectedResponse: DatedUserAchievement[] = [ 66 | { 67 | date: "2022-10-12 07:36:31", 68 | hardcoreMode: true, 69 | achievementId: 173_356, 70 | title: "Wind Beneath My Wings", 71 | description: "Collect all objects in the Wings Category.", 72 | badgeName: "193797", 73 | points: 10, 74 | author: "blendedsea", 75 | gameTitle: "Me & My Katamari", 76 | gameIcon: "/Images/047357.png", 77 | gameId: 3571, 78 | consoleName: "PlayStation Portable", 79 | cumulScore: 40, 80 | badgeUrl: "/Badge/193797.png", 81 | gameUrl: "/game/3571", 82 | type: null, 83 | }, 84 | ]; 85 | 86 | // ASSERT 87 | expect(response).toEqual(expectedResponse); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/user/getAchievementsEarnedOnDay.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | DatedUserAchievement, 10 | DatedUserAchievementsResponse, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve a list of achievements 15 | * earned by a given user on a specified date. 16 | * 17 | * @param authorization An object containing your username and webApiKey. 18 | * This can be constructed with `buildAuthorization()`. 19 | * 20 | * @param payload.username The user for which to retrieve the 21 | * list of achievements for. 22 | * 23 | * @param payload.fromDate A Date object specifying when 24 | * the list itself should begin. 25 | * 26 | * @param payload.onDate A Date object specifying the day 27 | * to query for a user's earned achievements. 28 | * 29 | * @example 30 | * ``` 31 | * const achievementsEarnedOnDay = await getAchievementsEarnedOnDay( 32 | * authorization, 33 | * { 34 | * username: "xelnia", 35 | * onDate: new Date("2022-10-13") 36 | * } 37 | * ); 38 | * ``` 39 | * 40 | * @returns An array containing metadata about the user 41 | * achievements earned on the specified day. 42 | * ``` 43 | * [ 44 | * { 45 | * date: '2022-10-12 07:58:05', 46 | * hardcoreMode: true, 47 | * achievementId: 173315, 48 | * title: 'Your Puny Human Weapons', 49 | * description: 'Collect all objects in the Weapons Category.', 50 | * badgeName: '193756', 51 | * points: 10, 52 | * author: 'blendedsea', 53 | * gameTitle: 'Me & My Katamari', 54 | * gameIcon: '/Images/047357.png', 55 | * gameId: 3571, 56 | * consoleName: 'PlayStation Portable', 57 | * cumulScore: 120, 58 | * badgeUrl: '/Badge/193756.png', 59 | * gameUrl: '/game/3571', 60 | * type: 'progression' 61 | * } 62 | * ] 63 | * ``` 64 | */ 65 | export const getAchievementsEarnedOnDay = async ( 66 | authorization: AuthObject, 67 | payload: { username: string; onDate: Date } 68 | ): Promise => { 69 | const { username, onDate } = payload; 70 | 71 | const url = buildRequestUrl( 72 | apiBaseUrl, 73 | "/API_GetAchievementsEarnedOnDay.php", 74 | authorization, 75 | { 76 | u: username, 77 | // YYYY-MM-DD 78 | d: `${onDate.getFullYear()}-${onDate.getMonth() + 1}-${onDate.getDate()}`, 79 | } 80 | ); 81 | 82 | const rawResponse = await call({ url }); 83 | 84 | return serializeProperties(rawResponse, { 85 | shouldCastToNumbers: ["AchievementID", "Points", "GameID"], 86 | shouldMapToBooleans: ["HardcoreMode"], 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /src/user/getUserAwards.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserAwards } from "./getUserAwards"; 7 | import type { GetUserAwardsResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserAwards", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserAwards).toBeDefined(); 20 | }); 21 | 22 | it("retrieves a list of a target user awards", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserAwardsResponse = { 30 | TotalAwardsCount: 10, 31 | HiddenAwardsCount: 5, 32 | MasteryAwardsCount: 5, 33 | CompletionAwardsCount: 0, 34 | BeatenHardcoreAwardsCount: 24, 35 | BeatenSoftcoreAwardsCount: 7, 36 | EventAwardsCount: 0, 37 | SiteAwardsCount: 0, 38 | VisibleUserAwards: [ 39 | { 40 | AwardedAt: "2022-08-26T19:34:43+00:00", 41 | AwardType: "Mastery/Completion", 42 | AwardData: 802, 43 | AwardDataExtra: 1, 44 | DisplayOrder: 114, 45 | Title: "WarioWare, Inc.: Mega Microgames!", 46 | ConsoleName: "Game Boy Advance", 47 | Flags: null, 48 | ImageIcon: "/Images/034678.png", 49 | }, 50 | ], 51 | }; 52 | 53 | server.use( 54 | http.get(`${apiBaseUrl}/API_GetUserAwards.php`, () => 55 | HttpResponse.json(mockResponse) 56 | ) 57 | ); 58 | 59 | // ACT 60 | const response = await getUserAwards(authorization, { username: "xelnia" }); 61 | 62 | // ASSERT 63 | expect(response).toEqual({ 64 | totalAwardsCount: 10, 65 | hiddenAwardsCount: 5, 66 | masteryAwardsCount: 5, 67 | completionAwardsCount: 0, 68 | beatenHardcoreAwardsCount: 24, 69 | beatenSoftcoreAwardsCount: 7, 70 | eventAwardsCount: 0, 71 | siteAwardsCount: 0, 72 | visibleUserAwards: [ 73 | { 74 | awardedAt: "2022-08-26T19:34:43+00:00", 75 | awardType: "Mastery/Completion", 76 | awardData: 802, 77 | awardDataExtra: 1, 78 | displayOrder: 114, 79 | title: "WarioWare, Inc.: Mega Microgames!", 80 | consoleName: "Game Boy Advance", 81 | flags: null, 82 | imageIcon: "/Images/034678.png", 83 | }, 84 | ], 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/user/getUserAwards.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { GetUserAwardsResponse, UserAwards } from "./models"; 9 | 10 | /** 11 | * A call to this function will retrieve metadata about the target user's 12 | * site awards, via their username. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @param payload.username The user for which to retrieve the site awards for. 18 | * 19 | * @example 20 | * ``` 21 | * const userAwards = await getUserAwards( 22 | * authorization, 23 | * { username: "xelnia" } 24 | * ) 25 | * ``` 26 | * 27 | * @returns 28 | * ```json 29 | * { 30 | * totalAwardsCount: 10, 31 | * hiddenAwardsCount: 2, 32 | * masteryAwardsCount: 6, 33 | * completionAwardsCount: 0, 34 | * beatenHardcoreAwardsCount: 24, 35 | * beatenSoftcoreAwardsCount: 7, 36 | * eventAwardsCount: 0, 37 | * siteAwardsCount: 2, 38 | * visibleUserAwards: [ 39 | * { 40 | * awardedAt: "2022-08-26T19:34:43+00:00", 41 | * awardType: "Mastery/Completion", 42 | * awardData: 802, 43 | * awardDataExtra: 1, 44 | * displayOrder: 114, 45 | * title: "WarioWare, Inc.: Mega Microgames!", 46 | * consoleName: "Game Boy Advance", 47 | * flags: null, 48 | * imageIcon: "/Images/034678.png" 49 | * } 50 | * ] 51 | * } 52 | * ``` 53 | */ 54 | export const getUserAwards = async ( 55 | authorization: AuthObject, 56 | payload: { username: string } 57 | ): Promise => { 58 | const { username } = payload; 59 | 60 | const queryParams: Record = { u: username }; 61 | 62 | const url = buildRequestUrl( 63 | apiBaseUrl, 64 | "/API_GetUserAwards.php", 65 | authorization, 66 | queryParams 67 | ); 68 | 69 | const rawResponse = await call({ url }); 70 | 71 | return serializeProperties(rawResponse); 72 | }; 73 | -------------------------------------------------------------------------------- /src/user/getUserClaims.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getUserClaims } from "./getUserClaims"; 9 | import type { GetUserClaimsResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getUserClaims", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getUserClaims).toBeDefined(); 22 | }); 23 | 24 | it("given a username, retrieves a list of achievement set claims for the user", async () => { 25 | const authorization = buildAuthorization({ 26 | username: "mockUserName", 27 | webApiKey: "mockWebApiKey", 28 | }); 29 | 30 | const mockResponse: GetUserClaimsResponse = [ 31 | { 32 | ID: "1685", 33 | User: "Jamiras", 34 | GameID: "1492", 35 | GameTitle: "Pinball Quest", 36 | GameIcon: "/Images/011326.png", 37 | ConsoleName: "NES", 38 | ClaimType: "0", 39 | SetType: "0", 40 | Status: "1", 41 | Extension: "0", 42 | Special: "0", 43 | Created: "2017-08-20 00:00:00", 44 | DoneTime: "2017-08-20 00:00:00", 45 | Updated: "2022-06-28 17:15:59", 46 | MinutesLeft: "-2862348", 47 | }, 48 | ]; 49 | 50 | server.use( 51 | http.get(`${apiBaseUrl}/API_GetUserClaims.php`, () => 52 | HttpResponse.json(mockResponse) 53 | ) 54 | ); 55 | 56 | // ACT 57 | const response = await getUserClaims(authorization, { 58 | username: "Jamiras", 59 | }); 60 | 61 | // ASSERT 62 | expect(response).toEqual([ 63 | { 64 | id: 1685, 65 | user: "Jamiras", 66 | gameId: 1492, 67 | gameTitle: "Pinball Quest", 68 | gameIcon: "/Images/011326.png", 69 | consoleName: "NES", 70 | claimType: 0, 71 | setType: 0, 72 | status: 1, 73 | extension: 0, 74 | special: 0, 75 | created: "2017-08-20 00:00:00", 76 | doneTime: "2017-08-20 00:00:00", 77 | updated: "2022-06-28 17:15:59", 78 | minutesLeft: -2_862_348, 79 | }, 80 | ]); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/user/getUserClaims.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { GetUserClaimsResponse, UserClaims } from "./models"; 9 | 10 | /** 11 | * A call to this function will retrieve a list of 12 | * achievement set claims made over the lifetime of a given 13 | * user, targeted by their username. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.username The user for which to retrieve the historical 19 | * achievement set claims list for. 20 | * 21 | * @example 22 | * ``` 23 | * const userClaims = await getUserClaims( 24 | * authorization, 25 | * { username: "Jamiras" } 26 | * ); 27 | * ``` 28 | * 29 | * @returns An array containing all the achievement set claims 30 | * made over the lifetime of the given user. 31 | */ 32 | export const getUserClaims = async ( 33 | authorization: AuthObject, 34 | payload: { username: string } 35 | ): Promise => { 36 | const { username } = payload; 37 | 38 | const url = buildRequestUrl( 39 | apiBaseUrl, 40 | "/API_GetUserClaims.php", 41 | authorization, 42 | { u: username } 43 | ); 44 | 45 | const rawResponse = await call({ url }); 46 | 47 | return serializeProperties(rawResponse, { 48 | shouldCastToNumbers: [ 49 | "ID", 50 | "GameID", 51 | "ClaimType", 52 | "SetType", 53 | "Status", 54 | "Extension", 55 | "Special", 56 | "MinutesLeft", 57 | ], 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /src/user/getUserCompletedGames.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getUserCompletedGames } from "./getUserCompletedGames"; 9 | import type { GetUserCompletedGamesResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getUserCompletedGames", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getUserCompletedGames).toBeDefined(); 22 | }); 23 | 24 | it("given a username, returns completion metadata", async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetUserCompletedGamesResponse = [ 32 | { 33 | GameID: "1881", 34 | Title: "Popeye", 35 | ImageIcon: "/Images/065073.png", 36 | ConsoleID: "7", 37 | ConsoleName: "NES", 38 | MaxPossible: "26", 39 | NumAwarded: "12", 40 | PctWon: "0.4615", 41 | HardcoreMode: "0", 42 | }, 43 | { 44 | GameID: "1881", 45 | Title: "Popeye", 46 | ImageIcon: "/Images/065073.png", 47 | ConsoleID: "7", 48 | ConsoleName: "NES", 49 | MaxPossible: "26", 50 | NumAwarded: "12", 51 | PctWon: "0.4615", 52 | HardcoreMode: "1", 53 | }, 54 | ]; 55 | 56 | server.use( 57 | http.get(`${apiBaseUrl}/API_GetUserCompletedGames.php`, () => 58 | HttpResponse.json(mockResponse) 59 | ) 60 | ); 61 | 62 | // ACT 63 | const response = await getUserCompletedGames(authorization, { 64 | username: "xelnia", 65 | }); 66 | 67 | // ASSERT 68 | expect(response).toEqual([ 69 | { 70 | gameId: 1881, 71 | title: "Popeye", 72 | imageIcon: "/Images/065073.png", 73 | consoleId: 7, 74 | consoleName: "NES", 75 | maxPossible: 26, 76 | numAwarded: 12, 77 | pctWon: 0.4615, 78 | hardcoreMode: false, 79 | }, 80 | { 81 | gameId: 1881, 82 | title: "Popeye", 83 | imageIcon: "/Images/065073.png", 84 | consoleId: 7, 85 | consoleName: "NES", 86 | maxPossible: 26, 87 | numAwarded: 12, 88 | pctWon: 0.4615, 89 | hardcoreMode: true, 90 | }, 91 | ]); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/user/getUserCompletedGames.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | GetUserCompletedGamesResponse, 10 | UserCompletedGames, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve completion metadata 15 | * about the games a given user has played. It returns two 16 | * entries per each game: one for the softcore completion and 17 | * one for the hardcore completion. These are designated by 18 | * the `hardcoreMode` property on each completion object. 19 | * 20 | * @param authorization An object containing your username and webApiKey. 21 | * This can be constructed with `buildAuthorization()`. 22 | * 23 | * @param payload.username The user for which to retrieve the 24 | * completion metadata for. 25 | * 26 | * @example 27 | * ``` 28 | * const userCompletedGames = await getUserCompletedGames( 29 | * authorization, 30 | * { username: "xelnia" } 31 | * ); 32 | * ``` 33 | * 34 | * @returns An array containing completion metadata objects 35 | * for a given user. Each game contains two completion records, 36 | * one for softcore and another for hardcore. 37 | * ```json 38 | * [ 39 | * { 40 | * gameId: 14976, 41 | * title: 'Mortal Kombat', 42 | * imageIcon: '/Images/036812.png', 43 | * consoleId: 27, 44 | * consoleName: 'Arcade', 45 | * maxPossible: 35, 46 | * numAwarded: 13, 47 | * pctWon: 0.3714, 48 | * hardcoreMode: false 49 | * }, 50 | * { 51 | * gameId: 14976, 52 | * title: 'Mortal Kombat', 53 | * imageIcon: '/Images/036812.png', 54 | * consoleId: 27, 55 | * consoleName: 'Arcade', 56 | * maxPossible: 35, 57 | * numAwarded: 13, 58 | * pctWon: 0.3714, 59 | * hardcoreMode: true 60 | * }, 61 | * ] 62 | * ``` 63 | */ 64 | export const getUserCompletedGames = async ( 65 | authorization: AuthObject, 66 | payload: { username: string } 67 | ): Promise => { 68 | const { username } = payload; 69 | 70 | const url = buildRequestUrl( 71 | apiBaseUrl, 72 | "/API_GetUserCompletedGames.php", 73 | authorization, 74 | { u: username } 75 | ); 76 | 77 | const rawResponse = await call({ url }); 78 | 79 | return serializeProperties(rawResponse, { 80 | shouldCastToNumbers: [ 81 | "GameID", 82 | "ConsoleID", 83 | "MaxPossible", 84 | "NumAwarded", 85 | "PctWon", 86 | ], 87 | shouldMapToBooleans: ["HardcoreMode"], 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /src/user/getUserCompletionProgress.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserCompletionProgress } from "./getUserCompletionProgress"; 7 | import type { GetUserCompletionProgressResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserCompletionProgress", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserCompletionProgress).toBeDefined(); 20 | }); 21 | 22 | it("retrieves completion progress by username", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserCompletionProgressResponse = { 30 | Count: 1, 31 | Total: 1, 32 | Results: [ 33 | { 34 | GameID: 680, 35 | Title: "Game & Watch Gallery", 36 | ImageIcon: "/Images/042952.png", 37 | ConsoleID: 4, 38 | ConsoleName: "Game Boy", 39 | MaxPossible: 27, 40 | NumAwarded: 8, 41 | NumAwardedHardcore: 8, 42 | MostRecentAwardedDate: "2022-07-26T23:56:15+00:00", 43 | HighestAwardKind: null, 44 | HighestAwardDate: null, 45 | }, 46 | ], 47 | }; 48 | 49 | server.use( 50 | http.get(`${apiBaseUrl}/API_GetUserCompletionProgress.php`, () => 51 | HttpResponse.json(mockResponse) 52 | ) 53 | ); 54 | 55 | // ACT 56 | const response = await getUserCompletionProgress(authorization, { 57 | username: "xelnia", 58 | }); 59 | 60 | // ASSERT 61 | expect(response).toEqual({ 62 | count: 1, 63 | total: 1, 64 | results: [ 65 | { 66 | gameId: 680, 67 | title: "Game & Watch Gallery", 68 | imageIcon: "/Images/042952.png", 69 | consoleId: 4, 70 | consoleName: "Game Boy", 71 | maxPossible: 27, 72 | numAwarded: 8, 73 | numAwardedHardcore: 8, 74 | mostRecentAwardedDate: "2022-07-26T23:56:15+00:00", 75 | highestAwardKind: null, 76 | highestAwardDate: null, 77 | }, 78 | ], 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/user/getUserCompletionProgress.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | GetUserCompletionProgressResponse, 10 | UserCompletionProgress, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve a given user's completion 15 | * progress, targeted by their username. 16 | * 17 | * @param authorization An object containing your username and webApiKey. 18 | * This can be constructed with `buildAuthorization()`. 19 | * 20 | * @param payload.username The user for which to retrieve the progress for. 21 | * 22 | * @param payload.offset Defaults to 0. The number of entries to skip. 23 | * 24 | * @param payload.count Defaults to 100, has a max of 500. 25 | * 26 | * @example 27 | * ``` 28 | * const userCompletionProgress = await getUserCompletionProgress( 29 | * authorization, 30 | * { username: "xelnia" } 31 | * ); 32 | * ``` 33 | * 34 | * @returns 35 | * ``` 36 | * { 37 | * "count": 100, 38 | * "total": 752, 39 | * "results": [ 40 | * { 41 | gameId: 11406, 42 | title: 'Mortal Kombat 4', 43 | imageIcon: '/Images/042133.png', 44 | consoleId: 12, 45 | consoleName: 'PlayStation', 46 | maxPossible: 131, 47 | numAwarded: 131, 48 | numAwardedHardcore: 131, 49 | mostRecentAwardedDate: '2022-08-07T18:24:44+00:00', 50 | highestAwardKind: 'mastered', 51 | highestAwardDate: '2022-08-07T18:24:44+00:00' 52 | * } 53 | * ] 54 | * } 55 | * ``` 56 | */ 57 | export const getUserCompletionProgress = async ( 58 | authorization: AuthObject, 59 | payload: { username: string; offset?: number; count?: number } 60 | ): Promise => { 61 | const { username, offset, count } = payload; 62 | 63 | const params: Record = { 64 | u: username, 65 | }; 66 | if (offset) { 67 | params["o"] = offset; 68 | } 69 | if (count) { 70 | params["c"] = count; 71 | } 72 | 73 | const url = buildRequestUrl( 74 | apiBaseUrl, 75 | "/API_GetUserCompletionProgress.php", 76 | authorization, 77 | params 78 | ); 79 | 80 | const rawResponse = await call({ url }); 81 | 82 | return serializeProperties(rawResponse); 83 | }; 84 | -------------------------------------------------------------------------------- /src/user/getUserGameRankAndScore.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserGameRankAndScore } from "./getUserGameRankAndScore"; 7 | import type { GetUserGameRankAndScoreResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserGameRankAndScore", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserGameRankAndScore).toBeDefined(); 20 | }); 21 | 22 | it("given a game ID and a user name, retrieves metadata about how that user ranks on the given game", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserGameRankAndScoreResponse = [ 30 | { 31 | User: "xelnia", 32 | TotalScore: "1000", 33 | LastAward: "2022-09-01 21:51:23", 34 | UserRank: "4", 35 | }, 36 | ]; 37 | 38 | server.use( 39 | http.get(`${apiBaseUrl}/API_GetUserGameRankAndScore.php`, () => 40 | HttpResponse.json(mockResponse) 41 | ) 42 | ); 43 | 44 | // ACT 45 | const response = await getUserGameRankAndScore(authorization, { 46 | username: "xelnia", 47 | gameId: 14_402, 48 | }); 49 | 50 | // ASSERT 51 | expect(response).toEqual([ 52 | { 53 | user: "xelnia", 54 | totalScore: 1000, 55 | lastAward: "2022-09-01 21:51:23", 56 | userRank: 4, 57 | }, 58 | ]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/user/getUserGameRankAndScore.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { 10 | GetUserGameRankAndScoreResponse, 11 | UserGameRankAndScore, 12 | } from "./models"; 13 | 14 | /** 15 | * A call to this function will retrieve metadata about 16 | * how a particular user has performed/ranked on a particular 17 | * game, targeted by game ID. 18 | * 19 | * @param authorization An object containing your username and webApiKey. 20 | * This can be constructed with `buildAuthorization()`. 21 | * 22 | * @param payload.gameId The unique game ID. If you are unsure, open the 23 | * game's page on the RetroAchievements.org website. For example, Dragster's 24 | * URL is https://retroachievements.org/game/14402. We can see from the 25 | * URL that the game ID is "14402". 26 | * 27 | * @param payload.username The user for which to retrieve the 28 | * game ranking metadata for. 29 | * 30 | * @example 31 | * ``` 32 | * const userGameRankAndScore = await getUserGameRankAndScore( 33 | * authorization, 34 | * { gameId: 14402, username: "xelnia" } 35 | * ); 36 | * ``` 37 | * 38 | * @returns An array containing metadata about the user's 39 | * rank and score for the target game ID. If metadata 40 | * cannot be found, the array is empty. 41 | * ```json 42 | * [ 43 | * { 44 | * user: "xelnia", 45 | * totalScore: 378, 46 | * lastAward: "2022-09-01 21:51:23", 47 | * userRank: 3 48 | * } 49 | * ] 50 | * ``` 51 | */ 52 | export const getUserGameRankAndScore = async ( 53 | authorization: AuthObject, 54 | payload: { gameId: ID; username: string } 55 | ): Promise => { 56 | const { gameId, username } = payload; 57 | 58 | const url = buildRequestUrl( 59 | apiBaseUrl, 60 | "/API_GetUserGameRankAndScore.php", 61 | authorization, 62 | { g: gameId, u: username } 63 | ); 64 | 65 | const rawResponse = await call({ url }); 66 | 67 | return serializeProperties(rawResponse, { 68 | shouldCastToNumbers: ["TotalScore", "UserRank"], 69 | }); 70 | }; 71 | -------------------------------------------------------------------------------- /src/user/getUserPoints.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserPoints } from "./getUserPoints"; 7 | import type { GetUserPointsResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserPoints", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserPoints).toBeDefined(); 20 | }); 21 | 22 | it("given a username, retrieves the point values associated with the user", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserPointsResponse = { 30 | Points: 10_000, 31 | SoftcorePoints: 5400, 32 | }; 33 | 34 | server.use( 35 | http.get(`${apiBaseUrl}/API_GetUserPoints.php`, () => 36 | HttpResponse.json(mockResponse) 37 | ) 38 | ); 39 | 40 | // ACT 41 | const response = await getUserPoints(authorization, { username: "xelnia" }); 42 | 43 | // ASSERT 44 | expect(response).toEqual({ 45 | points: 10_000, 46 | softcorePoints: 5400, 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/user/getUserPoints.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { GetUserPointsResponse, UserPoints } from "./models"; 9 | 10 | /** 11 | * A call to this function will retrieve a given user's hardcore 12 | * and softcore points. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @param payload.username The user for which to retrieve the point totals for. 18 | * 19 | * @example 20 | * ``` 21 | * const userPoints = await getUserPoints( 22 | * authorization, 23 | * { username: "xelnia" } 24 | * ); 25 | * ``` 26 | * 27 | * @returns An object containing metadata about a target user's points. 28 | * ```json 29 | * { 30 | * points: 7640, 31 | * softcorePoints: 25 32 | * } 33 | * ``` 34 | */ 35 | export const getUserPoints = async ( 36 | authorization: AuthObject, 37 | payload: { username: string } 38 | ): Promise => { 39 | const { username } = payload; 40 | 41 | const url = buildRequestUrl( 42 | apiBaseUrl, 43 | "/API_GetUserPoints.php", 44 | authorization, 45 | { u: username } 46 | ); 47 | 48 | const rawResponse = await call({ url }); 49 | 50 | return serializeProperties(rawResponse); 51 | }; 52 | -------------------------------------------------------------------------------- /src/user/getUserProfile.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserProfile } from "./getUserProfile"; 7 | import type { GetUserProfileResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserProfile", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserProfile).toBeDefined(); 20 | }); 21 | 22 | it("given a username, retrieves minimal user profile information about the user", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserProfileResponse = { 30 | User: "MaxMilyin", 31 | UserPic: "/UserPic/MaxMilyin.png", 32 | MemberSince: "2016-01-02 00:43:04", 33 | RichPresenceMsg: 34 | "Playing ~Hack~ 11th Annual Vanilla Level Design Contest, The", 35 | LastGameID: 19_504, 36 | ContribCount: 0, 37 | ContribYield: 0, 38 | TotalPoints: 399_597, 39 | TotalSoftcorePoints: 0, 40 | TotalTruePoints: 1_599_212, 41 | Permissions: 1, 42 | Untracked: 0, 43 | ID: 16_446, 44 | UserWallActive: 1, 45 | Motto: "Join me on Twitch! GameSquadSquad for live RA", 46 | }; 47 | 48 | server.use( 49 | http.get(`${apiBaseUrl}/API_GetUserProfile.php`, () => 50 | HttpResponse.json(mockResponse) 51 | ) 52 | ); 53 | 54 | // ACT 55 | const response = await getUserProfile(authorization, { 56 | username: "WCopeland", 57 | }); 58 | 59 | // ASSERT 60 | expect(response).toEqual({ 61 | user: "MaxMilyin", 62 | userPic: "/UserPic/MaxMilyin.png", 63 | memberSince: "2016-01-02 00:43:04", 64 | richPresenceMsg: 65 | "Playing ~Hack~ 11th Annual Vanilla Level Design Contest, The", 66 | lastGameId: 19_504, 67 | contribCount: 0, 68 | contribYield: 0, 69 | totalPoints: 399_597, 70 | totalSoftcorePoints: 0, 71 | totalTruePoints: 1_599_212, 72 | permissions: 1, 73 | untracked: false, 74 | id: 16_446, 75 | userWallActive: true, 76 | motto: "Join me on Twitch! GameSquadSquad for live RA", 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/user/getUserProfile.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { GetUserProfileResponse, UserProfile } from "./models"; 9 | 10 | /** 11 | * A call to this function will retrieve summary information about 12 | * a given user, targeted by username. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @param payload.username The user for which to retrieve the summary for. 18 | * 19 | * @example 20 | * ``` 21 | * const userSummary = await getUserProfile( 22 | * authorization, 23 | * { username: "xelnia" } 24 | * ); 25 | * ``` 26 | * 27 | * @returns An object containing profile summary metadata about a target user. 28 | */ 29 | export const getUserProfile = async ( 30 | authorization: AuthObject, 31 | payload: { 32 | username: string; 33 | } 34 | ): Promise => { 35 | const { username } = payload; 36 | 37 | const url = buildRequestUrl( 38 | apiBaseUrl, 39 | "/API_GetUserProfile.php", 40 | authorization, 41 | { u: username } 42 | ); 43 | 44 | const rawResponse = await call({ url }); 45 | 46 | return serializeProperties(rawResponse, { 47 | shouldCastToNumbers: [ 48 | "TotalPoints", 49 | "TotalSoftcorePoints", 50 | "TotalTruePoints", 51 | "Permissions", 52 | ], 53 | shouldMapToBooleans: ["Untracked", "UserWallActive"], 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /src/user/getUserProgress.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserProgress } from "./getUserProgress"; 7 | import type { GetUserProgressResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserProgress", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserProgress).toBeDefined(); 20 | }); 21 | 22 | it(`retrieves a map of a user's progress by game IDs`, async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserProgressResponse = { 30 | "1": { 31 | NumPossibleAchievements: "10", 32 | PossibleScore: "200", 33 | NumAchieved: "4", 34 | ScoreAchieved: "80", 35 | NumAchievedHardcore: "4", 36 | ScoreAchievedHardcore: "80", 37 | }, 38 | "14402": { 39 | NumPossibleAchievements: "10", 40 | PossibleScore: "200", 41 | NumAchieved: "4", 42 | ScoreAchieved: "80", 43 | NumAchievedHardcore: "4", 44 | ScoreAchievedHardcore: "80", 45 | }, 46 | }; 47 | 48 | server.use( 49 | http.get(`${apiBaseUrl}/API_GetUserProgress.php`, () => 50 | HttpResponse.json(mockResponse) 51 | ) 52 | ); 53 | 54 | // ACT 55 | const response = await getUserProgress(authorization, { 56 | username: "xelnia", 57 | gameIds: [1, 14_402], 58 | }); 59 | 60 | // ASSERT 61 | expect(response).toEqual({ 62 | "1": { 63 | numPossibleAchievements: 10, 64 | possibleScore: 200, 65 | numAchieved: 4, 66 | scoreAchieved: 80, 67 | numAchievedHardcore: 4, 68 | scoreAchievedHardcore: 80, 69 | }, 70 | "14402": { 71 | numPossibleAchievements: 10, 72 | possibleScore: 200, 73 | numAchieved: 4, 74 | scoreAchieved: 80, 75 | numAchievedHardcore: 4, 76 | scoreAchievedHardcore: 80, 77 | }, 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/user/getUserProgress.ts: -------------------------------------------------------------------------------- 1 | import type { ID } from "../utils/internal"; 2 | import { 3 | apiBaseUrl, 4 | buildRequestUrl, 5 | call, 6 | serializeProperties, 7 | } from "../utils/internal"; 8 | import type { AuthObject } from "../utils/public"; 9 | import type { GetUserProgressResponse, UserProgress } from "./models"; 10 | 11 | /** 12 | * A call to this function will retrieve a given user's 13 | * progress on a given list of games, targeted by game ID. 14 | * 15 | * @param authorization An object containing your username and webApiKey. 16 | * This can be constructed with `buildAuthorization()`. 17 | * 18 | * @param payload.username The user for which to retrieve the progress for. 19 | * 20 | * @param payload.gameIds An array of RetroAchievements game IDs. If you aren't 21 | * sure of the game ID, visit the game's page on the website and copy the number 22 | * at the end of the URL. 23 | * 24 | * @example 25 | * ``` 26 | * const userProgress = await getUserProgress( 27 | * authorization, 28 | * { username: "xelnia", gameIds: [1, 14402] } 29 | * ); 30 | * ``` 31 | * 32 | * @returns An object which is a map of summarized progress for games. 33 | * ```json 34 | * { 35 | * "1": { 36 | * numPossibleAchievements: 24, 37 | * possibleScore: 255, 38 | * numAchieved: 0, 39 | * scoreAchieved: 0, 40 | * numAchievedHardcore: 0, 41 | * scoreAchievedHardcore: 0 42 | * }, 43 | * "14402": { 44 | * numPossibleAchievements: 24, 45 | * possibleScore: 255, 46 | * numAchieved: 0, 47 | * scoreAchieved: 0, 48 | * numAchievedHardcore: 0, 49 | * scoreAchievedHardcore: 0 50 | * } 51 | * } 52 | * ``` 53 | */ 54 | export const getUserProgress = async ( 55 | authorization: AuthObject, 56 | payload: { username: string; gameIds: ID[] } 57 | ): Promise => { 58 | const { username, gameIds } = payload; 59 | 60 | const url = buildRequestUrl( 61 | apiBaseUrl, 62 | "/API_GetUserProgress.php", 63 | authorization, 64 | { u: username, i: gameIds.join(",") } 65 | ); 66 | 67 | const rawResponse = await call({ url }); 68 | 69 | return serializeProperties(rawResponse, { 70 | shouldCastToNumbers: [ 71 | "NumPossibleAchievements", 72 | "PossibleScore", 73 | "NumAchieved", 74 | "ScoreAchieved", 75 | "NumAchievedHardcore", 76 | "ScoreAchievedHardcore", 77 | ], 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /src/user/getUserRecentAchievements.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserRecentAchievements } from "./getUserRecentAchievements"; 7 | import type { GetUserRecentAchievementsResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserRecentAchievements", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserRecentAchievements).toBeDefined(); 20 | }); 21 | 22 | it("retrieves a list of recently-earned user achievements", async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserRecentAchievementsResponse = [ 30 | { 31 | Date: "2023-05-23 22:32:24", 32 | HardcoreMode: 1, 33 | AchievementID: 51_214, 34 | Title: "You're a special Champ!", 35 | Description: 36 | "Win the Tournament as [You] on Hard with 1 attribute on max. and 1 attribute on min.", 37 | BadgeName: "121991", 38 | Points: 25, 39 | Author: "Som1", 40 | GameTitle: "WWF King of the Ring", 41 | GameIcon: "/Images/062599.png", 42 | GameID: 6316, 43 | ConsoleName: "Game Boy", 44 | BadgeURL: "/Badge/121991.png", 45 | GameURL: "/game/6316", 46 | }, 47 | ]; 48 | 49 | server.use( 50 | http.get(`${apiBaseUrl}/API_GetUserRecentAchievements.php`, () => 51 | HttpResponse.json(mockResponse) 52 | ) 53 | ); 54 | 55 | // ACT 56 | const response = await getUserRecentAchievements(authorization, { 57 | username: "xelnia", 58 | }); 59 | 60 | // ASSERT 61 | expect(response).toEqual([ 62 | { 63 | date: "2023-05-23 22:32:24", 64 | hardcoreMode: true, 65 | achievementId: 51_214, 66 | title: "You're a special Champ!", 67 | description: 68 | "Win the Tournament as [You] on Hard with 1 attribute on max. and 1 attribute on min.", 69 | badgeName: "121991", 70 | points: 25, 71 | author: "Som1", 72 | gameTitle: "WWF King of the Ring", 73 | gameIcon: "/Images/062599.png", 74 | gameId: 6316, 75 | consoleName: "Game Boy", 76 | badgeUrl: "/Badge/121991.png", 77 | gameUrl: "/game/6316", 78 | }, 79 | ]); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/user/getUserRecentAchievements.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | GetUserRecentAchievementsResponse, 10 | UserRecentAchievement, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve a list of a target user's 15 | * recently earned achievements, via their username. By default, it 16 | * fetches achievements earned in the last hour. 17 | * 18 | * @param authorization An object containing your username and webApiKey. 19 | * This can be constructed with `buildAuthorization()`. 20 | * 21 | * @param payload.username The user for which to retrieve the recent achievements for. 22 | * 23 | * @param payload.recentMinutes Optional. Defaults to 60. How many minutes 24 | * back to fetch for the given user. 25 | * 26 | * @example 27 | * ``` 28 | * const userRecentAchievements = await getUserRecentAchievements( 29 | * authorization, 30 | * { username: "xelnia" } 31 | * ); 32 | * ``` 33 | * 34 | * @returns An array containing metadata about a user's recently earned achievements. 35 | * ```json 36 | * [ 37 | * { 38 | * date: '2023-05-23 22:32:24', 39 | * hardcoreMode: true, 40 | * achievementId: 51214, 41 | * title: "You're a special Champ!", 42 | * description: 'Win the Tournament as [You] on Hard with 1 attribute on max. and 1 attribute on min.', 43 | * badgeName: '121991', 44 | * points: 25, 45 | * author: 'Som1', 46 | * gameTitle: 'WWF King of the Ring', 47 | * gameIcon: '/Images/062599.png', 48 | * gameId: 6316, 49 | * consoleName: 'Game Boy', 50 | * badgeUrl: '/Badge/121991.png', 51 | * gameUrl: '/game/6316' 52 | * } 53 | * ] 54 | * ``` 55 | */ 56 | export const getUserRecentAchievements = async ( 57 | authorization: AuthObject, 58 | payload: { username: string; recentMinutes?: number } 59 | ): Promise => { 60 | const { username, recentMinutes } = payload; 61 | 62 | const queryParams: Record = { u: username }; 63 | 64 | if (recentMinutes !== undefined) { 65 | queryParams["m"] = recentMinutes; 66 | } 67 | 68 | const url = buildRequestUrl( 69 | apiBaseUrl, 70 | "/API_GetUserRecentAchievements.php", 71 | authorization, 72 | queryParams 73 | ); 74 | 75 | const rawResponse = await call({ url }); 76 | 77 | return serializeProperties(rawResponse, { 78 | shouldMapToBooleans: ["HardcoreMode"], 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /src/user/getUserRecentlyPlayedGames.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { apiBaseUrl } from "../utils/internal"; 5 | import { buildAuthorization } from "../utils/public"; 6 | import { getUserRecentlyPlayedGames } from "./getUserRecentlyPlayedGames"; 7 | import type { GetUserRecentlyPlayedGamesResponse } from "./models"; 8 | 9 | const server = setupServer(); 10 | 11 | describe("Function: getUserRecentlyPlayedGames", () => { 12 | // MSW Setup 13 | beforeAll(() => server.listen()); 14 | afterEach(() => server.resetHandlers()); 15 | afterAll(() => server.close()); 16 | 17 | it("is defined #sanity", () => { 18 | // ASSERT 19 | expect(getUserRecentlyPlayedGames).toBeDefined(); 20 | }); 21 | 22 | it(`retrieves a list of a given user's recently played games`, async () => { 23 | // ARRANGE 24 | const authorization = buildAuthorization({ 25 | username: "mockUserName", 26 | webApiKey: "mockWebApiKey", 27 | }); 28 | 29 | const mockResponse: GetUserRecentlyPlayedGamesResponse = [ 30 | { 31 | GameID: "6278", 32 | ConsoleID: "12", 33 | ConsoleName: "PlayStation", 34 | Title: "Duke Nukem: Land of the Babes", 35 | ImageIcon: "/Images/054546.png", 36 | LastPlayed: "2022-11-06 16:08:21", 37 | NumPossibleAchievements: "42", 38 | PossibleScore: "478", 39 | NumAchieved: 0, 40 | ScoreAchieved: 0, 41 | NumAchievedHardcore: 0, 42 | ScoreAchievedHardcore: 0, 43 | MyVote: "2", 44 | }, 45 | ]; 46 | 47 | server.use( 48 | http.get(`${apiBaseUrl}/API_GetUserRecentlyPlayedGames.php`, () => 49 | HttpResponse.json(mockResponse) 50 | ) 51 | ); 52 | 53 | // ACT 54 | const response = await getUserRecentlyPlayedGames(authorization, { 55 | username: "xelnia", 56 | }); 57 | 58 | // ASSERT 59 | expect(response).toEqual([ 60 | { 61 | gameId: 6278, 62 | consoleId: 12, 63 | consoleName: "PlayStation", 64 | title: "Duke Nukem: Land of the Babes", 65 | imageIcon: "/Images/054546.png", 66 | lastPlayed: "2022-11-06 16:08:21", 67 | numPossibleAchievements: 42, 68 | possibleScore: 478, 69 | numAchieved: 0, 70 | scoreAchieved: 0, 71 | numAchievedHardcore: 0, 72 | scoreAchievedHardcore: 0, 73 | myVote: 2, 74 | }, 75 | ]); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/user/getUserRecentlyPlayedGames.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | GetUserRecentlyPlayedGamesResponse, 10 | UserRecentlyPlayedGames, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve a list of a target user's 15 | * recently played games, via their username. 16 | * 17 | * @param authorization An object containing your username and webApiKey. 18 | * This can be constructed with `buildAuthorization()`. 19 | * 20 | * @param payload.username The user for which to retrieve the summary for. 21 | * 22 | * @param payload.count Optional. Defaults to 10. Max is 50. How many 23 | * recently played games for the user to retrieve. 24 | * 25 | * @param payload.offset Optional. Defaults to 0. Number of recently played 26 | * game entries to skip. This can be used for pagination. 27 | * 28 | * @example 29 | * ``` 30 | * const userRecentlyPlayedGames = await getUserRecentlyPlayedGames( 31 | * authorization, 32 | * { username: "xelnia" } 33 | * ); 34 | * ``` 35 | * 36 | * @returns An array containing metadata about a user's recently played games. 37 | * ```json 38 | * [ 39 | * { 40 | * gameId: 19010, 41 | * consoleId: 21, 42 | * consoleName: "PlayStation 2", 43 | * title: "Simpsons, The: Hit & Run", 44 | * imageIcon: "/Images/066024.png", 45 | * lastPlayed: "2022-10-24 22:05:12", 46 | * numPossibleAchievements: 131, 47 | * possibleScore: 865, 48 | * numAchieved: 23, 49 | * scoreAchieved: 84, 50 | * numAchievedHardcore: 23, 51 | * scoreAchievedHardcore: 84 52 | * } 53 | * ] 54 | * ``` 55 | */ 56 | export const getUserRecentlyPlayedGames = async ( 57 | authorization: AuthObject, 58 | payload: { username: string; offset?: number; count?: number } 59 | ): Promise => { 60 | const { username, offset, count } = payload; 61 | 62 | const queryParams: Record = { u: username }; 63 | 64 | if (offset !== undefined) { 65 | queryParams["o"] = offset; 66 | } 67 | 68 | if (count !== undefined) { 69 | queryParams["c"] = count; 70 | } 71 | 72 | const url = buildRequestUrl( 73 | apiBaseUrl, 74 | "/API_GetUserRecentlyPlayedGames.php", 75 | authorization, 76 | queryParams 77 | ); 78 | 79 | const rawResponse = await call({ url }); 80 | 81 | return serializeProperties(rawResponse, { 82 | shouldCastToNumbers: [ 83 | "GameID", 84 | "ConsoleID", 85 | "NumPossibleAchievements", 86 | "PossibleScore", 87 | "NumAchieved", 88 | "ScoreAchieved", 89 | "NumAchievedHardcore", 90 | "ScoreAchievedHardcore", 91 | "MyVote", 92 | ], 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /src/user/getUserSummary.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { GetUserSummaryResponse, UserSummary } from "./models"; 9 | 10 | /** 11 | * A call to this function will retrieve summary information about 12 | * a given user, targeted by username. 13 | * 14 | * @param authorization An object containing your username and webApiKey. 15 | * This can be constructed with `buildAuthorization()`. 16 | * 17 | * @param payload.username The user for which to retrieve the summary for. 18 | * 19 | * @param payload.recentGamesCount Optional. The number of recent games to return. 20 | * This defaults to 0. 21 | * 22 | * @param payload.recentAchievementsCount Optional. The number of recent achievements 23 | * to return. This defaults to 5. 24 | * 25 | * @example 26 | * ``` 27 | * const userSummary = await getUserSummary( 28 | * authorization, 29 | * { username: "xelnia" } 30 | * ); 31 | * ``` 32 | * 33 | * @returns An object containing summary metadata about a target user. 34 | */ 35 | export const getUserSummary = async ( 36 | authorization: AuthObject, 37 | payload: { 38 | username: string; 39 | recentGamesCount?: number; 40 | recentAchievementsCount?: number; 41 | } 42 | ): Promise => { 43 | const { username, recentGamesCount, recentAchievementsCount } = payload; 44 | 45 | const queryParams: Record = { u: username }; 46 | 47 | if (recentGamesCount !== undefined) { 48 | queryParams["g"] = recentGamesCount; 49 | } 50 | 51 | if (recentAchievementsCount !== undefined) { 52 | queryParams["a"] = recentAchievementsCount; 53 | } 54 | 55 | const url = buildRequestUrl( 56 | apiBaseUrl, 57 | "/API_GetUserSummary.php", 58 | authorization, 59 | queryParams 60 | ); 61 | 62 | const rawResponse = await call({ url }); 63 | 64 | return serializeProperties(rawResponse, { 65 | shouldCastToNumbers: [ 66 | "GameID", 67 | "ConsoleID", 68 | "ID", 69 | "LastGameID", 70 | "ForumTopicID", 71 | "activitytype", 72 | "ContribCount", 73 | "ContribYield", 74 | "TotalPoints", 75 | "TotalSoftcorePoints", 76 | "TotalTruePoints", 77 | "Permissions", 78 | "NumPossibleAchievements", 79 | "PossibleScore", 80 | "NumAchieved", 81 | "ScoreAchieved", 82 | "NumAchievedHardcore", 83 | "ScoreAchievedHardcore", 84 | "Points", 85 | "SoftcorePoints", 86 | ], 87 | shouldMapToBooleans: [ 88 | "Untracked", 89 | "UserWallActive", 90 | "IsAwarded", 91 | "HardcoreAchieved", 92 | ], 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /src/user/getUserWantToPlayList.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { http, HttpResponse } from "msw"; 4 | import { setupServer } from "msw/node"; 5 | 6 | import { apiBaseUrl } from "../utils/internal"; 7 | import { buildAuthorization } from "../utils/public"; 8 | import { getUserWantToPlayList } from "./getUserWantToPlayList"; 9 | import type { GetUserWantToPlayListResponse } from "./models"; 10 | 11 | const server = setupServer(); 12 | 13 | describe("Function: getUserWantToPlayList", () => { 14 | // MSW Setup 15 | beforeAll(() => server.listen()); 16 | afterEach(() => server.resetHandlers()); 17 | afterAll(() => server.close()); 18 | 19 | it("is defined #sanity", () => { 20 | // ASSERT 21 | expect(getUserWantToPlayList).toBeDefined(); 22 | }); 23 | 24 | it('given a username, retrieves that users "Want To Play Games"', async () => { 25 | // ARRANGE 26 | const authorization = buildAuthorization({ 27 | username: "mockUserName", 28 | webApiKey: "mockWebApiKey", 29 | }); 30 | 31 | const mockResponse: GetUserWantToPlayListResponse = { 32 | Count: 100, 33 | Total: 1287, 34 | Results: [ 35 | { 36 | ID: 20_246, 37 | Title: "~Hack~ Knuckles the Echidna in Sonic the Hedgehog", 38 | ImageIcon: "/Images/074560.png", 39 | ConsoleID: 1, 40 | ConsoleName: "Genesis/Mega Drive", 41 | PointsTotal: 1500, 42 | AchievementsPublished: 50, 43 | }, 44 | ], 45 | }; 46 | 47 | server.use( 48 | http.get(`${apiBaseUrl}/API_GetUserWantToPlayList.php`, () => 49 | HttpResponse.json(mockResponse) 50 | ) 51 | ); 52 | 53 | // ACT 54 | const response = await getUserWantToPlayList(authorization, { 55 | username: "xelnia", 56 | }); 57 | 58 | // ASSERT 59 | expect(response).toEqual({ 60 | count: 100, 61 | total: 1287, 62 | results: [ 63 | { 64 | id: 20_246, 65 | title: "~Hack~ Knuckles the Echidna in Sonic the Hedgehog", 66 | imageIcon: "/Images/074560.png", 67 | consoleId: 1, 68 | consoleName: "Genesis/Mega Drive", 69 | pointsTotal: 1500, 70 | achievementsPublished: 50, 71 | }, 72 | ], 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/user/getUserWantToPlayList.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apiBaseUrl, 3 | buildRequestUrl, 4 | call, 5 | serializeProperties, 6 | } from "../utils/internal"; 7 | import type { AuthObject } from "../utils/public"; 8 | import type { 9 | GetUserWantToPlayListResponse, 10 | UserWantToPlayList, 11 | } from "./models"; 12 | 13 | /** 14 | * A call to this function will retrieve a user's "Want to Play Games" list. 15 | * 16 | * @param authorization An object containing your username and webApiKey. 17 | * This can be constructed with `buildAuthorization()`. 18 | * 19 | * @param payload.username The user for which to retrieve the 20 | * want to play games list for. 21 | * 22 | * @param payload.offset Defaults to 0. The number of entries to skip. 23 | * 24 | * @param payload.count Defaults to 100, has a max of 500. 25 | * 26 | * @example 27 | * ``` 28 | * const wantToPlayList = await getUserWantToPlayList( 29 | * authorization, 30 | * { username: "wv_pinball" } 31 | * ); 32 | * ``` 33 | * 34 | * @returns An object containing a user's list of "Want to Play Games". 35 | * ```json 36 | * { 37 | * "count": 100, 38 | * "total": 1287, 39 | * "results": [ 40 | * { 41 | * "id": 20246, 42 | * "title": "~Hack~ Knuckles the Echidna in Sonic the Hedgehog", 43 | * "imageIcon": "/Images/074560.png", 44 | * "consoleID": 1, 45 | * "consoleName": "Genesis/Mega Drive", 46 | * "pointsTotal": 1500, 47 | * "achievementsPublished": 50 48 | * } 49 | * ] 50 | * } 51 | * ``` 52 | */ 53 | export const getUserWantToPlayList = async ( 54 | authorization: AuthObject, 55 | payload: { username: string; offset?: number; count?: number } 56 | ): Promise => { 57 | const queryParams: Record = {}; 58 | queryParams.u = payload.username; 59 | if (payload?.offset) { 60 | queryParams.o = payload.offset; 61 | } 62 | if (payload?.count) { 63 | queryParams.c = payload.count; 64 | } 65 | 66 | const url = buildRequestUrl( 67 | apiBaseUrl, 68 | "/API_GetUserWantToPlayList.php", 69 | authorization, 70 | queryParams 71 | ); 72 | 73 | const rawResponse = await call({ url }); 74 | 75 | return serializeProperties(rawResponse); 76 | }; 77 | -------------------------------------------------------------------------------- /src/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getAchievementsEarnedBetween"; 2 | export * from "./getAchievementsEarnedOnDay"; 3 | export * from "./getGameInfoAndUserProgress"; 4 | export * from "./getUserAwards"; 5 | export * from "./getUserClaims"; 6 | export * from "./getUserCompletedGames"; 7 | export * from "./getUserCompletionProgress"; 8 | export * from "./getUserGameRankAndScore"; 9 | export * from "./getUserPoints"; 10 | export * from "./getUserProfile"; 11 | export * from "./getUserProgress"; 12 | export * from "./getUserRecentAchievements"; 13 | export * from "./getUserRecentlyPlayedGames"; 14 | export * from "./getUserSummary"; 15 | export * from "./getUserWantToPlayList"; 16 | export * from "./models"; 17 | -------------------------------------------------------------------------------- /src/user/models/award-type.model.ts: -------------------------------------------------------------------------------- 1 | export type AwardType = 2 | | "Achievement Points Yield" 3 | | "Achievement Unlocks Yield" 4 | | "Certified Legend" 5 | | "Game Beaten" 6 | | "Invalid or deprecated award type" 7 | | "Mastery/Completion" 8 | | "Patreon Supporter"; 9 | -------------------------------------------------------------------------------- /src/user/models/dated-user-achievement.model.ts: -------------------------------------------------------------------------------- 1 | import type { AchievementType } from "../../achievement"; 2 | 3 | export type DatedUserAchievement = { 4 | date: string; 5 | hardcoreMode: boolean; 6 | achievementId: number; 7 | title: string; 8 | description: string; 9 | badgeName: string; 10 | points: number; 11 | author: string; 12 | gameTitle: string; 13 | gameIcon: string; 14 | gameId: number; 15 | consoleName: string; 16 | cumulScore: number; 17 | badgeUrl: string; 18 | gameUrl: string; 19 | type: AchievementType; 20 | }; 21 | -------------------------------------------------------------------------------- /src/user/models/dated-user-achievements-response.model.ts: -------------------------------------------------------------------------------- 1 | import type { AchievementType } from "../../achievement"; 2 | 3 | interface DatedUserAchievementResponseEntity { 4 | Date: string; 5 | HardcoreMode: string; 6 | AchievementID: string; 7 | Title: string; 8 | Description: string; 9 | BadgeName: string; 10 | Points: string; 11 | Author: string; 12 | GameTitle: string; 13 | GameIcon: string; 14 | GameID: string; 15 | ConsoleName: string; 16 | CumulScore: number; 17 | BadgeURL: string; 18 | GameURL: string; 19 | Type: AchievementType; 20 | } 21 | 22 | export type DatedUserAchievementsResponse = 23 | DatedUserAchievementResponseEntity[]; 24 | -------------------------------------------------------------------------------- /src/user/models/game-info-and-user-progress.model.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GameExtended, 3 | GameExtendedAchievementEntity, 4 | } from "../../game/models"; 5 | import type { AwardKind } from "../../utils/public"; 6 | 7 | export type GameExtendedAchievementEntityWithUserProgress = 8 | GameExtendedAchievementEntity & { 9 | dateEarned: string; 10 | dateEarnedHardcore: string; 11 | }; 12 | 13 | export interface GameInfoAndUserProgress extends GameExtended { 14 | achievements: Record; 15 | 16 | numAwardedToUser: number; 17 | numAwardedToUserHardcore: number; 18 | userCompletion: string; 19 | userCompletionHardcore: string; 20 | 21 | highestAwardKind?: AwardKind | null; 22 | highestAwardDate?: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/user/models/get-game-info-and-user-progress-response.model.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GameExtendedRawAchievementEntity, 3 | GetGameExtendedResponse, 4 | } from "../../game/models"; 5 | import type { AwardKind } from "../../utils/public"; 6 | 7 | type GetGameExtendedResponseWithoutClaims = Omit< 8 | GetGameExtendedResponse, 9 | "Claims" 10 | >; 11 | 12 | type GameExtendedRawAchievementEntityWithUserProgress = 13 | GameExtendedRawAchievementEntity & { 14 | DateEarned: string; 15 | DateEarnedHardcore: string; 16 | }; 17 | 18 | export interface GetGameInfoAndUserProgressResponse 19 | extends GetGameExtendedResponseWithoutClaims { 20 | Achievements: Record< 21 | number, 22 | GameExtendedRawAchievementEntityWithUserProgress 23 | >; 24 | 25 | NumAwardedToUser: number; 26 | NumAwardedToUserHardcore: number; 27 | UserCompletion: string; 28 | UserCompletionHardcore: string; 29 | 30 | HighestAwardKind?: AwardKind | null; 31 | HighestAwardDate?: string; 32 | } 33 | -------------------------------------------------------------------------------- /src/user/models/get-user-awards-response.model.ts: -------------------------------------------------------------------------------- 1 | import type { AwardType } from "./award-type.model"; 2 | 3 | interface GetUserAwardsEntity { 4 | AwardedAt: string; 5 | AwardType: AwardType; 6 | AwardData: number; 7 | AwardDataExtra: number; 8 | DisplayOrder: number; 9 | Title: string; 10 | ConsoleName: string; 11 | Flags: number | null; 12 | ImageIcon: string; 13 | } 14 | 15 | export interface GetUserAwardsResponse { 16 | TotalAwardsCount: number; 17 | HiddenAwardsCount: number; 18 | MasteryAwardsCount: number; 19 | CompletionAwardsCount: number; 20 | BeatenHardcoreAwardsCount: number; 21 | BeatenSoftcoreAwardsCount: number; 22 | EventAwardsCount: number; 23 | SiteAwardsCount: number; 24 | VisibleUserAwards: GetUserAwardsEntity[]; 25 | } 26 | -------------------------------------------------------------------------------- /src/user/models/get-user-completed-games-response.model.ts: -------------------------------------------------------------------------------- 1 | interface UserCompletedGamesResponseEntity { 2 | GameID: string; 3 | Title: string; 4 | ImageIcon: string; 5 | ConsoleID: string; 6 | ConsoleName: string; 7 | MaxPossible: string; 8 | NumAwarded: string; 9 | PctWon: string; 10 | HardcoreMode: "0" | "1"; 11 | } 12 | 13 | export type GetUserCompletedGamesResponse = UserCompletedGamesResponseEntity[]; 14 | -------------------------------------------------------------------------------- /src/user/models/get-user-completion-progress-response.model.ts: -------------------------------------------------------------------------------- 1 | import type { AwardKind } from "../../utils/public"; 2 | 3 | interface RawUserCompletionProgressEntity { 4 | GameID: number; 5 | Title: string; 6 | ImageIcon: string; 7 | ConsoleID: number; 8 | ConsoleName: string; 9 | MaxPossible: number; 10 | NumAwarded: number; 11 | NumAwardedHardcore: number; 12 | 13 | MostRecentAwardedDate?: string; 14 | HighestAwardKind?: AwardKind | null; 15 | HighestAwardDate?: string | null; 16 | } 17 | 18 | export interface GetUserCompletionProgressResponse { 19 | Count: number; 20 | Total: number; 21 | Results: RawUserCompletionProgressEntity[]; 22 | } 23 | -------------------------------------------------------------------------------- /src/user/models/get-user-game-rank-and-score-response.model.ts: -------------------------------------------------------------------------------- 1 | interface UserGameRankAndScoreResponseEntity { 2 | User: string; 3 | TotalScore: string; 4 | LastAward: string; 5 | UserRank: string; 6 | } 7 | 8 | export type GetUserGameRankAndScoreResponse = 9 | UserGameRankAndScoreResponseEntity[]; 10 | -------------------------------------------------------------------------------- /src/user/models/get-user-points-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GetUserPointsResponse { 2 | Points: number; 3 | SoftcorePoints: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/user/models/get-user-profile-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GetUserProfileResponse { 2 | User: string; 3 | UserPic: string; 4 | MemberSince: string; 5 | RichPresenceMsg: string; 6 | LastGameID: number; 7 | ContribCount: number; 8 | ContribYield: number; 9 | TotalPoints: number; 10 | TotalSoftcorePoints: number; 11 | TotalTruePoints: number; 12 | Permissions: number; 13 | Untracked: number; 14 | ID: number; 15 | UserWallActive: number; 16 | Motto: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/user/models/get-user-progress-response.model.ts: -------------------------------------------------------------------------------- 1 | interface UserProgressResponseEntity { 2 | NumPossibleAchievements: string; 3 | PossibleScore: string; 4 | NumAchieved: number | string; 5 | ScoreAchieved: number | string; 6 | NumAchievedHardcore: number | string; 7 | ScoreAchievedHardcore: number | string; 8 | } 9 | 10 | export type GetUserProgressResponse = Record< 11 | `${number}`, 12 | UserProgressResponseEntity 13 | >; 14 | -------------------------------------------------------------------------------- /src/user/models/get-user-recent-achievements-response.model.ts: -------------------------------------------------------------------------------- 1 | interface GetUserRecentAchievementsEntity { 2 | Date: string; 3 | HardcoreMode: 0 | 1; 4 | AchievementID: number; 5 | Title: string; 6 | Description: string; 7 | BadgeName: string; 8 | Points: number; 9 | Author: string; 10 | GameTitle: string; 11 | GameIcon: string; 12 | GameID: number; 13 | ConsoleName: string; 14 | BadgeURL: string; 15 | GameURL: string; 16 | } 17 | 18 | export type GetUserRecentAchievementsResponse = 19 | GetUserRecentAchievementsEntity[]; 20 | -------------------------------------------------------------------------------- /src/user/models/get-user-recently-played-games-response.model.ts: -------------------------------------------------------------------------------- 1 | interface UserRecentlyPlayedGameResponseEntity { 2 | GameID: string; 3 | ConsoleID: string; 4 | ConsoleName: string; 5 | Title: string; 6 | ImageIcon: string; 7 | LastPlayed: string; 8 | MyVote: "1" | "2" | "3" | "4" | "5" | null; 9 | NumPossibleAchievements: string; 10 | PossibleScore: string; 11 | NumAchieved: string | number; 12 | ScoreAchieved: string | number; 13 | NumAchievedHardcore: string | number; 14 | ScoreAchievedHardcore: string | number; 15 | } 16 | 17 | export type GetUserRecentlyPlayedGamesResponse = 18 | UserRecentlyPlayedGameResponseEntity[]; 19 | -------------------------------------------------------------------------------- /src/user/models/get-user-summary-response.model.ts: -------------------------------------------------------------------------------- 1 | interface RecentlyPlayedGameEntity { 2 | GameID: string; 3 | ConsoleID: string; 4 | ConsoleName: string; 5 | Title: string; 6 | ImageIcon: string; 7 | LastPlayed: string; 8 | } 9 | 10 | interface RecentlyAwardedAchievementEntity { 11 | NumPossibleAchievements: string; 12 | PossibleScore: string; 13 | NumAchieved: number | string; 14 | ScoreAchieved: number | string; 15 | NumAchievedHardcore: number | string; 16 | ScoreAchievedHardcore: number | string; 17 | } 18 | 19 | interface ExtendedRecentAchievementEntity { 20 | ID: string; 21 | GameID: string; 22 | GameTitle: string; 23 | Title: string; 24 | Description: string; 25 | Points: string; 26 | BadgeName: string; 27 | IsAwarded: "1"; 28 | DateAwarded: string; 29 | HardcoreAchieved: "0"; 30 | } 31 | 32 | interface LastGameEntity { 33 | ID: number; 34 | Title: string; 35 | ConsoleID: number; 36 | ForumTopicID: number; 37 | Flags: number; 38 | ImageIcon: string; 39 | ImageTitle: string; 40 | ImageIngame: string; 41 | ImageBoxArt: string; 42 | Publisher: string; 43 | Developer: string; 44 | Genre: string; 45 | Released: string; 46 | IsFinal: boolean; 47 | ConsoleName: string; 48 | RichPresencePatch: string; 49 | } 50 | 51 | export interface GetUserSummaryResponse { 52 | RecentlyPlayedCount: number; 53 | RecentlyPlayed: RecentlyPlayedGameEntity[]; 54 | MemberSince: string; 55 | 56 | LastActivity: { 57 | ID: string; 58 | timestamp: string; 59 | lastupdate: string; 60 | activitytype: string; 61 | User: string; 62 | data: string; 63 | data2: string; 64 | }; 65 | 66 | RichPresenceMsg: string; 67 | LastGameID: string; 68 | LastGame: LastGameEntity; 69 | ContribCount: string; 70 | ContribYield: string; 71 | TotalPoints: string; 72 | TotalSoftcorePoints: string; 73 | TotalTruePoints: string; 74 | Permissions: string; 75 | Untracked: "0" | "1"; 76 | ID: string; 77 | UserWallActive: "0" | "1"; 78 | Motto: string; 79 | Rank: number; 80 | Awarded: Record<`${number}`, RecentlyAwardedAchievementEntity>; 81 | 82 | RecentAchievements: Record< 83 | `${number}`, 84 | Record<`${number}`, ExtendedRecentAchievementEntity> 85 | >; 86 | 87 | Points: string; 88 | SoftcorePoints: string; 89 | UserPic: string; 90 | TotalRanked: number; 91 | Status: string; 92 | } 93 | -------------------------------------------------------------------------------- /src/user/models/get-user-want-to-play-list-response.model.ts: -------------------------------------------------------------------------------- 1 | export interface GetUserWantToPlayListResponse { 2 | Count: number; 3 | Total: number; 4 | Results: Array<{ 5 | ID: number; 6 | Title: string; 7 | ImageIcon: string; 8 | ConsoleID: number; 9 | ConsoleName: string; 10 | PointsTotal: number; 11 | AchievementsPublished: number; 12 | }>; 13 | } 14 | -------------------------------------------------------------------------------- /src/user/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./award-type.model"; 2 | export * from "./dated-user-achievement.model"; 3 | export * from "./dated-user-achievements-response.model"; 4 | export * from "./game-info-and-user-progress.model"; 5 | export * from "./get-game-info-and-user-progress-response.model"; 6 | export * from "./get-user-awards-response.model"; 7 | export * from "./get-user-completed-games-response.model"; 8 | export * from "./get-user-completion-progress-response.model"; 9 | export * from "./get-user-game-rank-and-score-response.model"; 10 | export * from "./get-user-points-response.model"; 11 | export * from "./get-user-profile-response.model"; 12 | export * from "./get-user-progress-response.model"; 13 | export * from "./get-user-recent-achievements-response.model"; 14 | export * from "./get-user-recently-played-games-response.model"; 15 | export * from "./get-user-summary-response.model"; 16 | export * from "./get-user-want-to-play-list-response.model"; 17 | export * from "./user-awards.model"; 18 | export * from "./user-claims.model"; 19 | export * from "./user-claims-response.model"; 20 | export * from "./user-completed-games.model"; 21 | export * from "./user-completion-progress.model"; 22 | export * from "./user-completion-progress-entity.model"; 23 | export * from "./user-game-rank-and-score.model"; 24 | export * from "./user-points.model"; 25 | export * from "./user-profile.model"; 26 | export * from "./user-progress.model"; 27 | export * from "./user-recent-achievement.model"; 28 | export * from "./user-recently-played-games.model"; 29 | export * from "./user-summary.model"; 30 | export * from "./user-want-to-play-list.model"; 31 | -------------------------------------------------------------------------------- /src/user/models/user-awards.model.ts: -------------------------------------------------------------------------------- 1 | import type { AwardType } from "./award-type.model"; 2 | 3 | interface UserAward { 4 | awardedAt: string; 5 | awardType: AwardType; 6 | awardData: number; 7 | awardDataExtra: number; 8 | displayOrder: number; 9 | title: string; 10 | consoleName: string; 11 | flags: number | null; 12 | imageIcon: string; 13 | } 14 | 15 | export interface UserAwards { 16 | totalAwardsCount: number; 17 | hiddenAwardsCount: number; 18 | masteryAwardsCount: number; 19 | completionAwardsCount: number; 20 | beatenHardcoreAwardsCount: number; 21 | beatenSoftcoreAwardsCount: number; 22 | eventAwardsCount: number; 23 | siteAwardsCount: number; 24 | visibleUserAwards: UserAward[]; 25 | } 26 | -------------------------------------------------------------------------------- /src/user/models/user-claims-response.model.ts: -------------------------------------------------------------------------------- 1 | interface UserClaimsResponseEntity { 2 | ID: string; 3 | User: string; 4 | GameID: string; 5 | GameTitle: string; 6 | GameIcon: string; 7 | ConsoleName: string; 8 | ClaimType: string; 9 | SetType: string; 10 | Status: string; 11 | Extension: string; 12 | Special: string; 13 | Created: string; 14 | DoneTime: string; 15 | Updated: string; 16 | MinutesLeft: string; 17 | } 18 | 19 | export type GetUserClaimsResponse = UserClaimsResponseEntity[]; 20 | -------------------------------------------------------------------------------- /src/user/models/user-claims.model.ts: -------------------------------------------------------------------------------- 1 | interface UserClaim { 2 | id: number; 3 | user: string; 4 | gameId: number; 5 | gameTitle: string; 6 | gameIcon: string; 7 | consoleName: string; 8 | claimType: number; 9 | setType: number; 10 | status: number; 11 | extension: number; 12 | special: number; 13 | created: string; 14 | doneTime: string; 15 | updated: string; 16 | minutesLeft: number; 17 | } 18 | 19 | export type UserClaims = UserClaim[]; 20 | -------------------------------------------------------------------------------- /src/user/models/user-completed-games.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserCompletedGame { 2 | gameId: number; 3 | title: string; 4 | imageIcon: string; 5 | consoleId: number; 6 | consoleName: string; 7 | maxPossible: number; 8 | numAwarded: number; 9 | pctWon: number; 10 | hardcoreMode: boolean; 11 | } 12 | 13 | export type UserCompletedGames = UserCompletedGame[]; 14 | -------------------------------------------------------------------------------- /src/user/models/user-completion-progress-entity.model.ts: -------------------------------------------------------------------------------- 1 | import type { AwardKind } from "../../utils/public"; 2 | 3 | export interface UserCompletionProgressEntity { 4 | gameId: number; 5 | title: string; 6 | imageIcon: string; 7 | consoleId: number; 8 | consoleName: string; 9 | maxPossible: number; 10 | numAwarded: number; 11 | numAwardedHardcore: number; 12 | 13 | mostRecentAwardedDate?: string; 14 | highestAwardKind?: AwardKind | null; 15 | highestAwardDate?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/user/models/user-completion-progress.model.ts: -------------------------------------------------------------------------------- 1 | import type { UserCompletionProgressEntity } from "./user-completion-progress-entity.model"; 2 | 3 | export interface UserCompletionProgress { 4 | count: number; 5 | total: number; 6 | results: UserCompletionProgressEntity[]; 7 | } 8 | -------------------------------------------------------------------------------- /src/user/models/user-game-rank-and-score.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserGameRankAndScoreEntity { 2 | user: string; 3 | totalScore: number; 4 | lastAward: string; 5 | userRank: number; 6 | } 7 | 8 | export type UserGameRankAndScore = UserGameRankAndScoreEntity[]; 9 | -------------------------------------------------------------------------------- /src/user/models/user-points.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserPoints { 2 | points: number; 3 | softcorePoints: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/user/models/user-profile.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserProfile { 2 | user: string; 3 | userPic: string; 4 | memberSince: string; 5 | richPresenceMsg: string; 6 | lastGameId: number; 7 | contribCount: number; 8 | contribYield: number; 9 | totalPoints: number; 10 | totalSoftcorePoints: number; 11 | totalTruePoints: number; 12 | permissions: number; 13 | untracked: boolean; 14 | id: number; 15 | userWallActive: boolean; 16 | motto: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/user/models/user-progress.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserProgressEntity { 2 | numPossibleAchievements: number; 3 | possibleScore: number; 4 | numAchieved: number; 5 | scoreAchieved: number; 6 | numAchievedHardcore: number; 7 | scoreAchievedHardcore: number; 8 | } 9 | 10 | export type UserProgress = Record<`${number}`, UserProgressEntity>; 11 | -------------------------------------------------------------------------------- /src/user/models/user-recent-achievement.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserRecentAchievement { 2 | date: string; 3 | hardcoreMode: boolean; 4 | achievementId: number; 5 | title: string; 6 | description: string; 7 | badgeName: string; 8 | points: number; 9 | author: string; 10 | gameTitle: string; 11 | gameIcon: string; 12 | gameId: number; 13 | consoleName: string; 14 | badgeUrl: string; 15 | gameUrl: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/user/models/user-recently-played-games.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserRecentlyPlayedGameEntity { 2 | gameId: number; 3 | consoleId: number; 4 | consoleName: string; 5 | title: string; 6 | imageIcon: string; 7 | lastPlayed: string; 8 | myVote: 1 | 2 | 3 | 4 | 5 | null; 9 | numPossibleAchievements: number; 10 | possibleScore: number; 11 | numAchieved: number; 12 | scoreAchieved: number; 13 | numAchievedHardcore: number; 14 | scoreAchievedHardcore: number; 15 | } 16 | 17 | export type UserRecentlyPlayedGames = UserRecentlyPlayedGameEntity[]; 18 | -------------------------------------------------------------------------------- /src/user/models/user-summary.model.ts: -------------------------------------------------------------------------------- 1 | export interface RecentlyPlayedGameEntity { 2 | gameId: number; 3 | consoleId: number; 4 | consoleName: string; 5 | title: string; 6 | imageIcon: string; 7 | lastPlayed: string; 8 | } 9 | 10 | export interface LastActivityEntity { 11 | id: number; 12 | timestamp: string; 13 | lastupdate: string; 14 | activitytype: number; 15 | user: string; 16 | data: string; 17 | data2: string; 18 | } 19 | 20 | export interface LastGameEntity { 21 | id: number; 22 | title: string; 23 | consoleId: number; 24 | forumTopicId: number; 25 | flags: number; 26 | imageIcon: string; 27 | imageTitle: string; 28 | imageIngame: string; 29 | imageBoxArt: string; 30 | publisher: string; 31 | developer: string; 32 | genre: string; 33 | released: string; 34 | isFinal: boolean; 35 | consoleName: string; 36 | richPresencePatch: string; 37 | } 38 | 39 | export interface AwardedGameEntity { 40 | numPossibleAchievements: number; 41 | possibleScore: number; 42 | numAchieved: number; 43 | scoreAchieved: number; 44 | numAchievedHardcore: number; 45 | scoreAchievedHardcore: number; 46 | } 47 | 48 | export interface ExtendedRecentAchievementEntity { 49 | id: number; 50 | gameId: number; 51 | gameTitle: string; 52 | title: string; 53 | description: string; 54 | points: number; 55 | badgeName: string; 56 | isAwarded: true; 57 | dateAwarded: string; 58 | hardcoreAchieved: false; 59 | } 60 | 61 | export interface UserSummary { 62 | recentlyPlayedCount: number; 63 | recentlyPlayed: RecentlyPlayedGameEntity[]; 64 | memberSince: string; 65 | lastActivity: LastActivityEntity; 66 | richPresenceMsg: string; 67 | lastGameId: number; 68 | lastGame: LastGameEntity; 69 | contribCount: number; 70 | contribYield: number; 71 | totalPoints: number; 72 | totalSoftcorePoints: number; 73 | totalTruePoints: number; 74 | permissions: number; 75 | untracked: boolean; 76 | id: number; 77 | userWallActive: boolean; 78 | motto: string; 79 | rank: number; 80 | awarded: Record<`${number}`, AwardedGameEntity>; 81 | 82 | recentAchievements: Record< 83 | `${number}`, 84 | Record<`${number}`, ExtendedRecentAchievementEntity> 85 | >; 86 | 87 | points: number; 88 | softcorePoints: number; 89 | userPic: string; 90 | totalRanked: number; 91 | status: string; 92 | } 93 | -------------------------------------------------------------------------------- /src/user/models/user-want-to-play-list.model.ts: -------------------------------------------------------------------------------- 1 | export interface UserWantToPlayList { 2 | count: number; 3 | total: number; 4 | results: Array<{ 5 | id: number; 6 | title: string; 7 | imageIcon: string; 8 | consoleId: number; 9 | consoleName: string; 10 | pointsTotal: number; 11 | achievementsPublished: number; 12 | }>; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/internal/apiBaseUrl.ts: -------------------------------------------------------------------------------- 1 | export const apiBaseUrl = "https://retroachievements.org/API"; 2 | -------------------------------------------------------------------------------- /src/utils/internal/buildRequestUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { buildRequestUrl } from "./buildRequestUrl"; 2 | 3 | describe("Util: buildRequestUrl", () => { 4 | it("is defined #sanity", () => { 5 | // ASSERT 6 | expect(buildRequestUrl).toBeDefined(); 7 | }); 8 | 9 | it("given a baseUrl, endpointUrl, and some arguments, returns a correctly-constructed URL", () => { 10 | // ARRANGE 11 | const baseUrl = "https://retroachievements.org/API/"; 12 | const endpointUrl = "/:baz/API_GetConsoleIDs.php"; 13 | 14 | const args = { 15 | baz: "myBazValue", 16 | limit: 10, 17 | offset: 2, 18 | notDefined: undefined, 19 | }; 20 | 21 | // ACT 22 | const requestUrl = buildRequestUrl( 23 | baseUrl, 24 | endpointUrl, 25 | { username: "TestUser", webApiKey: "mockWebApiKey" }, 26 | args as any 27 | ); 28 | 29 | // ASSERT 30 | expect(requestUrl).toEqual( 31 | "https://retroachievements.org/API/myBazValue/API_GetConsoleIDs.php?z=TestUser&y=mockWebApiKey&limit=10&offset=2" 32 | ); 33 | }); 34 | 35 | it("given no arguments, returns a correctly-constructed URL", () => { 36 | // ARRANGE 37 | const baseUrl = "https://retroachievements.org/API/"; 38 | const endpointUrl = "/:baz/API_GetConsoleIDs.php"; 39 | 40 | // ACT 41 | const requestUrl = buildRequestUrl(baseUrl, endpointUrl, { 42 | username: "TestUser", 43 | webApiKey: "mockWebApiKey", 44 | }); 45 | 46 | // ASSERT 47 | expect(requestUrl).toEqual( 48 | "https://retroachievements.org/API/:baz/API_GetConsoleIDs.php?z=TestUser&y=mockWebApiKey" 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/utils/internal/buildRequestUrl.ts: -------------------------------------------------------------------------------- 1 | import type { AuthObject } from "../public/models"; 2 | 3 | export const buildRequestUrl = ( 4 | baseUrl: string, 5 | endpointUrl: string, 6 | authObject: AuthObject, 7 | args: Record = {} 8 | ) => { 9 | const concatenated = `${baseUrl}/${endpointUrl}`; 10 | const withoutDoubleSlashes = concatenated.replaceAll(/([^:]\/)\/+/g, "$1"); 11 | 12 | let withArgs = withoutDoubleSlashes; 13 | 14 | // `z` and `y` are expected query params from the RA API. 15 | // Authentication is handled purely by query params. 16 | const queryParamValues: Record = { 17 | z: authObject.username, 18 | y: authObject.webApiKey, 19 | }; 20 | 21 | for (const [argKey, argValue] of Object.entries(args)) { 22 | // "abc.com/some-route/:foo/some-path" & {"foo": 4} --> "abc.com/some-route/4/some-path" 23 | if (withArgs.includes(`:${argKey}`)) { 24 | withArgs = withArgs.replace(`:${argKey}`, String(argValue)); 25 | } else if (argValue !== undefined) { 26 | queryParamValues[argKey] = String(argValue); 27 | } 28 | } 29 | 30 | const queryString = new URLSearchParams(queryParamValues).toString(); 31 | return `${withArgs}?${queryString}`; 32 | }; 33 | -------------------------------------------------------------------------------- /src/utils/internal/call.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | 4 | import { call } from "./call"; 5 | 6 | const server = setupServer(); 7 | 8 | describe("Util: call", () => { 9 | // MSW Setup 10 | beforeAll(() => server.listen()); 11 | afterEach(() => server.resetHandlers()); 12 | afterAll(() => server.close()); 13 | 14 | it("is defined #sanity", () => { 15 | // ASSERT 16 | expect(call).toBeDefined(); 17 | }); 18 | 19 | it("given a url, makes a GET call", async () => { 20 | // ARRANGE 21 | let receivedMethod = ""; 22 | 23 | const mockRequestUrl = "https://abc.xyz/v1/endpoint"; 24 | 25 | server.use( 26 | http.get(mockRequestUrl, (info) => { 27 | receivedMethod = info.request.method; 28 | return HttpResponse.json({ foo: "bar" }); 29 | }) 30 | ); 31 | 32 | // ACT 33 | const response = await call({ url: mockRequestUrl }); 34 | 35 | // ASSERT 36 | expect(response).toEqual({ foo: "bar" }); 37 | expect(receivedMethod).toEqual("GET"); 38 | }); 39 | 40 | it("given the call returns an error, throws an error", async () => { 41 | // ARRANGE 42 | const mockRequestUrl = "https://abc.xyz/v1/endpoint2"; 43 | 44 | server.use( 45 | http.get(mockRequestUrl, () => 46 | HttpResponse.text("something bad happened", { 47 | status: 503, 48 | }) 49 | ) 50 | ); 51 | 52 | // ASSERT 53 | await expect(call({ url: mockRequestUrl })).rejects.toThrow(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/utils/internal/call.ts: -------------------------------------------------------------------------------- 1 | const packageVersion = process.env?.["PACKAGE_VERSION"] ?? "Unknown"; 2 | 3 | /** 4 | * Fetch an HTTP resource. This is publicly exposed in the 5 | * event you would like to access an endpoint that this 6 | * library does not currently support. 7 | * 8 | * UNLESS YOU'RE SURE OF WHAT YOU'RE DOING, YOU PROBABLY 9 | * SHOULDN'T USE THIS FUNCTION. 10 | */ 11 | export const call = async < 12 | T extends readonly any[] | Record 13 | >(config: { 14 | url: string; 15 | }) => { 16 | const { url } = config; 17 | 18 | const headers = new Headers({ 19 | "User-Agent": `RetroAchievements-api-js/${packageVersion}`, 20 | }); 21 | 22 | const rawResponse = await fetch(url, { headers }); 23 | 24 | if (!rawResponse.ok) { 25 | throw new Error( 26 | `HTTP Error: Status ${rawResponse.status} ${rawResponse.statusText}` 27 | ); 28 | } 29 | 30 | return (await rawResponse.json()) as T; 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/internal/id.model.ts: -------------------------------------------------------------------------------- 1 | export type ID = string | number; 2 | -------------------------------------------------------------------------------- /src/utils/internal/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./apiBaseUrl"; 2 | export * from "./buildRequestUrl"; 3 | export * from "./call"; 4 | export * from "./id.model"; 5 | export * from "./serializeProperties"; 6 | -------------------------------------------------------------------------------- /src/utils/internal/serializeProperties.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-duplicate-string */ 2 | 3 | import { serializeProperties } from "./serializeProperties"; 4 | 5 | describe("Util: serializeProperties", () => { 6 | it("is defined #sanity", () => { 7 | // ASSERT 8 | expect(serializeProperties).toBeDefined(); 9 | }); 10 | 11 | it("can serialize a simple object", () => { 12 | // ARRANGE 13 | const originalObject = { 14 | ID: 1, 15 | GameID: 100, 16 | AchievementIDs: [1, 2, 3, 4, 5], 17 | RAPoints: 100, 18 | ConsoleName: "PlayStation Portable", 19 | }; 20 | 21 | // ACT 22 | const sanitizedObject = serializeProperties(originalObject); 23 | 24 | // ASSERT 25 | expect(sanitizedObject).toEqual({ 26 | id: 1, 27 | gameId: 100, 28 | achievementIds: [1, 2, 3, 4, 5], 29 | raPoints: 100, 30 | consoleName: "PlayStation Portable", 31 | }); 32 | }); 33 | 34 | it("can serialize an array of objects", () => { 35 | // ARRANGE 36 | const originalArray = [ 37 | { 38 | ID: 1, 39 | GameID: 100, 40 | AchievementIDs: [1, 2, 3, 4, 5], 41 | RAPoints: 100, 42 | ConsoleName: "PlayStation Portable", 43 | }, 44 | ]; 45 | 46 | // ACT 47 | const sanitizedArray = serializeProperties(originalArray); 48 | 49 | // ASSERT 50 | expect(sanitizedArray).toEqual([ 51 | { 52 | id: 1, 53 | gameId: 100, 54 | achievementIds: [1, 2, 3, 4, 5], 55 | raPoints: 100, 56 | consoleName: "PlayStation Portable", 57 | }, 58 | ]); 59 | }); 60 | 61 | it("can serialize an object that contains nested objects", () => { 62 | // ARRANGE 63 | const originalObject = { 64 | ID: 1, 65 | GameID: 100, 66 | AchievementIDs: [1, 2, 3, 4, 5], 67 | RAPoints: 100, 68 | ConsoleName: "PlayStation Portable", 69 | UserMeta: { 70 | Name: "xelnia", 71 | Points: 100, 72 | }, 73 | }; 74 | 75 | // ACT 76 | const sanitizedObject = serializeProperties(originalObject); 77 | 78 | // ASSERT 79 | expect(sanitizedObject).toEqual({ 80 | id: 1, 81 | gameId: 100, 82 | achievementIds: [1, 2, 3, 4, 5], 83 | raPoints: 100, 84 | consoleName: "PlayStation Portable", 85 | userMeta: { 86 | name: "xelnia", 87 | points: 100, 88 | }, 89 | }); 90 | }); 91 | 92 | it("can be instructed to cast values of certain keys to numbers", () => { 93 | // ARRANGE 94 | const originalObject = { 95 | UserName: "xelnia", 96 | Points: "500", 97 | Metadata: { 98 | TitleID: "100", 99 | }, 100 | }; 101 | 102 | // ACT 103 | const sanitizedObject = serializeProperties(originalObject, { 104 | shouldCastToNumbers: ["TitleID", "Points"], 105 | }); 106 | 107 | // ASSERT 108 | expect(sanitizedObject).toEqual({ 109 | userName: "xelnia", 110 | points: 500, 111 | metadata: { 112 | titleId: 100, 113 | }, 114 | }); 115 | }); 116 | 117 | it("can be instructed to map values of certain keys to booleans", () => { 118 | // ARRANGE 119 | const originalObject = { 120 | UserName: "xelnia", 121 | HardcoreMode: "0", 122 | Metadata: { 123 | IsCoolGuy: "1", 124 | }, 125 | }; 126 | 127 | // ACT 128 | const sanitizedObject = serializeProperties(originalObject, { 129 | shouldMapToBooleans: ["HardcoreMode", "IsCoolGuy"], 130 | }); 131 | 132 | // ASSERT 133 | expect(sanitizedObject).toEqual({ 134 | userName: "xelnia", 135 | hardcoreMode: false, 136 | metadata: { 137 | isCoolGuy: true, 138 | }, 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /src/utils/internal/serializeProperties.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/cognitive-complexity */ 2 | /* eslint-disable sonarjs/prefer-immediate-return */ 3 | 4 | export const serializeProperties = ( 5 | originalData: any, 6 | options: Partial<{ 7 | shouldCastToNumbers: string[]; 8 | shouldMapToBooleans: string[]; 9 | }> = {} 10 | ) => { 11 | const { shouldCastToNumbers, shouldMapToBooleans } = options; 12 | 13 | let returnValue = originalData; 14 | 15 | if (Array.isArray(originalData)) { 16 | const cleanedArray: any[] = []; 17 | 18 | for (const entity of originalData) { 19 | cleanedArray.push(serializeProperties(entity, options)); 20 | } 21 | 22 | returnValue = cleanedArray; 23 | } else if (!Array.isArray(originalData) && originalData instanceof Object) { 24 | let cleanedObject: Record = {}; 25 | 26 | for (const [originalKey, originalValue] of Object.entries(originalData)) { 27 | let sanitizedValue = originalValue; 28 | if (shouldCastToNumbers?.includes(originalKey)) { 29 | sanitizedValue = originalValue === null ? null : Number(originalValue); 30 | } 31 | 32 | if (shouldMapToBooleans?.includes(originalKey)) { 33 | if (originalValue === null) { 34 | sanitizedValue = null; 35 | } else { 36 | sanitizedValue = String(originalValue) === "1" ? true : false; 37 | } 38 | } 39 | 40 | cleanedObject = { 41 | ...cleanedObject, 42 | [naiveCamelCase(originalKey)]: serializeProperties( 43 | sanitizedValue, 44 | options 45 | ), 46 | }; 47 | } 48 | 49 | returnValue = cleanedObject; 50 | } 51 | 52 | return returnValue; 53 | }; 54 | 55 | const naiveCamelCase = (originalValue: string) => { 56 | // "ID" --> "id", "URL" --> "url" 57 | if (originalValue.toUpperCase() === originalValue) { 58 | return originalValue.toLowerCase(); 59 | } 60 | 61 | // "GameID" -> "gameID" 62 | let camelCased = 63 | originalValue.charAt(0).toLowerCase() + originalValue.slice(1); 64 | 65 | // "gameID" -> "gameId" 66 | camelCased = camelCased.replaceAll("ID", "Id"); 67 | 68 | // "badgeURL" --> "badgeUrl" 69 | camelCased = camelCased.replaceAll("URL", "Url"); 70 | 71 | // "rAPoints" -> "raPoints" 72 | camelCased = camelCased.replaceAll("rA", "ra"); 73 | 74 | // "visibleUserawards" -> "visibleUserAwards" 75 | camelCased = camelCased.replaceAll("visibleUserawards", "visibleUserAwards"); 76 | 77 | return camelCased; 78 | }; 79 | -------------------------------------------------------------------------------- /src/utils/public/buildAuthorization.test.ts: -------------------------------------------------------------------------------- 1 | import { buildAuthorization } from "./buildAuthorization"; 2 | 3 | describe("Util: buildAuthorization", () => { 4 | it("is defined #sanity", () => { 5 | // ASSERT 6 | expect(buildAuthorization).toBeDefined(); 7 | }); 8 | 9 | it("returns the same object it is given", () => { 10 | // ARRANGE 11 | const myAuth = { 12 | username: "myUserName", 13 | webApiKey: "myWebApiKey", 14 | }; 15 | 16 | // ACT 17 | const authorization = buildAuthorization(myAuth); 18 | 19 | // ASSERT 20 | expect(authorization).toEqual(myAuth); 21 | }); 22 | 23 | it("throws an error if missing a username", () => { 24 | // ASSERT 25 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 26 | // @ts-ignore - We're assuming the user is not using a TypeScript project. 27 | expect(() => buildAuthorization({ webApiKey: "mockWebApiKey" })).toThrow(); 28 | }); 29 | 30 | it("throws an error if missing a webApiKey", () => { 31 | // ASSERT 32 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 33 | // @ts-ignore - We're assuming the user is not using a TypeScript project. 34 | expect(() => buildAuthorization({ username: "mockUserName" })).toThrow(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/utils/public/buildAuthorization.ts: -------------------------------------------------------------------------------- 1 | import type { AuthObject } from "./models"; 2 | 3 | /** 4 | * Accepts your RetroAchievements.org username and web API key. After 5 | * receiving these inputs, the function returns you a value that can be 6 | * used for the authentication parameter by any of the async calls in this 7 | * library. 8 | * 9 | * Your account's personal Web API Key can be found on the Settings page 10 | * of RetroAchievements.org. Do not use a Web API Key that is not associated 11 | * with your account. 12 | * 13 | * @returns An `AuthObject` that you can pass to any of the API call functions. 14 | * 15 | * @example 16 | * ``` 17 | * const authorization = buildAuthorization({ 18 | * username: "Scott", 19 | * webApiKey: "LtjCwW16nJI7cqOyPIQtXk8v1cfF0tmO" 20 | * }); 21 | * ``` 22 | */ 23 | export const buildAuthorization = (options: AuthObject): AuthObject => { 24 | if (!options.username || !options.webApiKey) { 25 | throw new Error(` 26 | buildAuthorization() requires an object containing a 27 | username and webApiKey. eg: 28 | 29 | const authorization = buildAuthorization({ 30 | username: "myUserName", 31 | webApiKey: "myWebApiKey" 32 | }) 33 | `); 34 | } 35 | 36 | return options; 37 | }; 38 | 39 | // This function simply returns what it's given, however the return 40 | // value has the added benefit of type safety. 41 | -------------------------------------------------------------------------------- /src/utils/public/index.ts: -------------------------------------------------------------------------------- 1 | export { call as unsafe_call } from "../internal/call"; 2 | export * from "./buildAuthorization"; 3 | export * from "./models"; 4 | -------------------------------------------------------------------------------- /src/utils/public/models/auth-object.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Each RetroAchievements API call is uniquely authenticated 3 | * using a username + API key combination. Your account's personal 4 | * Web API Key can be found on the Settings page. 5 | */ 6 | export interface AuthObject { 7 | /** 8 | * You or your app's exact username on the RetroAchievements.org website. 9 | * For example, https://retroachievements.org/user/Scott would have a value 10 | * of "Scott". 11 | */ 12 | username: string; 13 | 14 | /** 15 | * This can be found in the "Keys" section of your Settings page on the 16 | * RetroAchievements.org website. This is a 32-digit alphanumeric key 17 | * that is case-sensitive. 18 | */ 19 | webApiKey: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/public/models/award-kind.model.ts: -------------------------------------------------------------------------------- 1 | export type AwardKind = 2 | | "beaten-softcore" 3 | | "beaten-hardcore" 4 | | "completed" 5 | | "mastered"; 6 | -------------------------------------------------------------------------------- /src/utils/public/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth-object.model"; 2 | export * from "./award-kind.model"; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "esnext", 7 | "moduleResolution": "node", 8 | "types": ["vitest/globals"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | root: process.cwd(), 6 | 7 | setupFiles: ["vitest.polyfills.ts"], 8 | 9 | globals: true, 10 | 11 | exclude: [ 12 | "node_modules", 13 | ".next", 14 | ".docusaurus", 15 | "cypress", 16 | "**/node_modules/**", 17 | ], 18 | 19 | coverage: { 20 | include: ["src/**/*.{ts,tsx}"], 21 | exclude: [ 22 | "**/*.model.ts", 23 | "**/*.enum.ts", 24 | "**/index.ts", 25 | "**/test/**", 26 | "**/__playground.ts", 27 | ], 28 | statements: 95, 29 | branches: 80, 30 | functions: 100, 31 | lines: 95, 32 | }, 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /vitest.polyfills.ts: -------------------------------------------------------------------------------- 1 | import { Blob } from "node:buffer"; 2 | import { TextDecoder, TextEncoder } from "node:util"; 3 | 4 | import { fetch, FormData, Headers, Request, Response } from "undici"; 5 | 6 | Reflect.set(globalThis, "TextEncoder", TextEncoder); 7 | Reflect.set(globalThis, "TextDecoder", TextDecoder); 8 | 9 | Reflect.set(globalThis, "fetch", fetch); 10 | Reflect.set(globalThis, "Blob", Blob); 11 | Reflect.set(globalThis, "Request", Request); 12 | Reflect.set(globalThis, "Response", Response); 13 | Reflect.set(globalThis, "Headers", Headers); 14 | Reflect.set(globalThis, "FormData", FormData); 15 | --------------------------------------------------------------------------------