diff --git a/.github/workflows/biome-js.yml b/.github/workflows/biome-js.yml new file mode 100644 index 0000000..b2b6c0e --- /dev/null +++ b/.github/workflows/biome-js.yml @@ -0,0 +1,60 @@ +name: BiomeJS + +on: + push: + branches: [master] + pull_request: + branches: [master] + + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '>=16' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + format: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '>=16' + + - name: Install dependencies + run: npm ci + + - name: Format + run: npm run format + + organized-imports: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: '>=16' + + - name: Install dependencies + run: npm ci + + - name: Organized Imports + run: npm run organized-imports \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 507f75c..7c2be88 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: TypeScript Build +name: TypeScript on: push: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 1e468f6..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Biome Lint - -on: - push: - branches: [master] - pull_request: - branches: [master] - - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Use Node.js - uses: actions/setup-node@v2 - with: - node-version: '>=16' - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e19c11..3d718dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Jest Test +name: Jest on: push: @@ -8,7 +8,7 @@ on: jobs: - build: + test: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index e4c541e..e015465 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,4 @@ # Build files /dist -/docs .env \ No newline at end of file diff --git a/README.md b/README.md index 6c7675a..1f65d40 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # WrapBlox +uhh hii this is for future purple to not forget ok ignore this thanks: +docs locally: mkdocs serve +deploy to pages: mkdocs gh-deploy + Wrapblox is a promise based API wrapper for the Roblox API written in typescript. It is currently in development and really isnt meant for production. ## Installation diff --git a/biome.json b/biome.json index 8e0d224..3e76cab 100644 --- a/biome.json +++ b/biome.json @@ -1,17 +1,39 @@ { - "$schema": "https://biomejs.dev/schemas/1.5.2/schema.json", - "organizeImports": { - "enabled": true + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true, + "defaultBranch": "main" }, + "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "style": { + "noParameterAssign": "off" + } + } + }, + + "json": { + "parser": { + "allowComments": true } }, - "vcs" :{ + + "formatter": { "enabled": true, - "clientKind": "git", - "useIgnoreFile": true + + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120, + + "formatWithErrors": true + }, + + "organizeImports": { + "enabled": true } } diff --git a/changelog.md b/changelog.md index 7975ec0..77e48f3 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. Date format is `MM/DD/YYYY`. +## [0.2.1b] - 1/16/2024 + +### Fixed + +- i actually fixed the types this time + +## [0.2.1a] - 1/16/2024 + +### Fixed + +- Bug where types were not exported and would cause errors + ## [0.2.1] - 1/16/2024 ### Changed diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..000ea34 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# Welcome to MkDocs + +For full documentation visit [mkdocs.org](https://www.mkdocs.org). + +## Commands + +* `mkdocs new [dir-name]` - Create a new project. +* `mkdocs serve` - Start the live-reloading docs server. +* `mkdocs build` - Build the documentation site. +* `mkdocs -h` - Print help message and exit. + +## Project layout + + mkdocs.yml # The configuration file. + docs/ + index.md # The documentation homepage. + ... # Other markdown pages, images and other files. diff --git a/jest.config.ts b/jest.config.ts index 8b441b4..f14c3bd 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -3,5 +3,5 @@ module.exports = { testEnvironment: "node", moduleNameMapper: { "^(\\.\\.?\\/.+)\\.js$": "$1", - }, -}; \ No newline at end of file + }, +}; diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..0656f70 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,32 @@ +site_name: WrapBlox +site_author: PurpleCreativity + +repo_name: WrapBlox +repo_url: https://github.com/SuperCater/WrapBlox + +theme: + name: material + features: + # - navigation.tabs + #- navigation.instant + #- navigation.sections + palette: + # Dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: purple + accent: blue + toggle: + icon: material/weather-night + name: Switch to light mode + # Light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: purple + accent: blue + toggle: + icon: material/weather-sunny + name: Switch to dark mode + highlightjs: true + hljs_languages: + - javascript diff --git a/package-lock.json b/package-lock.json index 942e70d..4e20144 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wrapblox", - "version": "0.5.4", + "version": "0.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wrapblox", - "version": "0.5.4", + "version": "0.6.0", "license": "GPL-3.0-or-later", "dependencies": { "dotenv": "^16.4.5" @@ -1741,169 +1741,252 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz", - "integrity": "sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz", + "integrity": "sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz", - "integrity": "sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz", + "integrity": "sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz", - "integrity": "sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz", + "integrity": "sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz", - "integrity": "sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz", + "integrity": "sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz", + "integrity": "sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz", + "integrity": "sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz", - "integrity": "sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz", + "integrity": "sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz", + "integrity": "sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz", - "integrity": "sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz", + "integrity": "sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz", - "integrity": "sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz", + "integrity": "sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz", + "integrity": "sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz", - "integrity": "sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz", + "integrity": "sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz", + "integrity": "sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", - "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz", + "integrity": "sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz", - "integrity": "sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz", + "integrity": "sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz", - "integrity": "sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz", + "integrity": "sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz", - "integrity": "sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz", + "integrity": "sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz", - "integrity": "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz", + "integrity": "sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1999,10 +2082,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/graceful-fs": { "version": "4.1.9", @@ -2318,12 +2402,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3008,10 +3093,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3360,6 +3446,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -4392,12 +4479,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -4905,12 +4993,13 @@ } }, "node_modules/rollup": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", - "integrity": "sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.25.0.tgz", + "integrity": "sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -4920,19 +5009,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.5", - "@rollup/rollup-android-arm64": "4.9.5", - "@rollup/rollup-darwin-arm64": "4.9.5", - "@rollup/rollup-darwin-x64": "4.9.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.5", - "@rollup/rollup-linux-arm64-gnu": "4.9.5", - "@rollup/rollup-linux-arm64-musl": "4.9.5", - "@rollup/rollup-linux-riscv64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-musl": "4.9.5", - "@rollup/rollup-win32-arm64-msvc": "4.9.5", - "@rollup/rollup-win32-ia32-msvc": "4.9.5", - "@rollup/rollup-win32-x64-msvc": "4.9.5", + "@rollup/rollup-android-arm-eabi": "4.25.0", + "@rollup/rollup-android-arm64": "4.25.0", + "@rollup/rollup-darwin-arm64": "4.25.0", + "@rollup/rollup-darwin-x64": "4.25.0", + "@rollup/rollup-freebsd-arm64": "4.25.0", + "@rollup/rollup-freebsd-x64": "4.25.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.25.0", + "@rollup/rollup-linux-arm-musleabihf": "4.25.0", + "@rollup/rollup-linux-arm64-gnu": "4.25.0", + "@rollup/rollup-linux-arm64-musl": "4.25.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.25.0", + "@rollup/rollup-linux-riscv64-gnu": "4.25.0", + "@rollup/rollup-linux-s390x-gnu": "4.25.0", + "@rollup/rollup-linux-x64-gnu": "4.25.0", + "@rollup/rollup-linux-x64-musl": "4.25.0", + "@rollup/rollup-win32-arm64-msvc": "4.25.0", + "@rollup/rollup-win32-ia32-msvc": "4.25.0", + "@rollup/rollup-win32-x64-msvc": "4.25.0", "fsevents": "~2.3.2" } }, @@ -5362,6 +5456,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -7112,93 +7207,128 @@ "optional": true }, "@rollup/rollup-android-arm-eabi": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz", - "integrity": "sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz", + "integrity": "sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==", "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz", - "integrity": "sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz", + "integrity": "sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==", "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz", - "integrity": "sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz", + "integrity": "sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==", "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz", - "integrity": "sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz", + "integrity": "sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz", + "integrity": "sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz", + "integrity": "sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz", - "integrity": "sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz", + "integrity": "sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz", + "integrity": "sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz", - "integrity": "sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz", + "integrity": "sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz", - "integrity": "sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz", + "integrity": "sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz", + "integrity": "sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz", - "integrity": "sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz", + "integrity": "sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz", + "integrity": "sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", - "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz", + "integrity": "sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz", - "integrity": "sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz", + "integrity": "sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==", "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz", - "integrity": "sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz", + "integrity": "sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==", "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz", - "integrity": "sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz", + "integrity": "sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz", - "integrity": "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz", + "integrity": "sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==", "dev": true, "optional": true }, @@ -7292,9 +7422,9 @@ } }, "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "@types/graceful-fs": { @@ -7553,12 +7683,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -8037,9 +8167,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -9075,12 +9205,12 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -9416,25 +9546,30 @@ "dev": true }, "rollup": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", - "integrity": "sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.9.5", - "@rollup/rollup-android-arm64": "4.9.5", - "@rollup/rollup-darwin-arm64": "4.9.5", - "@rollup/rollup-darwin-x64": "4.9.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.5", - "@rollup/rollup-linux-arm64-gnu": "4.9.5", - "@rollup/rollup-linux-arm64-musl": "4.9.5", - "@rollup/rollup-linux-riscv64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-musl": "4.9.5", - "@rollup/rollup-win32-arm64-msvc": "4.9.5", - "@rollup/rollup-win32-ia32-msvc": "4.9.5", - "@rollup/rollup-win32-x64-msvc": "4.9.5", - "@types/estree": "1.0.5", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.25.0.tgz", + "integrity": "sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.25.0", + "@rollup/rollup-android-arm64": "4.25.0", + "@rollup/rollup-darwin-arm64": "4.25.0", + "@rollup/rollup-darwin-x64": "4.25.0", + "@rollup/rollup-freebsd-arm64": "4.25.0", + "@rollup/rollup-freebsd-x64": "4.25.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.25.0", + "@rollup/rollup-linux-arm-musleabihf": "4.25.0", + "@rollup/rollup-linux-arm64-gnu": "4.25.0", + "@rollup/rollup-linux-arm64-musl": "4.25.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.25.0", + "@rollup/rollup-linux-riscv64-gnu": "4.25.0", + "@rollup/rollup-linux-s390x-gnu": "4.25.0", + "@rollup/rollup-linux-x64-gnu": "4.25.0", + "@rollup/rollup-linux-x64-musl": "4.25.0", + "@rollup/rollup-win32-arm64-msvc": "4.25.0", + "@rollup/rollup-win32-ia32-msvc": "4.25.0", + "@rollup/rollup-win32-x64-msvc": "4.25.0", + "@types/estree": "1.0.6", "fsevents": "~2.3.2" } }, diff --git a/package.json b/package.json index 61d6be3..0ddbdf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wrapblox", - "version": "0.5.8", + "version": "0.6.0", "description": "A wrapper for Roblox's API", "main": "./dist/index.js", "module": "./dist/index.js", @@ -12,8 +12,20 @@ "scripts": { "run": "node .", "build": "tsup", + "lint": "biome lint .", - "test": "jest" + "lint:fix": "biome lint --write .", + + "format": "biome format .", + "format:fix": "biome format --write .", + + "organize-imports": "biome check --formatter-enabled=false --linter-enabled=false --organize-imports-enabled=true .", + "organize-imports:fix": "biome check --formatter-enabled=false --linter-enabled=false --organize-imports-enabled=true --write .", + + "check": "biome check .", + "check:fix": "biome check --write .", + + "test": "jest --testPathPattern=tests --testPathIgnorePatterns=/dist/" }, "keywords": [ "roblox" diff --git a/src/Classes/AuthedUser.ts b/src/Classes/AuthedUser.ts index e9ccbec..8fa20bc 100644 --- a/src/Classes/AuthedUser.ts +++ b/src/Classes/AuthedUser.ts @@ -1,39 +1,50 @@ -import WrapBlox from "../index.js"; -import { BirthData, RawFriendData, RawFriendRequest, RawUserData } from "../Types/UserTypes.js"; -import FriendRequest from "./FriendRequest.js"; +import type WrapBlox from "../index.js"; +import type { BirthData, RawUserData } from "../Types/UserTypes.js"; import User from "./User.js"; -class AuthedUser extends User { - cookie : string; - - constructor(client : WrapBlox, data : RawUserData, cookie : string) { +export default class AuthedUser extends User { + private cookie: string; + + constructor(client: WrapBlox, data: RawUserData, cookie: string) { super(client, data); this.cookie = cookie; } - - async fetchBirthdate() { - return await this.client.fetchHandler.fetch('GET', 'Users', `/users/${this.id}/birthdate`, {cookie: this.cookie}); - } - - async setBirthdate(date : BirthData & {password : string}) : Promise { - return await this.client.fetchHandler.fetch('POST', 'Users', `/users/${this.id}/birthdate`, {cookie : this.cookie, body: date}); + + async fetchBirthdate() { + return await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Users", + `/users/${this.id}/birthdate`, + { cookie: this.cookie }, + ); } - - async setDescription(description : string) : Promise { - return await this.client.fetchHandler.fetch('POST', 'Users', `/users/${this.id}/description`, {cookie: this.cookie, body: {description}}); + + async setBirthdate(date: BirthData & { password: string }): Promise { + return await this.client.fetchHandler.fetchLegacyAPI( + "POST", + "Users", + `/users/${this.id}/birthdate`, + { cookie: this.cookie, body: date }, + ); } - - async fetchCountryCode() : Promise { - return (await this.client.fetchHandler.fetch('GET', 'Users', `/users/${this.id}/country`, {cookie: this.cookie})).countryCode; + + async setDescription(description: string): Promise { + return await this.client.fetchHandler.fetchLegacyAPI( + "POST", + "Users", + `/users/${this.id}/description`, + { cookie: this.cookie, body: { description } }, + ); } - - async fetchFriendRequests() { - const ret = await this.client.fetchHandler.fetchAll('GET', 'Friends', "/my/friend-requests", {cookie: this.cookie}); - - return ret.map((friend : RawFriendRequest) => { - return new FriendRequest(this.client, friend, this); - }); + + async fetchCountryCode(): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Users", + `/users/${this.id}/country`, + { cookie: this.cookie }, + ) + ).countryCode; } } - -export default AuthedUser; \ No newline at end of file diff --git a/src/Classes/Badge.ts b/src/Classes/Badge.ts new file mode 100644 index 0000000..2fbe010 --- /dev/null +++ b/src/Classes/Badge.ts @@ -0,0 +1,131 @@ +import type WrapBlox from "../index.js"; +import { + type BadgeServiceMetadata, + type RawBadgeData, + BadgeImageFormat, + BadgeImageSize, +} from "../index.js"; + +export default class Badge { + readonly client: WrapBlox; + readonly rawData: RawBadgeData; + + readonly id: number; + readonly name: string; + readonly displayName: string; + + readonly description: string; + readonly displayDescription: string; + + readonly iconImageId: number; + readonly displayIconImageId: number; + + readonly created: Date; + readonly updated: Date; + + readonly statistics: { + readonly pastDayAwardedCount: number; + readonly awardedCount: number; + readonly winRatePercentage: number; + }; + + readonly awardingUniverse: { + readonly id: number; + readonly name: string; + readonly rootPlaceId: number; + }; + + readonly enabled: boolean; + + constructor(client: WrapBlox, rawData: RawBadgeData) { + this.client = client; + this.rawData = rawData; + + this.id = rawData.id; + this.name = rawData.name; + this.displayName = rawData.displayName; + + this.description = rawData.description; + this.displayDescription = rawData.displayDescription; + + this.iconImageId = rawData.iconImageId; + this.displayIconImageId = rawData.displayIconImageId; + + this.created = new Date(rawData.created); + this.updated = new Date(rawData.updated); + + this.statistics = rawData.statistics; + this.awardingUniverse = rawData.awardingUniverse; + + this.enabled = rawData.enabled; + } + + /* + Methods related to the Badges API + Docs: https://badges.roblox.com/docs/index.html + */ + + /** + * Fetches the service metadata for badges. + * + * @param useCache - A boolean indicating whether to use cached data. Defaults to true. + * @returns A promise that resolves to the badge service metadata. + */ + async fetchServiceMetadata(useCache = true): Promise { + return await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Badges", + "/metadata", + { useCache: useCache }, + ); + } + + /* + Methods related to the Thumbnails API + Docs: https://thumbnails.roblox.com/docs/index.html + */ + + /** + * Fetches the icon for the badge. + * + * @param {BadgeImageSize} [size=BadgeImageSize["150x150"]] - The size of the badge image. + * @param {BadgeImageFormat} [format="Png"] - The format of the badge image. + * @param {boolean} [isCircular=false] - Whether the badge image should be circular. + * @param {boolean} [useCache=true] - Whether to use the cache for the request. + * @returns {Promise} - A promise that resolves to the URL of the badge image. + */ + async fetchIcon( + size: BadgeImageSize = BadgeImageSize["150x150"], + format: BadgeImageFormat = "Png", + isCircular = false, + useCache = true, + ): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Thumbnails", + "/badges/icons", + { + useCache: useCache, + params: { + badgeIds: [this.id], + size: size, + format: format, + isCircular: isCircular, + }, + }, + ) + ).data[0].imageUrl; + } + + // Miscellaneous + + /** + * Converts the Badge instance to a string representation. + * + * @returns {string} A string in the format `${this.name}:${this.id}`. + */ + toString(): string { + return `${this.name}:${this.id}`; + } +} diff --git a/src/Classes/Friend.ts b/src/Classes/Friend.ts index 7a4701e..6d4e8f8 100644 --- a/src/Classes/Friend.ts +++ b/src/Classes/Friend.ts @@ -1,37 +1,26 @@ -import WrapBlox, { RawFriendData } from "../index.js"; +import type WrapBlox from "../index.js"; +import type { RawFriendData } from "../index.js"; import AuthedUser from "./AuthedUser.js"; import User from "./User.js"; - -class Friend extends User { +export default class Friend extends User { friendFrequentScore: number; isDeleted: boolean; isOnline: boolean; /** * The other user in the friendship */ - friend : User | AuthedUser; - - - constructor(client : WrapBlox, rawdata : RawFriendData, friend : User | AuthedUser) { + friend: User | AuthedUser; + + constructor( + client: WrapBlox, + rawdata: RawFriendData, + friend: User | AuthedUser, + ) { super(client, rawdata); this.friendFrequentScore = rawdata.friendFrequentScore; this.isOnline = rawdata.isOnline; this.isDeleted = rawdata.isDeleted; this.friend = friend; - - - } - - /** - * Removes friendship - * @throws {Error} You can only unfriend a user if you are authenticated - * @returns {Promise} - */ - async unfriend(): Promise { - if (!(this.friend instanceof AuthedUser)) throw new Error("You can only unfriend a user if you are authenticated"); - await this.client.fetchHandler.fetch("POST", "Friends", `/users/${this.id}/unfriend`, {cookie: this.friend.cookie}); } } - -export default Friend; \ No newline at end of file diff --git a/src/Classes/FriendRequest.ts b/src/Classes/FriendRequest.ts deleted file mode 100644 index 635ae51..0000000 --- a/src/Classes/FriendRequest.ts +++ /dev/null @@ -1,34 +0,0 @@ -import WrapBlox, { RawFriendRequest } from "../index.js"; -import AuthedUser from "./AuthedUser.js"; - -class FriendRequest { - client : WrapBlox; - rawdata : RawFriendRequest; - senderId : number; - created : Date; - target : AuthedUser; - constructor(client : WrapBlox, rawdata : RawFriendRequest, target : AuthedUser) { - this.client = client; - this.rawdata = rawdata; - this.senderId = rawdata.friendRequest.senderId; - this.created = new Date(rawdata.friendRequest.sentAt); - this.target = target; - } - - async fetchUser() { - return await this.client.fetchUser(this.senderId); - } - - async accept() : Promise { - return await this.client.fetchHandler.fetch('POST', 'Friends', `/users/${this.senderId}/accept-friend-request`, {cookie : this.target.cookie}); - } - - async decline() : Promise { - return await this.client.fetchHandler.fetch('POST', 'Friends', `/users/${this.senderId}/decline-friend-request`, {cookie : this.target.cookie}); - } - - - -} - -export default FriendRequest; \ No newline at end of file diff --git a/src/Classes/Game.ts b/src/Classes/Game.ts deleted file mode 100644 index 34451c1..0000000 --- a/src/Classes/Game.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { APICreator, APIGameData } from "../Types/GameTypes.js"; -import WrapBlox from "../index.js"; - -class Game { - rawdata : APIGameData; - name : string; - id : number; - description : string; - creator : APICreator; - client : WrapBlox; - playing : number; - visits : number; - maxPlayers : number; - created : Date; - updated : Date; - constructor(client : WrapBlox, rawdata : APIGameData) { - this.rawdata = rawdata; - this.name = rawdata.name; - this.id = rawdata.id; - this.description = rawdata.description; - this.creator = rawdata.creator; - this.client = client; - this.playing = rawdata.playing; - this.visits = rawdata.visits; - this.maxPlayers = rawdata.maxPlayers; - - this.created = new Date(rawdata.created); - this.updated = new Date(rawdata.updated); - } - - async fetchCreator() { - - return await this.client.fetchUser(this.creator.id); - } - - async favorite() { - return await this.client.fetchHandler.fetch('POST', 'Games', `/games/${this.id}/favorites`); - } - - async fetchFavoriteCount() { - return (await this.client.fetchHandler.fetch('GET', 'Games', `/games/${this.id}/favorites/count`)).count; - } - -} - -export default Game; \ No newline at end of file diff --git a/src/Classes/Group.ts b/src/Classes/Group.ts index dc63014..e035719 100644 --- a/src/Classes/Group.ts +++ b/src/Classes/Group.ts @@ -1,171 +1,171 @@ -import { APIGroupSettings, APIRoles, RawGroupData, RawMemberData, RawShout } from "../Types/GroupTypes.js"; -import WrapBlox, { SortOrder } from "../index.js"; -import Member from "./Member.js"; -import Role from "./Role.js"; - -class Group { - rawdata: RawGroupData; - - name: string; - description: string; - id: number; - client: WrapBlox; - - - - constructor(client: WrapBlox, rawdata: RawGroupData) { +import type WrapBlox from "../index.js"; +import { + type GroupActionType, + type GroupAuditLog, + type RawGroupData, + GroupOwnerType, + SortOrder, +} from "../index.js"; +import Universe from "./Universe.js"; +import User from "./User.js"; + +export default class Group { + readonly client: WrapBlox; + readonly rawData: RawGroupData; + + readonly id: number; + readonly name: string; + readonly description: string; + readonly owner: { + readonly id: number; + readonly type: GroupOwnerType; + readonly name: string; + }; + + readonly memberCount: number; + readonly created: Date; + readonly hasVerifiedBadge: boolean; + + constructor(client: WrapBlox, rawData: RawGroupData) { this.client = client; - this.rawdata = rawdata; - this.name = rawdata.name; - this.description = rawdata.description; - this.id = rawdata.id; - } - - async fetchOwner() { - return await this.client.fetchUser(this.rawdata.owner.userId); - } - - async fetchJoinRequests() { - return await this.client.fetchHandler.fetchAll('GET', 'Groups', `/groups/${this.id}/join-requests`); - } - - async fetchMembers(): Promise { - const ret = await this.client.fetchHandler.fetchAll('GET', 'Groups', `/groups/${this.id}/users`) - return ret.map((member: RawMemberData) => { - return new Member(this.client, this, member); - }); - } - - async fetchIcon(format: "Png" | "Webp" = "Png", size: "150x150" | "420x420" = "150x150"): Promise { - const ret = await this.client.fetchHandler.fetch('GET', 'Thumbnails', "/groups/icons", { - params: { - groupIds: [this.id], - format: format, - size: size, - } - }); - - const real = ret.data[0]; - if (!real) return undefined; - return real.imageUrl; - - - } - - async fetchSettings(): Promise { - return await this.client.fetchHandler.fetch('GET', 'Groups', `/groups/${this.id}/settings`); - } - - async fetchPayoutInfo() { - const ret = await this.client.fetchHandler.fetch('GET', 'Groups', `/groups/${this.id}/payouts`); - return ret.data; - } - - async payoutUser(userId: number, amount: number) { - return await this.client.fetchHandler.fetch('POST', 'Groups', `/groups/${this.id}/payouts`, { - body: { - PayoutType: 1, - Recipients: [{ - recipientId: userId, - recipientType: 1, - amount: amount - - }] - } - }); - } - - async fetchRawRoles(): Promise { - const ret = await this.client.fetchHandler.fetch('GET', 'Groups', `/groups/${this.id}/roles`); - return ret.roles; + this.rawData = rawData; + + this.id = rawData.id; + this.name = rawData.name; + this.description = rawData.description; + this.owner = rawData.owner; + + this.memberCount = rawData.memberCount; + this.created = new Date(rawData.created); + this.hasVerifiedBadge = rawData.hasVerifiedBadge; + } + + /* + Methods related to the Groups API + Docs: https://groups.roblox.com/docs/index.html + */ + + /** + * Fetches the audit log for the group. + * + * @param maxResults - The maximum number of results to return. Defaults to 100. + * @param sortOrder - The order in which to sort the results. Defaults to "Asc". + * @param actionType - The type of action to filter the audit log by. Optional. + * @param useCache - Whether to use cached data. Defaults to true. + * @returns A promise that resolves to an array of GroupAuditLog objects. + */ + async fetchAuditLog( + maxResults = 100, + sortOrder: SortOrder = "Asc", + actionType?: GroupActionType, + useCache = true, + ): Promise { + const returnData = [] as GroupAuditLog[]; + const rawData = await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "Groups", + `/groups/${this.id}/audit-log`, + { + useCache: useCache, + params: { sortOrder: sortOrder, actionType: actionType }, + }, + { maxResults: maxResults, perPage: 100 }, + ); + + for (const data of rawData) { + returnData.push({ + actor: { + user: data.actor.user, + role: data.actor.role, + }, + actionType: data.actionType, + description: data.description, + created: new Date(data.created), + }); + } + + return returnData; + } + + /* + Methods related to the Thumbnails API + Docs: https://thumbnails.roblox.com/docs/index.html + */ + + async fetchIcon( + format: "Png" | "Webp" = "Png", + size: "150x150" | "420x420" = "150x150", + ): Promise { + return ( + ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Thumbnails", + "/groups/icons", + { + params: { + groupIds: [this.id], + format: format, + size: size, + }, + }, + ) + ).data[0]?.imageUrl || undefined + ); + } + + /* + Methods related to the Games API + Docs: https://games.roblox.com/docs/index.html + */ + + async fetchUniverses( + maxResults = 100, + accessFilter: "All" | "Public" | "Private" = "Public", + sortOrder: SortOrder = "Asc", + useCache = true, + ): Promise { + const returnData = [] as Universe[]; + const rawData = await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "GamesV2", + `/groups/${this.id}/games`, + { + useCache: useCache, + params: { + accessFilter: accessFilter, + sortOrder: sortOrder, + }, + }, + { maxResults: maxResults, perPage: 50 }, + ); + + for (const data of rawData) { + const universe = await this.client.fetchUniverse(data.id, useCache); + returnData.push(universe); + } + + return returnData; + } + + // Miscellaneous + + /** + * Fetches the owner of the group. + * + * @returns {Promise} A promise that resolves to the User object representing the owner. + */ + async fetchOwner(): Promise { + return await this.client.fetchUser(this.owner.id); + } + + /** + * Converts the Group object to a string representation. + * The string in the format `${this.name}:${this.id}`. + * + * @returns {string} The formatted string. + */ + toString(): string { + return `${this.name}:${this.id}`; } - - async fetchRoles() { - const ret = await this.fetchRawRoles(); - return ret.map((role: APIRoles) => { - return new Role(this.client, this, role); - }); - } - - async fetchRole(roleId: number): Promise { - const roles = await this.fetchRoles(); - return roles.find((role) => role.id === roleId); - } - - async fetchRoleByName(name: string): Promise { - const roles = await this.fetchRoles(); - return roles.find((role) => role.name === name); - } - - async fetchRoleByRank(rank: number): Promise { - const roles = await this.fetchRoles(); - return roles.find((role) => role.rank === rank); - - } - - async fetchWallPosts(limit: 25 | 50 | 100 = 25, order: SortOrder = "Asc", cursor?: string) { - const ret = await this.client.fetchHandler.fetch('GET', 'Groups', `/groups/${this.id}/wall/posts`, { - params: { - limit: limit, - cursor: cursor, - sortOrder: order - } - }); - return ret.data; - } - - - async setRoleByRank(userid : number, rank : number) { - const role = await this.fetchRoleByRank(rank); - if (!role) throw new Error('Role not found'); - - return await this.client.fetchHandler.fetch('PATCH', 'Groups', `/groups/${this.id}/users/${userid}`, { - body: { - roleId: role.id, - } - }); - } - - async setRoleByName(userid : number, name : string) { - const role = await this.fetchRoleByName(name); - if (!role) throw new Error('Role not found'); - - return await this.client.fetchHandler.fetch('PATCH', 'Groups', `/groups/${this.id}/users/${userid}`, { - body: { - roleId: role.id, - } - }); - } - - async setRoleById(userid : number, roleid : number) { - return await this.client.fetchHandler.fetch('PATCH', 'Groups', `/groups/${this.id}/users/${userid}`, { - body: { - roleId: roleid, - } - }); - } - - async setShout(message : string): Promise { - return await this.client.fetchHandler.fetch('PATCH', 'Groups', `/groups/${this.id}/status`, { - body: { - message: message - } - }); - } - - - toString() { - return this.name; - } - - - - - - - - - } - -export default Group; \ No newline at end of file diff --git a/src/Classes/Internal/FetchError.ts b/src/Classes/Internal/FetchError.ts index 7f50a6d..65f8efa 100644 --- a/src/Classes/Internal/FetchError.ts +++ b/src/Classes/Internal/FetchError.ts @@ -1,9 +1,30 @@ +export type FetchedError = { + code: number; + message: string; +}; + export default class FetchError extends Error { - code : number; - response : Response; - constructor(message : string, code : number, response : Response) { + response: Response; + + constructor(message: string, response: Response) { super(message); - this.code = code; this.response = response; } -} \ No newline at end of file + + async getErrors(): Promise { + const body = await this.response.json(); + return body.errors; + } + + async format(): Promise { + const errors = await this.getErrors(); + const formattedErrors = errors + .map( + (error: { code: number; message: string }) => + `[${error.code}]: ${error.message}`, + ) + .join("\n"); + + return `[${this.response.status} ${this.response.statusText}]:\n${formattedErrors}`; + } +} diff --git a/src/Classes/Internal/cacheManager.ts b/src/Classes/Internal/cacheManager.ts index b41e28b..1b3951b 100644 --- a/src/Classes/Internal/cacheManager.ts +++ b/src/Classes/Internal/cacheManager.ts @@ -1,28 +1,31 @@ -class CacheManager { - cache = new Map(); - +export default class CacheManager { + cache = new Map< + T, + { + value: V; + time: number; + } + >(); + timeout = 1000 * 60 * 5; // How long the cache should last - - constructor(values : [T, V][] = []) { + + constructor(values: [T, V][] = []) { for (const [key, value] of values) { this.setValues(key, value); } } - + setTimeout = (time: number) => { this.timeout = time; - } - + }; + setValues = (key: T, value: V) => { this.cache.set(key, { value: value, time: Date.now(), }); - } - + }; + rawGetValues = (key: T) => { const data = this.cache.get(key); if (!data) return undefined; @@ -30,22 +33,18 @@ class CacheManager { this.cache.delete(key); return undefined; } - return data - } - + return data; + }; + getValues = (key: T) => { return this.rawGetValues(key)?.value; - } - + }; + deleteValues = (key: T) => { return this.cache.delete(key); - } - + }; + clearCache = () => { this.cache.clear(); - } - - + }; } - -export default CacheManager; \ No newline at end of file diff --git a/src/Classes/Internal/factory.ts b/src/Classes/Internal/factory.ts index a70b2ee..b6421c5 100644 --- a/src/Classes/Internal/factory.ts +++ b/src/Classes/Internal/factory.ts @@ -5,15 +5,42 @@ */ -import WrapBlox, { RawFriendData, User } from "../../index.js"; +import type WrapBlox from "../../index.js"; +import type { + RawBadgeData, + RawFriendData, + RawUniverseData, + RawUserData, +} from "../../index.js"; import AuthedUser from "../AuthedUser.js"; - +import User from "../User.js"; export default { - - async createFriend(client : WrapBlox, data : RawFriendData, friend : User | AuthedUser) { - const Friend = (await import('../Friend.js')).default; - + async createUser(client: WrapBlox, data: RawUserData) { + const User = (await import("../User.js")).default; + + return new User(client, data); + }, + + async createFriend( + client: WrapBlox, + data: RawFriendData, + friend: User | AuthedUser, + ) { + const Friend = (await import("../Friend.js")).default; + return new Friend(client, data, friend); - } -} \ No newline at end of file + }, + + async createBadge(client: WrapBlox, data: RawBadgeData) { + const Badge = (await import("../Badge.js")).default; + + return new Badge(client, data); + }, + + async createUniverse(client: WrapBlox, data: RawUniverseData) { + const Universe = (await import("../Universe.js")).default; + + return new Universe(client, data); + }, +}; diff --git a/src/Classes/Internal/fetchHandler.ts b/src/Classes/Internal/fetchHandler.ts index 0ca345d..2959391 100644 --- a/src/Classes/Internal/fetchHandler.ts +++ b/src/Classes/Internal/fetchHandler.ts @@ -1,37 +1,70 @@ -import { FetchOptions, HttpMethods, ValidUrls } from "../../Types/BaseTypes.js" +import type { FetchOptions, HttpMethods } from "../../Types/BaseTypes.js"; import CacheManager from "./cacheManager.js"; import FetchError from "./FetchError.js"; +export default class FetchHandler { + readonly cacheManager = new CacheManager(); + + private credentials: { + cookie?: string; + CSRFToken?: string; + APIKey?: string; + } = { + cookie: undefined, + CSRFToken: undefined, + APIKey: undefined, + }; -class FetchHandler { - cookie?: string; - CsrfToken?: string; - urls = { - Users: 'https://users.roblox.com/v1', - Groups: 'https://groups.roblox.com/v1', - Thumbnails: 'https://thumbnails.roblox.com/v1', + readonly LegacyAPI = { + Users: "https://users.roblox.com/v1", + Thumbnails: "https://thumbnails.roblox.com/v1", Friends: "https://friends.roblox.com/v1", - GamesV2: "https://games.roblox.com/v2", + Presence: "https://presence.roblox.com/v1", + + Groups: "https://groups.roblox.com/v1", + GroupsV2: "https://groups.roblox.com/v2", + Games: "https://games.roblox.com/v1", + GamesV2: "https://games.roblox.com/v2", + Badges: "https://badges.roblox.com/v1", + BadgesV2: "https://badges.roblox.com/v2", + Inventory: "https://inventory.roblox.com/v1", - }; + InventoryV2: "https://inventory.roblox.com/v2", + + AccountSettings: "https://accountsettings.roblox.com/v1", + PremiumFeatures: "https://premiumfeatures.roblox.com/v1", + + Auth: "https://auth.roblox.com/v1", + AuthV2: "https://auth.roblox.com/v2", + AuthV3: "https://auth.roblox.com/v3", - cacheManager = new CacheManager() + Avatar: "https://avatar.roblox.com/v1", + AvatarV2: "https://avatar.roblox.com/v2", + AvatarV3: "https://avatar.roblox.com/v3", + }; constructor(cookie?: string) { - this.cookie = cookie; + this.credentials.cookie = cookie; } + setCredential(key: keyof typeof this.credentials, value: string) { + this.credentials[key] = value; + } clearCache = () => { this.cacheManager.clearCache(); - } - - // biome-ignore lint/suspicious/noExplicitAny: - fetch = async (method: HttpMethods, url: ValidUrls, route: string, opts: FetchOptions = {}): Promise => { // params?: { [key: string | number]: unknown }, body?: { [key: string]: unknown }, usecache = true, cookie? : string) => { + }; - let RealUrl = this.urls[url] + route; + fetchLegacyAPI = async ( + method: HttpMethods, + api: keyof typeof this.LegacyAPI, + route: string, + opts: FetchOptions = {}, + // biome-ignore lint/suspicious/noExplicitAny: Convert to use Promise + ): Promise => { + let RealUrl = this.LegacyAPI[api] + route; if (opts.params) { const query = new URLSearchParams(); @@ -41,36 +74,48 @@ class FetchHandler { } if (RealUrl.includes("?")) { - RealUrl += `&${query.toString()}` - } else RealUrl += `?${query.toString()}` + RealUrl += `&${query.toString()}`; + } else RealUrl += `?${query.toString()}`; } const cached = this.cacheManager.getValues(RealUrl); - if (cached && opts.usecache) return cached; + if (cached && (opts.useCache || opts.useCache === undefined)) + return cached; const headers = new Headers(); - if (this.CsrfToken) headers.set("X-Csrf-Token", this.CsrfToken); - if (this.cookie) headers.set("Cookie", `.ROBLOSECURITY=${this.cookie}`); + if (this.credentials.CSRFToken) + headers.set("X-Csrf-Token", this.credentials.CSRFToken); + if (opts.CSRFToken) headers.set("X-Csrf-Token", opts.CSRFToken); + if (this.credentials.cookie) + headers.set("Cookie", `.ROBLOSECURITY=${this.credentials.cookie}`); if (opts.cookie) headers.set("Cookie", `.ROBLOSECURITY=${opts.cookie}`); headers.set("Content-Type", "application/json"); const response = await fetch(RealUrl, { method: method, - credentials: 'include', + credentials: "include", headers: headers, body: opts.body ? JSON.stringify(opts.body) : undefined, - }) - - if (!this.CsrfToken && response.headers.get("x-csrf-token")) { - this.CsrfToken = response.headers.get("x-csrf-token") as string; + }); + + if ( + !this.credentials.CSRFToken && + response.headers.get("x-csrf-token") + ) { + this.credentials.CSRFToken = response.headers.get( + "x-csrf-token", + ) as string; if (response.status === 403) { - return await this.fetch(method, url, route, opts); + return await this.fetchLegacyAPI(method, api, route, opts); } } if (!response.ok) { - throw new FetchError(`Failed to fetch data: ${response.status} ${response.statusText}`, response.status, response); + throw new FetchError( + `Failed to fetch data: ${response.status} ${response.statusText}`, + response, + ); } if (!response.body) return; @@ -81,21 +126,44 @@ class FetchHandler { return json; }; - fetchAll = async (method: HttpMethods, url: ValidUrls, route: string, params?: { [key: string | number]: unknown }) => { + fetchLegacyAPIList = async ( + method: HttpMethods, + api: keyof typeof this.LegacyAPI, + route: string, + opts: FetchOptions = {}, + pageOptions: { maxResults: number; perPage: 10 | 25 | 50 | 100 } = { + maxResults: 100, + perPage: 100, + }, + // biome-ignore lint/suspicious/noExplicitAny: Convert to use Promise + ): Promise => { const data = []; let cursor = ""; while (true) { try { - const response = await this.fetch(method, url, `${route}?limit=100${cursor ? `&cursor=${cursor}` : ""}`, params); - data.push(...response.data); - if (!response.nextPageCursor) { + const response = await this.fetchLegacyAPI( + method, + api, + `${route}?limit=${pageOptions.perPage}${ + cursor ? `&cursor=${cursor}` : "" + }`, + opts, + ); + if (response.data) data.push(...response.data); + + if ( + !response.nextPageCursor || + data.length >= pageOptions.maxResults + ) { break; } cursor = response.nextPageCursor; } catch (e) { if (e instanceof FetchError) { - if (e.code === 429) { - await new Promise((resolve) => setTimeout(resolve, 1000)); + if (e.response.status === 429) { + await new Promise((resolve) => + setTimeout(resolve, 1000), + ); continue; } throw e; // If it's not a rate limit error, throw it @@ -104,9 +172,4 @@ class FetchHandler { } return data; }; - - } - - -export default FetchHandler; \ No newline at end of file diff --git a/src/Classes/Member.ts b/src/Classes/Member.ts deleted file mode 100644 index 70affa7..0000000 --- a/src/Classes/Member.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { RawMemberData } from "../Types/GroupTypes.js"; -import WrapBlox from "../index.js"; -import Group from "./Group.js"; - -export default class Member { - rawdata : RawMemberData; - client : WrapBlox; - userId : number; - group : Group; - role : { - id : number; - name : string; - rank : number; - } - name : string; - displayName : string; - - - constructor(client : WrapBlox, group : Group, rawdata : RawMemberData) { - this.client = client; - this.rawdata = rawdata; - this.userId = rawdata.user.userId; - this.name = rawdata.user.username; - this.displayName = rawdata.user.displayName; - this.role = rawdata.role; - this.group = group; - }; - - async fetchUser() { - return await this.client.fetchUser(this.userId); - } - - toString() { - if (this.name === this.displayName) return this.name; - return `${this.displayName} (@${this.name})`; - } -} \ No newline at end of file diff --git a/src/Classes/Place.ts b/src/Classes/Place.ts new file mode 100644 index 0000000..5057837 --- /dev/null +++ b/src/Classes/Place.ts @@ -0,0 +1,93 @@ +import type WrapBlox from "../index.js"; +import { PlaceServer, ServerType, type RawPlaceData } from "../index.js"; +import Universe from "./Universe.js"; + +export default class Place { + readonly client: WrapBlox; + readonly rawdata: RawPlaceData; + + readonly id: number; + readonly name: string; + readonly description: string; + + readonly sourceName: string; + readonly sourceDescription: string; + + readonly url: string; + readonly builder: string; + readonly builderId: number; + readonly hasVerifiedBadge: boolean; + + readonly isPlayable: boolean; + readonly reasonProhibited: string; + + readonly universeId: number; + readonly universeRootPlaceId: number; + readonly price: number; + + readonly imageToken: string; + + constructor(client: WrapBlox, rawdata: RawPlaceData) { + this.client = client; + this.rawdata = rawdata; + + this.id = rawdata.placeId; + this.name = rawdata.name; + this.description = rawdata.description; + + this.sourceName = rawdata.sourceName; + this.sourceDescription = rawdata.sourceDescription; + + this.url = rawdata.url; + this.builder = rawdata.builder; + this.builderId = rawdata.builderId; + this.hasVerifiedBadge = rawdata.hasVerifiedBadge; + + this.isPlayable = rawdata.isPlayable; + this.reasonProhibited = rawdata.reasonProhibited; + + this.universeId = rawdata.universeId; + this.universeRootPlaceId = rawdata.universeRootPlaceId; + this.price = rawdata.price; + + this.imageToken = rawdata.imageToken; + } + + /* + Methods related to the Games API + Docs: https://games.roblox.com/docs/index.html + */ + + async fetchServers( + maxResults = 100, + serverType: ServerType = ServerType.Public, + useCache = true, + ): Promise { + return await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "Games", + `/games/${this.id}/servers/${serverType}`, + { useCache: useCache, params: {} }, + { maxResults: maxResults, perPage: 100 }, + ); + } + + /** + * Fetches the universe data associated with the current instance. + * + * @returns {Promise} A promise that resolves to the universe data. + */ + async fetchUniverse(): Promise { + return this.client.fetchUniverse(this.universeId); + } + + /** + * Returns a string representation of the Place object. + * The format of the returned string is `${this.name}:${this.id}`. + * + * @returns {string} The formatted string + */ + toString(): string { + return `${this.name}:${this.id}`; + } +} diff --git a/src/Classes/Role.ts b/src/Classes/Role.ts deleted file mode 100644 index 05dd549..0000000 --- a/src/Classes/Role.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { APIRoles } from "../Types/GroupTypes.js"; -import WrapBlox, { Group, Member } from "../index.js"; - -export default class Role { - id: number; - name: string; - memberCount: number; - rank: number; - rawdata: APIRoles; - client: WrapBlox; - group: Group; - - constructor(client: WrapBlox, group: Group, data: APIRoles) { - this.id = data.id; - this.name = data.name; - this.client = client; - this.group = group; - this.rawdata = data; - this.memberCount = data.memberCount; - this.rank = data.rank; - } - - /** - * Fetches all members in the role - * @returns {Promise} Array of members in the role - */ - async fetchMembers(): Promise { - const ret = await this.client.fetchHandler.fetchAll('GET', 'Groups', `/groups/${this.group.id}/roles/${this.id}/users`); - return ret.map((member) => { - const newData = { - user: member, - role: { - id: this.id, - name: this.name, - rank: this.rank, - } - } - - return new Member(this.client, this.group, newData); - }); - - } - - /** - * Sets the rank number of the role - * @param rank The rank number to set the role to - * @returns this - */ - async setRank(rank: number) { - const data = await this.client.fetchHandler.fetch("PATCH", 'Groups', `/groups/${this.group.id}/rolesets/${this.id}`, { - body: { - rank: rank - }, - }); - this.rank = data.rank; - return this; - } - - /** - * Sets the name of the role - * @param name The name to set the role to - * @returns this - */ - async setName(name: string) { - const data = await this.client.fetchHandler.fetch('PATCH', 'Groups', `/groups/${this.group.id}/rolesets/${this.id}`, { - body: { - name: name - } - }); - this.name = data.name; - return this; - } - - /** - * Sets the description of the role - * @param description The description to set the role to - * @returns this - */ - async setDescription(description: string) { - const data = await this.client.fetchHandler.fetch('POST', 'Groups', `/groups/${this.group.id}/rolesets/${this.id}`, { - body: { - description: description - } - }); - this.rawdata.description = data.description; - return this; - } - - /** - * Deletes the role - */ - async delete() { - await this.client.fetchHandler.fetch('DELETE', 'Groups', `/groups/${this.group.id}/rolesets/${this.id}`); - } - - toString() { - return `${this.name} (${this.rank})`; - } - - - - - -} \ No newline at end of file diff --git a/src/Classes/Universe.ts b/src/Classes/Universe.ts new file mode 100644 index 0000000..a6d0574 --- /dev/null +++ b/src/Classes/Universe.ts @@ -0,0 +1,120 @@ +import type WrapBlox from "../index.js"; +import { + type UniverseAvatarType, + type RawUniverseData, + type UniverseCreatorType, +} from "../index.js"; +import Place from "./Place.js"; + +export default class Universe { + readonly client: WrapBlox; + readonly rawData: RawUniverseData; + + readonly id: number; + readonly rootPlaceId: number; + + readonly name: string; + readonly description: string; + + readonly sourceName: string; + readonly sourceDescription: string; + + readonly creator: { + readonly id: number; + readonly name: string; + readonly type: UniverseCreatorType; + readonly isRNVAccount: boolean; + readonly hasVerifiedBadge: boolean; + }; + + readonly price: number; + readonly allowedGearGenres: string[]; + readonly allowedGearCategories: string[]; + + readonly isGenreEnforced: boolean; + readonly copyingAllowed: boolean; + + readonly playing: number; + readonly visits: number; + readonly maxPlayers: number; + + readonly created: Date; + readonly updated: Date; + + readonly studioAccessToApisAllowed: boolean; + readonly createVipServersAllowed: boolean; + readonly universeAvatarType: UniverseAvatarType; + + readonly genre: string; + readonly genre_l1: string; + readonly genre_l2: string; + readonly isAllGenre: boolean; + + readonly isFavoritedByUser: boolean; + readonly favoritedCount: number; + readonly licenseDescription: string; + + constructor(client: WrapBlox, rawData: RawUniverseData) { + this.client = client; + this.rawData = rawData; + + this.id = rawData.id; + this.rootPlaceId = rawData.rootPlaceId; + + this.name = rawData.name; + this.description = rawData.description || ""; + + this.sourceName = rawData.sourceName; + this.sourceDescription = rawData.sourceDescription; + + this.creator = rawData.creator; + + this.price = rawData.price || 0; + this.allowedGearGenres = rawData.allowedGearGenres; + this.allowedGearCategories = rawData.allowedGearCategories; + + this.isGenreEnforced = rawData.isGenreEnforced; + this.copyingAllowed = rawData.copyingAllowed; + + this.playing = rawData.playing; + this.visits = rawData.visits; + this.maxPlayers = rawData.maxPlayers; + + this.created = new Date(rawData.created); + this.updated = new Date(rawData.updated); + + this.studioAccessToApisAllowed = rawData.studioAccessToApisAllowed; + this.createVipServersAllowed = rawData.createVipServersAllowed; + this.universeAvatarType = rawData.universeAvatarType; + + this.genre = rawData.genre; + this.genre_l1 = rawData.genre_l1; + this.genre_l2 = rawData.genre_l2; + this.isAllGenre = rawData.isAllGenre; + + this.isFavoritedByUser = rawData.isFavoritedByUser; + this.favoritedCount = rawData.favoritedCount; + this.licenseDescription = rawData.licenseDescription; + } + + // Miscellanous + + /** + * Fetches the root place associated with the universe. + * + * @returns {Promise} A promise that resolves to the root place. + */ + async fetchRootPlace(): Promise { + return await this.client.fetchPlace(this.rootPlaceId); + } + + /** + * Returns a string representation of the Universe instance. + * The format of the returned string is `${this.name}:${this.id}`. + * + * @returns {string} The formatted string + */ + toString(): string { + return `${this.name}:${this.id}`; + } +} diff --git a/src/Classes/User.ts b/src/Classes/User.ts index 7ece540..d501f29 100644 --- a/src/Classes/User.ts +++ b/src/Classes/User.ts @@ -1,150 +1,887 @@ -import { APIUserGroup, AvatarImageTypes, RawFriendData, RawUserData } from "../Types/UserTypes.js"; -import WrapBlox, { AwardedBadge, OwnedItem } from "../index.js"; -// import type Friend from "./Friend.js"; -import UserRoleManager from "./UserRoleManager.js"; - -import Factory from "./Internal/factory.js"; import Friend from "./Friend.js"; -import { AvatarSize, ItemTypes } from "../Types/Enums.js"; +import factory from "./Internal/factory.js"; + +import type WrapBlox from "../index.js"; +import { + type AvatarImageFormat, + type RawUserGroupRoles, + type RawUserData, + type AvatarV1, + type AvatarV2, + type Avatar3D, + type FriendMetadata, + type AvatarBustImageFormat, + type UserPresence, + type OwnedAsset, + type GroupRole, + type SortOrder, + AvatarBustImageSize, + AvatarImageSize, + ItemTypes, +} from "../index.js"; +import Group from "./Group.js"; +import Badge from "./Badge.js"; +import Universe from "./Universe.js"; + +export default class User { + readonly client: WrapBlox; + readonly rawData: RawUserData; + + readonly id: number; + readonly name: string; + readonly displayName: string; + readonly description: string; + readonly hasVerifiedBadge: boolean; + readonly externalAppDisplayName?: string; + readonly isBanned: boolean; + readonly joinDate: Date; + + readonly accountAge: number; + + constructor(client: WrapBlox, rawData: RawUserData) { + this.client = client; + this.rawData = rawData; + + this.id = rawData.id; + this.name = rawData.name; + this.displayName = rawData.displayName; + this.description = rawData.description; + this.hasVerifiedBadge = rawData.hasVerifiedBadge; + this.externalAppDisplayName = rawData.externalAppDisplayName; + this.isBanned = rawData.isBanned; + this.joinDate = new Date(rawData.created); + + this.accountAge = Math.ceil( + Math.abs(new Date().getTime() - this.joinDate.getTime()) / + (1000 * 60 * 60 * 24), + ); + } + /* + Methods related to the Users API + Docs: https://users.roblox.com/docs/index.html + */ + /** + * Fetches the username history of the user. + * + * @param {number} [maxResults=100] - The maximum number of results to return. + * @param {boolean} [useCache=true] - Whether to use cached data if available. + * @returns {Promise} A promise that resolves to an array of usernames. + */ + async fetchUsernameHistory( + maxResults = 100, + useCache = true, + ): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "Users", + `/users/${this.id}/username-history`, + { useCache: useCache }, + { maxResults: maxResults, perPage: 100 }, + ) + ).map((name: { name: string }) => name.name); + } -class User { - rawData: RawUserData; - id: number; - name: string; - displayName: string; - description: string; - client: WrapBlox; + /* + Methods related to the Presence API + Docs: https://presence.roblox.com/docs/index.html + */ + /** + * Fetches the last online date of the user. + * + * @param {boolean} [useCache=true] - Determines whether to use cached data or not. + * @returns {Promise} A promise that resolves to the last online date of the user. + */ + async fetchLastOnlineDate(useCache = true): Promise { + return new Date( + ( + await this.client.fetchHandler.fetchLegacyAPI( + "POST", + "Presence", + "/presence/last-online", + { + useCache: useCache, + body: { + userIds: [this.id], + }, + }, + ) + ).lastOnlineTimestamps[0].lastOnline, + ); + } - constructor(client: WrapBlox, rawdata: RawUserData) { - this.rawData = rawdata; - this.id = rawdata.id; - this.name = rawdata.name; - this.displayName = rawdata.displayName; - this.description = rawdata.description; - this.client = client; - - + /** + * Fetches the presence status of the user. + * + * @param useCache - A boolean indicating whether to use the cache. Defaults to true. + * @returns A promise that resolves to the user's presence status. + */ + async fetchPresence(useCache = true): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "POST", + "Presence", + "/presence/users", + { + useCache: useCache, + body: { + userIds: [this.id], + }, + }, + ) + ).userPresences[0]; } - async fetchFriends(): Promise { - const ret = await this.client.fetchHandler.fetch('GET', 'Friends', `/users/${this.id}/friends`); + /* + Methods related to the Games API + Docs: https://games.roblox.com/docs/index.html + */ - return ret.data.map((friend: RawFriendData) => { - Factory.createFriend(this.client, friend, this); - }); - } + /** + * Fetches the universes created by the user. + * + * @param maxResults - The maximum number of results to return. Defaults to 100. + * @param accessFilter - The access filter for the universes. Can be "All", "Public", or "Private". Defaults to "All". + * @param sortOrder - The order in which to sort the results. Can be "Asc" for ascending or "Desc" for descending. Defaults to "Asc". + * @param useCache - Whether to use cached data. Defaults to true. + * @returns A promise that resolves to an array of Universe objects. + */ + async fetchCreatedUniverses( + maxResults = 50, + accessFilter: "All" | "Public" | "Private" = "All", + sortOrder: SortOrder = "Asc", + useCache = true, + ): Promise { + const returnData = [] as Universe[]; + const rawData = await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "GamesV2", + `/users/${this.id}/games`, + { + useCache: useCache, + params: { + //accessFilter: accessFilter, + // Returns a 501 [Not Implemented] + sortOrder: sortOrder, + }, + }, + { maxResults: maxResults, perPage: 50 }, + ); + + for (const data of rawData) { + const universe = await this.client.fetchUniverse(data.id, useCache); + returnData.push(universe); + } - async fetchFriendCount(): Promise { - return (await this.client.fetchHandler.fetch('GET', 'Friends', `/users/${this.id}/friends/count`)).count; + return returnData; } - async fetchNameHistory(): Promise { - return (await this.client.fetchHandler.fetchAll('GET', 'Users', `/users/${this.id}/name-history`)).map((name: { name: string }) => name.name); + /* + Methods related to the Groups API + Docs: https://groups.roblox.com/docs/index.html + */ + + /** + * Fetches the raw group roles for the user. + * + * @param includelocked - Whether to include locked roles in the response. Defaults to `false`. + * @param includeNotificationPreferences - Whether to include notification preferences in the response. Defaults to `false`. + * @param useCache - Whether to use cached data if available. Defaults to `true`. + * @returns A promise that resolves to an array of `RawUserGroupRoles`. + */ + private async fetchRawGroupRoles( + includelocked = false, + includeNotificationPreferences = false, + useCache = true, + ): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "GroupsV2", + `/users/${this.id}/groups/roles`, + { + useCache: useCache, + params: { + includeLocked: includelocked, + includeNotificationPreferences: + includeNotificationPreferences, + }, + }, + ) + ).data; } + /** + * Checks if the user is in a specified group. + * + * @param groupId - The ID of the group to check. + * @param includelocked - Optional. Whether to include locked groups in the check. Defaults to `false`. + * @param includeNotificationPreferences - Optional. Whether to include notification preferences in the check. Defaults to `false`. + * @param useCache - Optional. Whether to use cached data for the check. Defaults to `true`. + * @returns A promise that resolves to `true` if the user is in the specified group, otherwise `false`. + */ + async inGroup( + groupId: number, + includelocked = false, + includeNotificationPreferences = false, + useCache = true, + ): Promise { + return ( + await this.fetchRawGroupRoles( + includelocked, + includeNotificationPreferences, + useCache, + ) + ).some((entry) => entry.group.id === groupId); + } - async fetchRawRoles(includelocked = false, includeNotificationPreferences = false): Promise { - const ret = await this.client.fetchHandler.fetch('GET', 'Groups', `/users/${this.id}/groups/roles`, { - params: { - includeLocked: includelocked, - includeNotificationPreferences: includeNotificationPreferences, - } - }); + /** + * Retrieves the role of the user in a specified group. + * + * @param groupId - The ID of the group to retrieve the role for. + * @param includelocked - Optional. Whether to include locked roles. Defaults to `false`. + * @param includeNotificationPreferences - Optional. Whether to include notification preferences. Defaults to `false`. + * @param useCache - Optional. Whether to use cached data. Defaults to `true`. + * @returns A promise that resolves to the user's role in the specified group, or `undefined` if not found. + */ + async getRoleInGroup( + groupId: number, + includelocked = false, + includeNotificationPreferences = false, + useCache = true, + ): Promise { + return ( + await this.fetchRawGroupRoles( + includelocked, + includeNotificationPreferences, + useCache, + ) + ).find((entry) => entry.group.id === groupId)?.role; + } - return ret.data; + /** + * Fetches the primary group of the user. + * + * @param useCache - A boolean indicating whether to use the cache. Defaults to true. + * @returns A promise that resolves to the primary group of the user, or undefined if no primary group is found. + */ + async fetchPrimaryGroup(useCache = true): Promise { + const groupId = ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Groups", + `/users/${this.id}/groups/primary/role`, + ) + )?.group.id; + if (!groupId) return undefined; + + return await this.client.fetchGroup(groupId, useCache); + } + /** + * Fetches the groups that the user in. + * + * @param includelocked - Whether to include locked groups in the fetch. + * @param includeNotificationPreferences - Whether to include notification preferences in the fetch. + * @param useCache - Whether to use cached data if available. + * @returns A promise that resolves to an array of Group objects. + */ + async fetchGroups( + includelocked = false, + includeNotificationPreferences = false, + useCache = true, + ): Promise { + const returnData = [] as Group[]; + const rawData = await this.fetchRawGroupRoles( + includelocked, + includeNotificationPreferences, + useCache, + ); + + for (const data of rawData) + returnData.push( + await this.client.fetchGroup(data.group.id, useCache), + ); + + return returnData; } + /* + Methods related to the Badges API + Docs: https://badges.roblox.com/docs/index.html + */ - async fetchUserAvatarThumbnailUrl(size: AvatarSize = AvatarSize["150x150"], format: AvatarImageTypes = "Png", isCircular = false): Promise { - const ret = await this.client.fetchHandler.fetch('GET', "Thumbnails", "/users/avatar", { - params: { - userIds: [this.id], - size: size, - format: format, - isCircular: isCircular, - } - }); - return ret.data[0].imageUrl; + /** + * Fetches the badges for the user. + * + * @param {number} [maxResults=100] - The maximum number of results to return. + * @param {SortOrder} [sortOrder="Asc"] - The order in which to sort the results. Can be "Asc" or "Desc". + * @param {boolean} [useCache=true] - Whether to use cached data or not. + * @returns {Promise} A promise that resolves to an array of Badge objects. + */ + async fetchBadges( + maxResults = 100, + sortOrder: SortOrder = "Asc", + useCache = true, + ): Promise { + const returnData = [] as Badge[]; + const rawData = await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "Badges", + `/users/${this.id}/badges`, + { useCache: useCache, params: { sortOrder: sortOrder } }, + { maxResults: maxResults, perPage: 100 }, + ); + + for (const data of rawData) + returnData.push(await factory.createBadge(this.client, data)); + + return returnData; } - - async fetchUserHeadshotUrl(size: AvatarSize = AvatarSize["150x150"], format: AvatarImageTypes = "Png", isCircular = false): Promise { - const ret = await this.client.fetchHandler.fetch('GET', "Thumbnails", "/users/avatar-headshot", { - params: { - userIds: [this.id], - size: size, - format: format, - isCircular: isCircular, - } - }); - return ret.data[0].imageUrl; - } - - async fetchRoles(includelocked = false, includeNotificationPreferences = false): Promise { - return new UserRoleManager(this.client, await this.fetchRawRoles(includelocked, includeNotificationPreferences)); - + /** + * Fetches the award date of a specific badge for the user. + * + * @param badgeId - The ID of the badge to fetch the award date for. + * @param useCache - Optional parameter to determine whether to use cached data. Defaults to true. + * @returns A promise that resolves to the award date of the badge as a Date object, or undefined if the date is not found. + */ + async fetchBadgeAwardDate( + badgeId: number, + useCache = true, + ): Promise { + const rawDate = ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Badges", + `/users/${this.id}/badges/${badgeId}/awarded-date`, + { useCache: useCache }, + ) + )?.awardedDate; + if (!rawDate) return undefined; + + return new Date(rawDate); } + /* + Methods related to the Inventory API + Docs: https://inventory.roblox.com/docs/index.html + */ - async inGroup(groupId: number): Promise { - return (await this.fetchRawRoles()).some((group) => group.group.id === groupId); + /** + * Determines if the user can view the inventory. + * + * @param useCache - Optional boolean to specify whether to use cached data. Defaults to true. + * @returns A promise that resolves to a boolean indicating if the user can view the inventory. + */ + async canViewInventory(useCache = true): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Inventory", + `/users/${this.id}/can-view-inventory`, + { useCache: useCache }, + ) + ).canView; } - async getOwnedAsset(type: ItemTypes, id: number): Promise { + /** + * Retrieves an owned asset for the user. + * + * @param type - The type of the item to retrieve. + * @param id - The ID of the item to retrieve. + * @param useCache - Optional. Whether to use the cache for the request. Defaults to true. + * @returns A promise that resolves to the owned item if found, or undefined if not found or an error occurs. + */ + async getOwnedAsset( + type: ItemTypes, + id: number, + useCache = true, + ): Promise { try { - return await this.client.fetchHandler.fetch('GET', 'Inventory', `/users/${this.id}/items/${type}/${id}`); + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Inventory", + `/users/${this.id}/items/${type}/${id}`, + { useCache: useCache }, + ) + ).data[0]; } catch { return undefined; } - } - async ownsAsset(type: ItemTypes, assetId: number): Promise { + /** + * Checks if the user owns a specific asset. + * + * @param type - The type of the item. + * @param assetId - The ID of the asset. + * @param useCache - Optional. Whether to use cache for the request. Defaults to true. + * @returns A promise that resolves to a boolean indicating whether the user owns the asset. + */ + async ownsAsset( + type: ItemTypes, + assetId: number, + useCache = true, + ): Promise { try { - return await this.client.fetchHandler.fetch('GET', 'Inventory', `/users/${this.id}/items/${type}/${assetId}/is-owned`); + return await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Inventory", + `/users/${this.id}/items/${type}/${assetId}/is-owned`, + { useCache: useCache }, + ); } catch { return false; } } + /** + * Checks if the user owns a specific badge. + * + * @param badgeId - The ID of the badge to check. + * @param useCache - Optional. Whether to use cached data. Defaults to true. + * @returns A promise that resolves to a boolean indicating whether the user owns the badge. + */ + async ownsBadge(badgeId: number, useCache = true): Promise { + return await this.ownsAsset(ItemTypes.Badge, badgeId, useCache); + } + + /** + * Checks if the user owns a specific gamepass. + * + * @param gamepassId - The ID of the gamepass to check. + * @param useCache - Optional parameter to determine whether to use cached data. Defaults to true. + * @returns A promise that resolves to a boolean indicating whether the user owns the gamepass. + */ + async ownsGamepass(gamepassId: number, useCache = true): Promise { + return await this.ownsAsset(ItemTypes.GamePass, gamepassId, useCache); + } - async ownsBadge(badgeId: number): Promise { - return await this.ownsAsset(ItemTypes.Badge, badgeId); + /** + * Checks if the user owns a specific bundle. + * + * @param bundleId - The ID of the bundle to check. + * @param useCache - Optional. Whether to use cached data. Defaults to true. + * @returns A promise that resolves to a boolean indicating whether the user owns the bundle. + */ + async ownsBundle(bundleId: number, useCache = true): Promise { + return await this.ownsAsset(ItemTypes.Bundle, bundleId, useCache); } - async ownsGamePass(gamePassId: number): Promise { - return await this.ownsAsset(ItemTypes.GamePass, gamePassId); + /* + Methods related to the Avatar API + Docs: https://avatar.roblox.com/docs/index.html + */ + + /** + * Fetches the avatar for the user using the V1 API. + * + * @param {boolean} [useCache=true] - Determines whether to use the cache for the request. + * @returns {Promise} A promise that resolves to the user's avatar. + */ + async fetchAvatarV1(useCache = true): Promise { + return await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Avatar", + `/users/${this.id}/avatar`, + { useCache: useCache }, + ); } + /** + * Fetches the user's avatar using the AvatarV2 endpoint. + * + * @param {boolean} [useCache=true] - Determines whether to use the cache for the request. + * @returns {Promise} A promise that resolves to the user's AvatarV2 data. + */ + async fetchAvatarV2(useCache = true): Promise { + return await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "AvatarV2", + `/avatar/users/${this.id}/avatar`, + { useCache: useCache }, + ); + } - async getBadgeAwardedDate(badgeId: number): Promise { - const response: AwardedBadge | undefined = await this.client.fetchHandler.fetch('GET', 'Badges', `/users/${this.id}/badges/${badgeId}/awarded-date`) - if (response) { - return new Date(response.awardedDate); + /* + Methods related to the Thumbnails API + Docs: https://thumbnails.roblox.com/docs/index.html + */ + + /** + * Fetches the avatar thumbnail URL for the user. + * + * @param size - The desired size of the avatar image. Defaults to `AvatarImageSize["150x150"]`. + * @param format - The format of the avatar image. Defaults to `"Png"`. + * @param isCircular - Whether the avatar image should be circular. Defaults to `false`. + * @param useCache - Whether to use the cache for the request. Defaults to `true`. + * @returns A promise that resolves to the URL of the avatar thumbnail image. + */ + async fetchAvatarThumbnailUrl( + size: AvatarImageSize = AvatarImageSize["150x150"], + format: AvatarImageFormat = "Png", + isCircular = false, + useCache = true, + ): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Thumbnails", + "/users/avatar", + { + useCache: useCache, + params: { + userIds: [this.id], + size: size, + format: format, + isCircular: isCircular, + }, + }, + ) + ).data[0].imageUrl; + } + + /** + * Fetches the 3D avatar of the user. + * + * @param useCache - A boolean indicating whether to use the cache. Defaults to true. + * @returns A promise that resolves to an `Avatar3D` object containing the 3D avatar data. + */ + async fetchAvatar3D(useCache = true): Promise { + const jsonUrl = ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Thumbnails", + "/users/avatar-3d", + { + useCache: useCache, + params: { + userId: this.id, + }, + }, + ) + ).imageUrl; + if (!jsonUrl || jsonUrl === "") + throw new Error("Failed to fetch the 3D avatar data."); + + return ( + await fetch(jsonUrl, { + method: "GET", + }) + ).json(); + } + + /** + * Fetches the avatar bust URL for the user. + * + * @param size - The size of the avatar bust image. Defaults to `AvatarBustImageSize["150x150"]`. + * @param format - The format of the avatar bust image. Defaults to `"Png"`. + * @param isCircular - Whether the avatar bust image should be circular. Defaults to `false`. + * @param useCache - Whether to use the cache for fetching the avatar bust image. Defaults to `true`. + * @returns A promise that resolves to the URL of the avatar bust image. + */ + async fetchAvatarBustUrl( + size: AvatarBustImageSize = AvatarBustImageSize["150x150"], + format: AvatarBustImageFormat = "Png", + isCircular = false, + useCache = true, + ): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Thumbnails", + "/users/avatar-bust", + { + useCache: useCache, + params: { + userIds: [this.id], + size: size, + format: format, + isCircular: isCircular, + }, + }, + ) + ).data[0].imageUrl; + } + + /** + * Fetches the avatar headshot URL for the user. + * + * @param size - The size of the avatar image. Defaults to "150x150". + * @param format - The format of the avatar image. Defaults to "Png". + * @param isCircular - Whether the avatar image should be circular. Defaults to false. + * @param useCache - Whether to use the cache for the request. Defaults to true. + * @returns A promise that resolves to the URL of the avatar headshot image. + */ + async fetchAvatarHeadshotUrl( + size: AvatarImageSize = AvatarImageSize["150x150"], + format: AvatarImageFormat = "Png", + isCircular = false, + useCache = true, + ): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Thumbnails", + "/users/avatar-headshot", + { + useCache: useCache, + params: { + userIds: [this.id], + size: size, + format: format, + isCircular: isCircular, + }, + }, + ) + ).data[0].imageUrl; + } + + /* + Methods related to the Friends API + Docs: https://friends.roblox.com/docs/index.html + */ + + /** + * Fetches the metadata for the user's friends. + * + * @param {boolean} [useCache=true] - Determines whether to use cached data. + * @returns {Promise} A promise that resolves to the friends' metadata. + */ + async fetchFriendsMetadata(useCache = true): Promise { + return await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Friends", + "/metadata", + { + useCache: useCache, + params: { + targetUserId: this.id, + }, + }, + ); + } + + //? Friends + + /** + * Fetches the list of friends for the user. + * + * @param {number} [maxResults=100] - The maximum number of friends to return. + * @param {boolean} [useCache=true] - Whether to use cached data if available. + * @returns {Promise} A promise that resolves to an array of Friend objects. + * @throws {Error} Throws an error if the client is not authenticated. + */ + async fetchFriends(maxResults = 100, useCache = true): Promise { + if (!this.client.isLoggedIn()) + throw new Error( + "You must be authenticated to view someone's friend list.", + ); + + const returnData = [] as Friend[]; + for (const friend of ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Friends", + `/users/${this.id}/friends`, + { useCache: useCache }, + ) + ).data) { + returnData.push( + await factory.createFriend(this.client, friend, this), + ); + if (maxResults && returnData.length >= maxResults) break; } + return returnData; } - async canViewInventory(): Promise { - return (await this.client.fetchHandler.fetch('GET', 'Inventory', `/users/${this.id}/can-view-inventory`)).canView; + /** + * Fetches the count of friends for the user. + * + * @param {boolean} [useCache=true] - Determines whether to use cached data. + * @returns {Promise} - A promise that resolves to the number of friends. + */ + async fetchFriendCount(useCache = true): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Friends", + `/users/${this.id}/friends/count`, + { useCache: useCache }, + ) + ).count; } + //? Followers + + /** + * Fetches the followers of the user. + * + * @param maxResults - The maximum number of results to return. Defaults to 100. + * @param sortOrder - The order in which to sort the results. Defaults to "Asc". + * @param useCache - Whether to use cached data. Defaults to true. + * @returns A promise that resolves to an array of User objects representing the followers. + */ + async fetchFollowers( + maxResults = 100, + sortOrder: SortOrder = "Asc", + useCache = true, + ): Promise { + const returnData = [] as User[]; + const rawData = await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "Friends", + `/users/${this.id}/followers`, + { useCache: useCache, params: { sortOrder: sortOrder } }, + { maxResults: maxResults, perPage: 100 }, + ); + + for (const data of rawData) + returnData.push(await factory.createUser(this.client, data)); + //? Why not use wrapblox.fetchUser() here? Because it would use an unnecessary API request. + + return returnData; + } + + /** + * Fetches the follower count for the user. + * + * @param {boolean} [useCache=true] - Determines whether to use cached data or not. + * @returns {Promise} A promise that resolves to the follower count. + */ + async fetchFollowerCount(useCache = true): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Friends", + `/users/${this.id}/followers/count`, + { useCache: useCache }, + ) + ).count; + } + + //? Followings + + /** + * Fetches the list of users that the current user is following. + * + * @param maxResults - The maximum number of results to return. Defaults to 100. + * @param sortOrder - The order in which to sort the results. Defaults to "Asc". + * @param useCache - Whether to use cached data. Defaults to true. + * @returns A promise that resolves to an array of User objects representing the followings. + */ + async fetchFollowings( + maxResults = 100, + sortOrder: SortOrder = "Asc", + useCache = true, + ): Promise { + const returnData = [] as User[]; + const rawData = await this.client.fetchHandler.fetchLegacyAPIList( + "GET", + "Friends", + `/users/${this.id}/followings`, + { useCache: useCache, params: { sortOrder: sortOrder } }, + { maxResults: maxResults, perPage: 100 }, + ); + + for (const data of rawData) + returnData.push(await factory.createUser(this.client, data)); + //? Why not use wrapblox.fetchUser() here? Because it would use an unnecessary API request. + + return returnData; + } + + /** + * Fetches the count of followings for the user. + * + * @param {boolean} [useCache=true] - Determines whether to use cached data. + * @returns {Promise} - A promise that resolves to the count of followings. + */ + async fetchFollowingsCount(useCache = true): Promise { + return ( + await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "Friends", + `/users/${this.id}/followings/count`, + { useCache: useCache }, + ) + ).count; + } /* - async fetchFavoriteGames() : Promise { - // WIP + Methods related to the AccountSettings API + Docs: https://accountsettings.roblox.com/docs/index.html + */ + + /** + * Blocks the user associated with this instance. + * + * @returns {Promise} A promise that resolves when the user has been successfully blocked. + * @throws {Error} Throws an error if the client is not authenticated. + */ + async block(): Promise { + if (!this.client.isLoggedIn()) + throw new Error("You must be authenticated to block users."); + + await this.client.fetchHandler.fetchLegacyAPI( + "POST", + "AccountSettings", + `/users/${this.id}/block`, + ); } + + /** + * Unblocks the user associated with this instance. + * + * @returns {Promise} A promise that resolves when the user has been successfully unblocked. + * @throws {Error} Throws an error if the client is not authenticated. + */ + async unblock(): Promise { + if (!this.client.isLoggedIn()) + throw new Error("You must be authenticated to unblock users."); + + await this.client.fetchHandler.fetchLegacyAPI( + "POST", + "AccountSettings", + `/users/${this.id}/unblock`, + ); + } + + /* + Methods related to the PremiumFeatures API + Docs: https://premiumfeatures.roblox.com/docs/index.html */ - - toString() { - if (this.name === this.displayName) return this.name; - return `${this.displayName} (@${this.name})`; + + /** + * Checks if the user has a premium membership. + * + * @param {boolean} [useCache=true] - Whether to use cached data or not. + * @returns {Promise} A promise that resolves to a boolean indicating if the user has a premium membership. + */ + async hasPremium(useCache = true): Promise { + return await this.client.fetchHandler.fetchLegacyAPI( + "GET", + "PremiumFeatures", + `/users/${this.id}/validate-membership`, + { useCache: useCache }, + ); } -} + // Miscellaneous -export default User; \ No newline at end of file + /** + * Returns a string representation of the user. + * The format of the returned string is `${this.name}:${this.id}`. + * + * @returns {string} The formatted string. + */ + toString(): string { + return `${this.name}:${this.id}`; + } +} diff --git a/src/Classes/UserRoleManager.ts b/src/Classes/UserRoleManager.ts deleted file mode 100644 index 790d6f3..0000000 --- a/src/Classes/UserRoleManager.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { APIUserGroup } from "../Types/UserTypes.js"; -import WrapBlox, { APIRoles, Group, Role } from "../index.js"; - -class UserRoleManager { - rawdata : APIUserGroup[]; - client : WrapBlox; - constructor(client : WrapBlox, data : APIUserGroup[]) { - this.rawdata = data; - this.client = client; - } - - async fetchGroups() : Promise { - return await Promise.all(this.rawdata.map(async (group) => { - return await this.client.fetchGroup(group.group.id); - })); - } - - isInGroup(groupId : number) : boolean { - return this.rawdata.some((group) => group.group.id === groupId); - } - - async getGroup(groupId : number) : Promise { - const group = this.rawdata.find((group) => group.group.id === groupId); - if (!group) return undefined; - return await this.client.fetchGroup(group.group.id); - } - - getRawRole(groupId : number) : APIRoles | undefined{ - const group = this.rawdata.find((group) => group.group.id === groupId); - if (!group) return undefined; - return group.role - } - - async getRole(groupId : number) : Promise { - const group = this.rawdata.find((group) => group.group.id === groupId); - if (!group) return undefined; - const realGroup = await this.getGroup(group.group.id); - if (!realGroup) return undefined; - - return realGroup.fetchRole(group.role.id); - } - - - - - -} - -export default UserRoleManager; \ No newline at end of file diff --git a/src/Types/BadgeTypes.d.ts b/src/Types/BadgeTypes.d.ts index d69edbb..eda9900 100644 --- a/src/Types/BadgeTypes.d.ts +++ b/src/Types/BadgeTypes.d.ts @@ -1,4 +1,44 @@ +export type RawBadgeData = { + id: number; + + name: string; + displayName: string; + + description: string; + displayDescription: string; + + iconImageId: number; + displayIconImageId: number; + + created: string; + updated: string; + + statistics: { + pastDayAwardedCount: number; + awardedCount: number; + winRatePercentage: number; + }; + + awardingUniverse: { + id: number; + name: string; + rootPlaceId: number; + }; + + enabled: boolean; +}; + +export type BadgeImageFormat = "Png" | "Webp"; +export type BadgeCreatorType = "Place" | "Group"; +export type BadgeAwarderType = "Place"; + +export type BadgeServiceMetadata = { + badgeCreationPrice: number; + maxBadgeNameLength: number; + maxBadgeDescriptionLength: number; +}; + export type AwardedBadge = { - badgeId : number, - awardedDate : string, -} \ No newline at end of file + badgeId: number; + awardedDate: string; +}; diff --git a/src/Types/BaseTypes.d.ts b/src/Types/BaseTypes.d.ts index 05f42af..af52a52 100644 --- a/src/Types/BaseTypes.d.ts +++ b/src/Types/BaseTypes.d.ts @@ -1,12 +1,24 @@ -export type ValidUrls = "Users" | "Groups" | "Thumbnails" | "Friends" | "GamesV2" | "Games" | "Badges" | "Inventory" +export type HttpMethods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; -export type HttpMethods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" - -export type SortOrder = "Asc" | "Desc" +export type SortOrder = "Asc" | "Desc"; export type FetchOptions = { - usecache?: boolean, - cookie?: string, - params?: { [key: string | number]: unknown }, - body?: { [key: string]: unknown }, -} \ No newline at end of file + useCache?: boolean; + cookie?: string; + CSRFToken?: string; + APIKey?: string; + params?: { [key: string | number]: unknown }; + body?: { [key: string]: unknown }; +}; + +export type Enumerate< + N extends number, + Acc extends number[] = [], +> = Acc["length"] extends N + ? Acc[number] + : Enumerate; + +export type IntRange = Exclude< + Enumerate, + Enumerate +>; diff --git a/src/Types/Enums.ts b/src/Types/Enums.ts index a9d2ece..5df238f 100644 --- a/src/Types/Enums.ts +++ b/src/Types/Enums.ts @@ -1,4 +1,4 @@ -export enum AvatarSize { +export enum AvatarImageSize { "30x30" = "30x30", "48x48" = "48x48", "60x60" = "60x60", @@ -15,9 +15,48 @@ export enum AvatarSize { "720x720" = "720x720", } +export enum AvatarBustImageSize { + "48x48" = "48x48", + "50x50" = "50x50", + "60x60" = "60x60", + "75x75" = "75x75", + "100x100" = "100x100", + "150x150" = "150x150", + "180x180" = "180x180", + "352x352" = "352x352", + "420x420" = "420x420", +} + +export enum BadgeImageSize { + "150x150" = "150x150", +} + export enum ItemTypes { Asset = 0, GamePass = 1, Badge = 2, Bundle = 3, -} \ No newline at end of file +} + +export enum GroupOwnerType { + User = 0, +} + +export enum UniverseAvatarType { + MorphToR6 = 1, + PlayerChoice = 2, + MorphToR15 = 3, +} + +export enum UserPresenceState { + Offline = 0, + Online = 1, + InGame = 2, + InStudio = 3, + Invisible = 4, +} + +export enum ServerType { + Public = 0, + Private = 1, +} diff --git a/src/Types/GameTypes.d.ts b/src/Types/GameTypes.d.ts deleted file mode 100644 index f347851..0000000 --- a/src/Types/GameTypes.d.ts +++ /dev/null @@ -1,35 +0,0 @@ - -export interface APICreator { - id: number; - name: string; - type: string; - isRNVAccount: boolean; - hasVerifiedBadge: boolean; -} - -export interface APIGameData { - id: number; - rootPlaceId: number; - name: string; - description: string; - sourceName: string; - sourceDescription: string; - creator: APICreator; - price: number; - allowedGearGenres: string[]; - allowedGearCategories: string[]; - isGenreEnforced: boolean; - copyingAllowed: boolean; - playing: number; - visits: number; - maxPlayers: number; - created: string; - updated: string; - studioAccessToApisAllowed: boolean; - createVipServersAllowed: boolean; - universeAvatarType: number; - genre: string; - isAllGenre: boolean; - isFavoritedByUser: boolean; - favoritedCount: number; -} \ No newline at end of file diff --git a/src/Types/GroupTypes.d.ts b/src/Types/GroupTypes.d.ts index eec3c49..fce13c1 100644 --- a/src/Types/GroupTypes.d.ts +++ b/src/Types/GroupTypes.d.ts @@ -1,86 +1,93 @@ -export type RawShout = { - body : string, - poster : { - buildersClubMembershipType : number, - hasVerifiedBadge : boolean, - userId : number, - username : string, - displayName : string, - }, - created : string, - updated : string, -} +import { IntRange } from "./BaseTypes.js"; +import { GroupOwnerType } from "./Enums.ts"; export type RawGroupData = { - id : number, - name : string, - description : string, - owner : { - buildersClubMembershipType? : number, - hasVerifiedBadge : boolean, - userId : number, - username : string, - displayName : string, - }, - shout : RawShout, - memberCount : number, - isBuildersClubOnly : boolean, - publicEntryAllowed : boolean, - isLocked : boolean, - hasVerifiedBadge : boolean, -} + id: number; + name: string; + description: string; + owner: { + id: number; + type: GroupOwnerType; + name: string; + }; + memberCount: number; + created: string; + hasVerifiedBadge: boolean; +}; -export type APIGroupLookup = { - id : number, - name : string, - memberCount : number, - hasVerifiedBadge : boolean, -} +export type GroupRole = { + id: number; + name: string; + rank: IntRange<1, 256>; +}; -export type RawMemberData = { - user : { - hasVerifiedBadge : boolean, - userId : number, - username : string, - displayName : string, - }, - role : { - name : string, - rank : number, - id : number, - }, -} +export type GroupAuditLog = { + actor: { + user: { + buildersClubMembershipType: number; + hasVerifiedBadge: boolean; + userId: number; + username: string; + displayName: string; + }; + role: { + id: number; + name: string; + description: string; + rank: number; + memberCount: number; + }; + }; + actionType: GroupActionType; + description: unknown; + created: Date; +}; -export type APIGroupSettings = { - isApprovalRequired : boolean, - areEnemiesAllowed : boolean, - areGroupFundsVisible : boolean, - areGroupGamesVisible : boolean, - isGroupNameChangeEnabled : boolean, - isBuildersClubRequired : boolean, -} - -export type APIPayoutInfo = { - user : { - buildersClubMembershipType: number, - hasVerifiedBadge: boolean, - userId: number, - username: string, - displayName: string, - } - percentage : number, -} - -export type APIMemberRole = { - name : string, - rank : number, - id : number, -} - -export type APIRoles = { - id : number, - name : string, - description : string, - rank : number, - memberCount : number, -} \ No newline at end of file +export type GroupActionType = + | "DeletePost" + | "RemoveMember" + | "AcceptJoinRequest" + | "DeclineJoinRequest" + | "PostStatus" + | "ChangeRank" + | "BuyAd" + | "SendAllyRequest" + | "CreateEnemy" + | "AcceptAllyRequest" + | "DeclineAllyRequest" + | "DeleteAlly" + | "DeleteEnemy" + | "AddGroupPlace" + | "RemoveGroupPlace" + | "CreateItems" + | "ConfigureItems" + | "SpendGroupFunds" + | "ChangeOwner" + | "Delete" + | "AdjustCurrencyAmounts" + | "Abandon" + | "Claim" + | "Rename" + | "ChangeDescription" + | "InviteToClan" + | "KickFromClan" + | "CancelClanInvite" + | "BuyClan" + | "CreateGroupAsset" + | "UpdateGroupAsset" + | "ConfigureGroupAsset" + | "RevertGroupAsset" + | "CreateGroupDeveloperProduct" + | "ConfigureGroupGame" + | "CreateGroupDeveloperSubscriptionProduct" + | "Lock" + | "Unlock" + | "CreateGamePass" + | "CreateBadge" + | "ConfigureBadge" + | "SavePlace" + | "PublishPlace" + | "UpdateRolesetRank" + | "UpdateRolesetData" + | "BanMember" + | "UnbanMember"; diff --git a/src/Types/InventoryTypes.d.ts b/src/Types/InventoryTypes.d.ts index 2bb2c94..23c6671 100644 --- a/src/Types/InventoryTypes.d.ts +++ b/src/Types/InventoryTypes.d.ts @@ -1,6 +1,6 @@ -export type OwnedItem = { - type : "Badge" | "Asset" | "GamePass" | "Bundle", - id : number, - name : string, - instanceId : null, -} \ No newline at end of file +export type OwnedAsset = { + type: "Badge" | "Asset" | "GamePass" | "Bundle"; + id: number; + name: string; + instanceId: null; +}; diff --git a/src/Types/PlaceTypes.d.ts b/src/Types/PlaceTypes.d.ts new file mode 100644 index 0000000..fd5e3cb --- /dev/null +++ b/src/Types/PlaceTypes.d.ts @@ -0,0 +1,43 @@ +export type RawPlaceData = { + placeId: number; + name: string; + description: string; + sourceName: string; + sourceDescription: string; + url: string; + builder: string; + builderId: number; + hasVerifiedBadge: boolean; + isPlayable: boolean; + reasonProhibited: string; + universeId: number; + universeRootPlaceId: number; + price: number; + imageToken: string; +}; + +export type PlaceServer = { + id: number; + maxPlayers: number; + playing: number; + playerTokens: string[]; + players: { + playerToken: string; + id: number; + name: string; + displayName: string; + }[]; + + fps: number; + ping: number; + + name: string; + vipServerId: number; + accessCode: string; + owner: { + hasVerifiedBadge: boolean; + id: number; + name: string; + displayName: string; + }; +}; diff --git a/src/Types/UniverseTypes.d.ts b/src/Types/UniverseTypes.d.ts new file mode 100644 index 0000000..d800ac4 --- /dev/null +++ b/src/Types/UniverseTypes.d.ts @@ -0,0 +1,49 @@ +import { UniverseAvatarType } from "./Enums.ts"; + +export type RawUniverseData = { + id: number; + rootPlaceId: number; + + name: string; + description: string | null; + + sourceName: string; + sourceDescription: string; + + creator: { + id: number; + name: string; + type: UniverseCreatorType; + isRNVAccount: boolean; + hasVerifiedBadge: boolean; + }; + + price: number | null; + allowedGearGenres: string[]; //TODO: properly type this + allowedGearCategories: string[]; //TODO: properly type this + + isGenreEnforced: boolean; + copyingAllowed: boolean; + + playing: number; + visits: number; + maxPlayers: number; + + created: string; + updated: string; + + studioAccessToApisAllowed: boolean; + createVipServersAllowed: boolean; + universeAvatarType: UniverseAvatarType; + + genre: string; //TODO: properly type this + genre_l1: string; + genre_l2: string; + isAllGenre: boolean; + + isFavoritedByUser: boolean; + favoritedCount: number; + licenseDescription: string; +}; + +export type UniverseCreatorType = "User" | "Group"; diff --git a/src/Types/UserTypes.d.ts b/src/Types/UserTypes.d.ts index e62e8b2..89dc194 100644 --- a/src/Types/UserTypes.d.ts +++ b/src/Types/UserTypes.d.ts @@ -1,54 +1,218 @@ -import { APIRoles, RawGroupData } from "./GroupTypes.js"; +import { IntRange } from "./BaseTypes.js"; +import { UserPresenceState } from "./Enums.ts"; +import type { GroupRole } from "./GroupTypes.js"; export type RawUserData = { - name : string, - displayName : string, - id : number, - hasVerifiedBadge : boolean, - externalAppDisplayName? : string, - isBanned : boolean, - created : string, - description : string, -} - -export type RawFriendData = { - isOnline : boolean, - isDeleted : boolean, - friendFrequentScore : number, - friendFrequentRank : number, -} & RawUserData; - - -export type RawFriendRequest = { - friendRequest : { - sentAt : string, - senderId : number, - sourceUniverseId : number, - originSourceType : number, - contactName : string, - }, - mutualFriendsList : string[], -} & RawUserData; - - -export type APIUserGroup = { - group: RawGroupData; - role: APIRoles; -} + name: string; + displayName: string; + id: number; + hasVerifiedBadge: boolean; + externalAppDisplayName?: string; + isBanned: boolean; + created: string; + description: string; +}; + +export type RawUserGroupRoles = { + group: { + id: number; + name: string; + memberCount: number; + hasVerifiedBadge: boolean; + }; + role: GroupRole; + isNotificationsEnabled: boolean; +}; + +export type FriendMetadata = { + isFriendsFilterBarEnabled: boolean; + isFriendsPageSortExperimentEnabled: boolean; + isFriendsUserDataStoreCacheEnabled: boolean; + frequentFriendSortRollout: number; + userName: string; + displayName: string; +}; + +export type RawFriendData = RawUserData & { + isOnline: boolean; + isDeleted: boolean; + friendFrequentScore: number; + friendFrequentRank: number; +}; + +export type RawFriendRequest = RawUserData & { + friendRequest: { + sentAt: string; + senderId: number; + sourceUniverseId: number; + originSourceType: number; + contactName: string; + }; + mutualFriendsList: string[]; +}; export type BirthData = { - birthMonth : number, - birthDay : number, - birthYear : number, -} + birthMonth: number; + birthDay: number; + birthYear: number; +}; + +export type UserLastLocation = "Website" | ""; + +export type UserPresence = { + userPresenceType: userPresenceState; + lastLocation: lastLocation; + placeId: number | null; + rootPlaceId: number | null; + gameId: number | null; + universeId: number | null; + lastOnline: Date; +}; + +export type AvatarImageFormat = "Png" | "Jpeg" | "Webp"; +export type AvatarBustImageFormat = "Png" | "Webp"; +export type AvatarType = "R6" | "R15"; +export type AvatarAssetType = + | "Gear" + | "Pants" + | "Shirt" + | "TShirt" + | "LeftShoeAccessory" + | "RightShoeAccessory" + | "ShirtAccessory" + | "PantsAccessory" + | "TShirtAccessory" + | "SweaterAccessory" + | "JacketAccessory" + | "ShortsAccessory" + | "DressSkirtAccessory" + | "Hat" + | "WaistAccessory" + | "NeckAccessory" + | "ShoulderAccessory" + | "FrontAccessory" + | "BackAccessory" + | "FaceAccessory" + | "HairAccessory" + | "RunAnimation" + | "WalkAnimation" + | "FallAnimation" + | "JumpAnimation" + | "IdleAnimation" + | "SwimAnimation" + | "ClimbAnimation"; + +export type Avatar3D = { + camera: { + position: { + x: number; + y: number; + z: number; + }; + direction: { + x: number; + y: number; + z: number; + }; + fov: number; + }; + aabb: { + min: { + x: number; + y: number; + z: number; + }; + max: { + x: number; + y: number; + z: number; + }; + }; + mtl: string; + obj: string; + textures: string[]; +}; + +export type AvatarAsset = { + id: number; + name: string; + assetType: { + id: number; + name: AvatarAssetType; + }; + currentVersionId: number; + meta?: { + order?: number; + puffiness?: number; + position?: { + X: number; + Y: number; + Z: number; + }; + rotation?: { + X: number; + Y: number; + Z: number; + }; + scale?: { + X: number; + Y: number; + Z: number; + }; + version?: number; + }; +}; -export type APIUserLookup = { - previousUsernames: string[], - id: number, - hasVerifiedBadge : boolean, - name : string, - displayName : string, -} +export type AvatarEmote = { + assetId: number; + assetName: string; + position: number; +}; +export type AvatarV1 = { + scales: { + height: number; + width: number; + head: number; + depth: number; + proportion: number; + bodyType: number; + }; + playerAvatarType: AvatarType; + bodyColors: { + headColorId: number; + torsoColorId: number; + rightArmColorId: number; + leftArmColorId: number; + rightLegColorId: number; + leftLegColorId: number; + }; + assets: AvatarAsset[]; + defaultShirtApplied: boolean; + defaultPantsApplied: boolean; + emotes: AvatarEmote[]; +}; -export type AvatarImageTypes = "Png" | "Jpeg" | "WebP"; \ No newline at end of file +export type AvatarV2 = { + scales: { + height: number; + width: number; + head: number; + depth: number; + proportion: number; + bodyType: number; + }; + playerAvatarType: AvatarType; + bodyColor3s: { + headColor3: string; + torsoColor3: string; + rightArmColor3: string; + leftArmColor3: string; + rightLegColor3: string; + leftLegColor3: string; + }; + assets: AvatarAsset[]; + defaultShirtApplied: boolean; + defaultPantsApplied: boolean; + emotes: AvatarEmote[]; +}; diff --git a/src/index.ts b/src/index.ts index 97c7452..8e6c711 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,143 +1,322 @@ +import FetchHandler from "./Classes/Internal/fetchHandler.js"; +import Badge from "./Classes/Badge.js"; import Group from "./Classes/Group.js"; +import Place from "./Classes/Place.js"; +import Universe from "./Classes/Universe.js"; import User from "./Classes/User.js"; -import FetchHandler from "./Classes/Internal/fetchHandler.js"; -import { APIGroupLookup, RawGroupData } from "./Types/GroupTypes.js"; -import { APIUserLookup, RawUserData } from "./Types/UserTypes.js"; -import Member from "./Classes/Member.js"; -import Role from "./Classes/Role.js"; import AuthedUser from "./Classes/AuthedUser.js"; -import { APIGameData } from "./Types/GameTypes.js"; -import Game from "./Classes/Game.js"; -export { Member, Group, User, Role} +import { RawGroupData } from "./Types/GroupTypes.js"; +import { RawBadgeData } from "./Types/BadgeTypes.js"; +import { RawUniverseData } from "./Types/UniverseTypes.js"; +import { RawUserData } from "./Types/UserTypes.js"; + +//export { Badge, Group, Place, Universe, User }; export type * from "./Types/BaseTypes.js"; +export type * from "./Types/BadgeTypes.js"; export type * from "./Types/GroupTypes.js"; -export type * from "./Types/UserTypes.js"; -export type * from "./Types/GameTypes.js"; -export type * from "./Types/BadgeTypes.js" export type * from "./Types/InventoryTypes.js"; +export type * from "./Types/PlaceTypes.js"; +export type * from "./Types/UniverseTypes.js"; +export type * from "./Types/UserTypes.js"; export * from "./Types/Enums.js"; +export default class WrapBlox { + fetchHandler: FetchHandler; + self: AuthedUser | null = null; -class WrapBlox { - fetchHandler : FetchHandler; - self : AuthedUser | null = null; - - constructor() { this.fetchHandler = new FetchHandler(); } - + /** - * + * Authenticates a user using the provided cookie. + * Note that the cookie should be unmodified, + * it should contain the `_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this-will-allow-someone-to-log-in-as-you-and-to-steal-your-ROBUX-and-items.|_` at the start. + * + * @param cookie - The authentication cookie. + * @returns A promise that resolves to an instance of `AuthedUser` representing the authenticated user. */ - - isLoggedIn = () : this is {self : AuthedUser} => { + login = async (cookie: string) => { + this.fetchHandler.setCredential("cookie", cookie); + + const userInfo = await this.fetchHandler.fetchLegacyAPI( + "GET", + "Users", + "/users/authenticated", + ); + const realUserData = await this.fetchRawUser(userInfo.id); + this.self = new AuthedUser(this, realUserData, cookie); + return this.self; + }; + + fetchAuthedUser = async (cookie: string) => { + const userInfo = await this.fetchHandler.fetchLegacyAPI( + "GET", + "Users", + "/users/authenticated", + { cookie }, + ); + const realUserData = await this.fetchRawUser(userInfo.id); + return new AuthedUser(this, realUserData, cookie); + }; + + /** + * Checks if the user is logged in. + * + * @returns {this is {self: AuthedUser}} Returns true if `self` is not null, indicating the user is authenticated. + */ + isLoggedIn = (): this is { self: AuthedUser } => { return this.self !== null; - } - + }; + + //? Users + /** - * Get the raw data of a user - * @param userId The ID of the user to fetch - * @returns The raw data of the user + * Fetches raw user data based on the provided query. + * + * @param query - The query to search for the user, which can be a string (username) or a number (Id). + * @param useCache - Optional parameter to determine whether to use cached data. Defaults to true. + * @returns A promise that resolves to a User object. + * @throws Will throw an error if no results are found when searching by user name. */ - fetchRawUser = async (userId : number, usecache = true) : Promise => { - return await this.fetchHandler.fetch('GET', 'Users', `/users/${userId}`, {usecache}); - } - + private fetchRawUser = async ( + query: string | number, + useCache = true, + ): Promise => { + if (typeof query === "number") { + return await this.fetchHandler.fetchLegacyAPI( + "GET", + "Users", + `/users/${query}`, + { useCache: useCache }, + ); + } + + const userId = ( + await this.fetchHandler.fetchLegacyAPI( + "POST", + "Users", + "/usernames/users", + { + useCache: useCache, + body: { + usernames: [query], + excludeBannedUsers: false, + }, + }, + ) + ).data[0]?.id; + if (!userId) throw new Error("User not found"); + + return await this.fetchRawUser(userId, useCache); + }; + /** - * Gets the user object of a user - * @param userId The ID of the user to fetch - * @returns The user object + * Fetches a user based on the provided query. + * + * @param query - The query to search for the user, which can be a string (username) or a number (Id). + * @param useCache - Optional parameter to determine whether to use cached data. Defaults to true. + * @returns A promise that resolves to a User object. + * @throws Will throw an error if the user is not found. */ - fetchUser = async (userId : number, usecache = true) => { - const rawData = await this.fetchRawUser(userId, usecache); + fetchUser = async ( + query: string | number, + useCache = true, + ): Promise => { + const rawData = await this.fetchRawUser(query, useCache); + if (!rawData) throw new Error("User not found"); + return new User(this, rawData); - } + }; + + //? Badges + /** - * Get the user object of a user by their username - * @param username The username of the user to fetch - * @returns The user object + * Fetches a raw data of a badge by its Id. + * + * @param badgeId - The ID of the badge to fetch. + * @param useCache - A boolean indicating whether to use the cache. Defaults to true. + * @returns A promise that resolves to a `Badge` object. + * @throws Will throw an error if the badge is not found. */ - fetchUserByName = async (username : string, usecache = true) => { - // const rawData = (await this.fetchHandler.fetch('POST', 'Users', "/usernames/users", undefined, {usernames: [username]})).data[0]; - const rawData = (await this.fetchHandler.fetch("POST", "Users", "/usernames/users", { - body: { - usernames: [username] - } - })).data[0]; - if (!rawData) throw new Error("User not found"); - return await this.fetchUser(rawData.id, usecache); - } + private fetchRawBadge = async ( + badgeId: number, + useCache = true, + ): Promise => { + return await this.fetchHandler.fetchLegacyAPI( + "GET", + "Badges", + `/badges/${badgeId}`, + { useCache: useCache }, + ); + }; + /** - * Get the raw data of a group - * @param groupId The ID of the group to fetch - * @returns The raw data of the group + * Fetches a badge by its Id. + * + * @param badgeId - The ID of the badge to fetch. + * @param useCache - A boolean indicating whether to use the cache. Defaults to true. + * @returns A promise that resolves to a `Badge` object. + * @throws Will throw an error if the badge is not found. */ - fetchRawGroup = async (groupId : number, usecache = true) : Promise => { - return await this.fetchHandler.fetch('GET', 'Groups', `/groups/${groupId}`, {usecache}); - } - + fetchBadge = async (badgeId: number, useCache = true): Promise => { + const rawData = await this.fetchRawBadge(badgeId, useCache); + if (!rawData) throw new Error("Badge not found"); + + return new Badge(this, rawData); + }; + + //? Groups + /** - * Get the group object of a group - * @param groupId The ID of the group to fetch - * @returns The group object + * Fetches raw group data based on a query which can be either a group name or a group ID. + * If a group name is provided, it will first look up the group Id and then fetch the group data. + * It is recommended to use the group Id over the name. + * + * @param query - The group name (string) or group Id (number) to search for. + * @param useCache - Optional boolean to indicate whether to use cached data. Defaults to true. + * @returns A promise that resolves to the raw group data. + * @throws Will throw an error if no results are found when searching by group name. */ - fetchGroup = async (groupId : number, usecache = true) => { - const rawData = await this.fetchRawGroup(groupId, usecache); + private fetchRawGroup = async ( + query: string | number, + useCache = true, + ): Promise => { + if (typeof query === "number") { + return ( + await this.fetchHandler.fetchLegacyAPI( + "GET", + "GroupsV2", + "/groups", + { + useCache: useCache, + params: { + groupIds: [query], + }, + }, + ) + ).data[0]; + } + + const groupId = ( + await this.fetchHandler.fetchLegacyAPI( + "GET", + "Groups", + "/groups/search/lookup", + { + useCache: useCache, + params: { + groupName: query, + }, + }, + ) + )?.data[0]?.id; + + if (!groupId) throw new Error("No results found"); + + return await this.fetchRawGroup(groupId, useCache); + }; + + /** + * Fetches a group based on the provided query. + * If a group name is provided, it will first look up the group Id and then fetch the group data. + * It is recommended to use the group Id over the name. + * + * @param query - The group name (string) or group Id (number) to search for. + * @param useCache - Optional boolean to indicate whether to use cached data. Defaults to true. + * @returns A promise that resolves to a `Group` object. + * @throws Will throw an error if the group is not found. + */ + fetchGroup = async ( + query: string | number, + useCache = true, + ): Promise => { + const rawData = await this.fetchRawGroup(query, useCache); + if (!rawData) throw new Error("Group not found"); + return new Group(this, rawData); - } - + }; + + //? Universes + /** - * Logs in with a cookie, and sets the client's self to the logged in user - * @param cookie The cookie to log in with - * @returns The user object of the logged in user + * Fetches raw universe data for a given universe Id. + * + * @param universeId - The Id of the universe to fetch data for. + * @param useCache - Optional. Whether to use cached data. Defaults to true. + * @returns A promise that resolves to the raw universe data. */ - - login = async (cookie : string) => { - this.fetchHandler.cookie = cookie; - const userInfo = await this.fetchHandler.fetch('GET', 'Users', '/users/authenticated'); - const realUserData = await this.fetchRawUser(userInfo.id); - this.self = new AuthedUser(this, realUserData, cookie); - return this.self; - } - + private fetchRawUniverse = async ( + universeId: number, + useCache = true, + ): Promise => { + return ( + await this.fetchHandler.fetchLegacyAPI("GET", "Games", "/games", { + useCache: useCache, + params: { + universeIds: [universeId], + }, + }) + ).data[0]; + }; + /** - * Similar to login, but does not set the client's self to the logged in user - * @param cookie The cookie to log in with - * @returns The user object of the logged in user + * Fetches the universe data for a given universe Id. + * + * @param universeId - The Id of the universe to fetch. + * @param useCache - Optional. Whether to use cached data if available. Defaults to true. + * @returns A promise that resolves to a `Universe` object. + * @throws Will throw an error if the universe is not found. */ - fetchAuthedUser = async (cookie : string) => { - const userInfo = await this.fetchHandler.fetch('GET', 'Users', '/users/authenticated', {cookie}); - const realUserData = await this.fetchRawUser(userInfo.id); - return new AuthedUser(this, realUserData, cookie); - } - - fetchRawGame = async (universeID : number) : Promise => { - return (await this.fetchHandler.fetch('GET', 'Games', "/games", { - params: { - universeIds: [universeID] - } - })).data[0]; - } - - fetchGame = async (universeID : number) => { - const rawData = await this.fetchRawGame(universeID); - return new Game(this, rawData); - } - - - searchGroups = async (query : string) : Promise => { - const rawData = (await this.fetchHandler.fetch('GET', 'Groups', "/groups/search/lookup", {params: {groupName : query}})).data; - return rawData - } - - searchUsers = async(query : string, limit = 10): Promise => { - const rawData = (await this.fetchHandler.fetch('GET', 'Users', "/users/search", {params: {keyword : query, limit}})).data; - return rawData; - } -} + fetchUniverse = async ( + universeId: number, + useCache = true, + ): Promise => { + const rawData = await this.fetchRawUniverse(universeId, useCache); + if (!rawData) throw new Error("Universe not found"); + return new Universe(this, rawData); + }; -export default WrapBlox; \ No newline at end of file + //? Places + + /** + * Fetches raw place details for a given place Id. + * + * @param placeId - The Id of the place to fetch details for. + * @param useCache - Optional. Whether to use cached data. Defaults to true. + * @returns A promise that resolves to the raw place details. + */ + private fetchRawPlace = async (placeId: number, useCache = true) => { + return ( + await this.fetchHandler.fetchLegacyAPI( + "GET", + "Games", + "/games/multiget-place-details", + { + useCache: useCache, + params: { + placeIds: [placeId], + }, + }, + ) + )[0]; + }; + + /** + * Fetches a place by its Id and returns a Place object. + * + * @param placeId - The Id of the place to fetch. + * @param useCache - Optional. Whether to use cached data if available. Defaults to true. + * @returns A Promise that resolves to a Place object. + * @throws Will throw an error if the place is not found. + */ + fetchPlace = async (placeId: number, useCache = true) => { + const rawData = await this.fetchRawPlace(placeId, useCache); + if (!rawData) throw new Error("Place not found"); + + return new Place(this, rawData); + }; +} diff --git a/tests/badge.test.ts b/tests/badge.test.ts new file mode 100644 index 0000000..ec93aa3 --- /dev/null +++ b/tests/badge.test.ts @@ -0,0 +1,51 @@ +import WrapBlox from "../src/index.js"; +import Badge from "../src/Classes/Badge.js"; + +import dotenv from "dotenv"; +dotenv.config(); + +const client = new WrapBlox(); +const badgeId = 1308554930906649; +const silent = true; + +const log = (message: unknown, ...optionalParams: unknown[]) => { + if (silent) return; + console.log(message, ...optionalParams); +}; + +const authenticated = () => { + if (!client.isLoggedIn()) { + log("Not logged in, skipping..."); + return false; + } + + return true; +}; + +beforeAll(async () => { + if (!process.env.TESTCOOKIE) { + console.log("No cookie provided, skipping..."); + return; + } + + const user = await client.login(process.env.TESTCOOKIE); + log(`Logged in as ${user.name}:${user.id}`); + + expect(user).toBeDefined(); +}); + +test("fetchBadge", async () => { + const badge = await client.fetchBadge(badgeId); + + log("Fetched badge:\n", badge); + + expect(badge).toBeDefined(); + expect(badge).toBeInstanceOf(Badge); +}); + +test("fetchIcon", async () => { + const badge = await client.fetchBadge(badgeId); + const icon = await badge.fetchIcon(); + + log("Fetched icon:\n", icon); +}); diff --git a/tests/group.test.ts b/tests/group.test.ts index b820317..4dd9564 100644 --- a/tests/group.test.ts +++ b/tests/group.test.ts @@ -1,76 +1,95 @@ +import Group from "../src/Classes/Group.js"; +import Universe from "../src/Classes/Universe.js"; import WrapBlox from "../src/index.js"; -import dotenv from "dotenv"; +import dotenv from "dotenv"; dotenv.config(); const client = new WrapBlox(); +const groupId = 33991282; +let preFetchedGroup: Group; +const silent = false; + +const log = (message: unknown, ...optionalParams: unknown[]) => { + if (silent) return; + console.log(message, ...optionalParams); +}; + +const authenticated = () => { + if (!client.isLoggedIn()) { + log("Not logged in, skipping..."); + return false; + } + + return true; +}; -test("login", async () => { - if (process.env.TESTCOOKIE === undefined) { - console.log("No cookie provided, skipping test"); +beforeAll(async () => { + if (!process.env.TESTCOOKIE) { + console.log("No cookie provided, skipping..."); return; } + const user = await client.login(process.env.TESTCOOKIE); - expect(user).toBeDefined(); -}) + log(`Logged in as ${user.name}:${user.id}`); -test("getRawGroup", async () => { - const group = await client.fetchRawGroup(10345148); - expect(group).toBeDefined(); + expect(user).toBeDefined(); }); -test("getGroup", async () => { - const group = await client.fetchGroup(10345148); - expect(group).toBeDefined(); -}); +test("fetchGroup", async () => { + const groupById = await client.fetchGroup(groupId); + const groupByName = await client.fetchGroup("Purple Robotics, LLC"); + preFetchedGroup = groupById; + log(`Fetched group: ${groupById.toString()}, ${groupByName.toString()}`); -test("getJoinRequests", async () => { - if (process.env.TESTCOOKIE === undefined) { - console.log("No cookie provided, skipping test"); - return; - } - const group = await client.fetchGroup(10345148); - const joinRequests = await group.fetchJoinRequests(); + expect(groupById).toBeDefined(); + expect(groupById).toMatchObject(groupById); - expect(joinRequests).toBeDefined(); + expect(groupByName).toBeDefined(); + expect(groupByName).toMatchObject(groupByName); }); -test("getMembers", async () => { +test("fetchAuditLog", async () => { + if (!authenticated()) return; + + const auditLog = await preFetchedGroup.fetchAuditLog(); - const group = await client.fetchGroup(10345148); - const members = await group.fetchMembers(); + log("Group audit log:\n", auditLog); - expect(members).toBeDefined(); + expect(auditLog).toBeDefined(); }); -test("getIcon", async () => { - const group = await client.fetchGroup(10345148); - const icon = await group.fetchIcon(); +/* + Methods related to the Thumbnails API + Docs: https://thumbnails.roblox.com/docs/index.html +*/ +test("fetchIcon", async () => { + const icon = await preFetchedGroup.fetchIcon(); - expect(icon).toBeDefined(); + log("Fetched icon:\n", icon); }); -test("getRoles", async () => { - const group = await client.fetchGroup(10345148); - const roles = await group.fetchRoles(); +test("fetchUniverses", async () => { + const universes = await preFetchedGroup.fetchUniverses(1); + log( + `Fetched [${universes.length}] created universes:\n`, + universes.map((data: Universe) => data.toString()).join("\n"), + ); - expect(roles).toBeDefined(); + expect(universes).toBeDefined(); + expect(universes).toBeInstanceOf(Array); + for (const universe of universes) { + expect(universe).toMatchObject(universe); + } }); -test("getRoleMembers", async () => { - const group = await client.fetchGroup(10345148); - const roles = await group.fetchRoles(); - const members = await roles[1].fetchMembers(); - - - expect(members).toBeDefined(); -}) +test("fetchOwner", async () => { + const owner = await preFetchedGroup.fetchOwner(); -test("searchGroups", async () => { - const groups = await client.searchGroups("Pinewood Builders"); + log(`Fetched owner: ${owner.toString()}`); - expect(groups.length).toBeGreaterThan(0); + expect(owner).toBeDefined(); }); diff --git a/tests/place.test.ts b/tests/place.test.ts new file mode 100644 index 0000000..4a30635 --- /dev/null +++ b/tests/place.test.ts @@ -0,0 +1,68 @@ +import Place from "../src/Classes/Place.js"; +import Universe from "../src/Classes/Universe.js"; +import WrapBlox, { PlaceServer } from "../src/index.js"; + +import dotenv from "dotenv"; +dotenv.config(); + +const client = new WrapBlox(); +const placeId = 24040136; +let preFetchedPlace: Place; +const silent = true; + +const log = (message: unknown, ...optionalParams: unknown[]) => { + if (silent) return; + console.log(message, ...optionalParams); +}; + +const authenticated = () => { + if (!client.isLoggedIn()) { + log("Not logged in, skipping..."); + return false; + } + + return true; +}; + +beforeAll(async () => { + if (!process.env.TESTCOOKIE) { + console.log("No cookie provided, skipping..."); + return; + } + + const user = await client.login(process.env.TESTCOOKIE); + log(`Logged in as ${user.name}:${user.id}`); + + expect(user).toBeDefined(); +}); + +test("fetchPlace", async () => { + const placeById = await client.fetchPlace(placeId); + + preFetchedPlace = placeById; + log(`Fetched place: ${placeById.toString()}`); + + expect(placeById).toBeDefined(); + expect(placeById).toMatchObject(placeById); +}); + +test("fetchServers", async () => { + const servers = await preFetchedPlace.fetchServers(); + + log(`Fetched [${servers.length}] servers:`, servers); + + expect(servers).toBeDefined(); + expect(servers).toBeInstanceOf(Array); + for (const server of servers) { + expect(server).toMatchObject(server); + } +}); + +test("fetchUniverse", async () => { + const universe = await preFetchedPlace.fetchUniverse(); + + log(`Fetched universe: ${universe.toString()}`); + + expect(universe).toBeDefined(); + expect(universe).toMatchObject(universe); +}); diff --git a/tests/universe.test.ts b/tests/universe.test.ts new file mode 100644 index 0000000..ea2ed79 --- /dev/null +++ b/tests/universe.test.ts @@ -0,0 +1,55 @@ +import Place from "../src/Classes/Place.js"; +import Universe from "../src/Classes/Universe.js"; +import WrapBlox from "../src/index.js"; + +import dotenv from "dotenv"; +dotenv.config(); + +const client = new WrapBlox(); +const universeId = 7065948; +let preFetchedUniverse: Universe; +const silent = true; + +const log = (message: unknown, ...optionalParams: unknown[]) => { + if (silent) return; + console.log(message, ...optionalParams); +}; + +const authenticated = () => { + if (!client.isLoggedIn()) { + log("Not logged in, skipping..."); + return false; + } + + return true; +}; + +beforeAll(async () => { + if (!process.env.TESTCOOKIE) { + console.log("No cookie provided, skipping..."); + return; + } + + const user = await client.login(process.env.TESTCOOKIE); + log(`Logged in as ${user.name}:${user.id}`); + + expect(user).toBeDefined(); +}); + +test("fetchUniverse", async () => { + const universeById = await client.fetchUniverse(universeId); + + preFetchedUniverse = universeById; + log(`Fetched universe: ${universeById.toString()}`); + + expect(universeById).toBeDefined(); + expect(universeById).toMatchObject(universeById); +}); + +test("fetchRootPlace", async () => { + const rootPlace = await preFetchedUniverse.fetchRootPlace(); + + log(`Fetched root place: ${rootPlace.toString()}`); + expect(rootPlace).toBeDefined(); + expect(rootPlace).toMatchObject(rootPlace); +}); diff --git a/tests/user.test.ts b/tests/user.test.ts index 1ad079c..a0e5235 100644 --- a/tests/user.test.ts +++ b/tests/user.test.ts @@ -1,51 +1,445 @@ -import WrapBlox from "../src/index.js"; +import Badge from "../src/Classes/Badge.js"; +import Friend from "../src/Classes/Friend.js"; +import Group from "../src/Classes/Group.js"; +import FetchError from "../src/Classes/Internal/FetchError.js"; +import Universe from "../src/Classes/Universe.js"; +import User from "../src/Classes/User.js"; +import WrapBlox, { + ItemTypes, + UserPresence, + GroupRole, + OwnedAsset, + AvatarV1, + AvatarV2, + Avatar3D, + FriendMetadata, +} from "../src/index.js"; + +import dotenv from "dotenv"; +dotenv.config(); const client = new WrapBlox(); +let preFetchedUser: User; +const userId = 2897964600; +const silent = true; + +const log = (message: unknown, ...optionalParams: unknown[]) => { + if (silent) return; + console.log(message, ...optionalParams); +}; + +const authenticated = () => { + if (!client.isLoggedIn()) { + log("Not logged in, skipping..."); + return false; + } + return true; +}; -test("getThumbnail", async () => { - const user = await client.fetchUser(1); - const thumbnail = await user.fetchUserAvatarThumbnailUrl(); - - - expect(thumbnail).toBeDefined(); -}) +beforeAll(async () => { + if (!process.env.TESTCOOKIE) { + console.log("No cookie provided, skipping..."); + return; + } -test("groupTest", async () => { - const user = await client.fetchUser(1494607434); - expect(await user.inGroup(645836)).toBeTruthy(); + const user = await client.login(process.env.TESTCOOKIE); + log(`Logged in as ${user.name}:${user.id}`); + + expect(user).toBeDefined(); }); -test("role", async () => { - const user = await client.fetchUser(1494607434); - const roles = await user.fetchRoles() - const role = await roles.getRole(645836) - - - expect(role).toBeDefined(); +/* + Methods related to the Users API + Docs: https://users.roblox.com/docs/index.html +*/ + +test("fetchUser", async () => { + const userbyId = await client.fetchUser(userId); + const userbyName = await client.fetchUser("Purple_Creativity"); + + preFetchedUser = userbyId; + log(`Fetched users: ${userbyId.toString()}, ${userbyName.toString()}`); + + expect(userbyId).toBeDefined(); + expect(userbyId).toBeInstanceOf(User); + + expect(userbyName).toBeDefined(); + expect(userbyName).toBeInstanceOf(User); }); -test("userLookup", async () => { - const users = await client.searchUsers("Cater") - expect(users).toBeDefined(); -}) - -test("badgeTest", async () => { - const user = await client.fetchUser(1494607434); - const ownsBadge = await user.ownsBadge(150538265); - const ownsBadge2 = await user.ownsBadge(2124453108); - - const date = await user.getBadgeAwardedDate(150538265); - +test("fetchUsernameHistory", async () => { + const usernameHistory = await preFetchedUser.fetchUsernameHistory(); + + log(`Fetched username history:\n${usernameHistory}`); + + expect(usernameHistory).toBeDefined(); + expect(usernameHistory).toBeInstanceOf(Array); +}); + +/* + Methods related to the Presence API + Docs: https://presence.roblox.com/docs/index.html +*/ + +test("fetchLastOnlineDate", async () => { + const date = await preFetchedUser.fetchLastOnlineDate(); + + log(`Fetched user last online date: ${date.toDateString()}`); + expect(date).toBeDefined(); - expect(ownsBadge).toBeTruthy(); - expect(ownsBadge2).toBeFalsy(); - -}) - -test("getUser", async () => { - const user = await client.fetchUser(1); - console.log(user) - console.log(`Fetched ${user}`) - expect(user).toBeDefined(); -}) \ No newline at end of file + expect(date).toBeInstanceOf(Date); +}); + +test("fetchPresence", async () => { + const presence = await preFetchedUser.fetchPresence(); + + log("Fetched user presence:\n", presence); + + expect(presence).toBeDefined(); + expect(presence).toMatchObject(presence); +}); + +/* + Methods related to the Games API + Docs: https://games.roblox.com/docs/index.html +*/ + +test("fetchCreatedUniverses", async () => { + const universes = await preFetchedUser.fetchCreatedUniverses(1); + + log( + `Fetched [${universes.length}] created universes:\n`, + universes.map((data: Universe) => data.toString()).join("\n"), + ); + + expect(universes).toBeDefined(); + expect(universes).toBeInstanceOf(Array); + + for (const universe of universes) { + expect(universe).toBeInstanceOf(Universe); + } +}); + +/* + Methods related to the Groups API + Docs: https://groups.roblox.com/docs/index.html +*/ + +test("inGroup", async () => { + const boolean = await preFetchedUser.inGroup(33991282); + + log(`In group [Purple Robotics, LLC]: ${boolean}`); + + expect(boolean).toBeDefined(); +}); + +test("getRoleInGroup", async () => { + const role = await preFetchedUser.getRoleInGroup(33991282); + + log("Role in group [Purple Robotics, LLC]:\n", role); + + if (role) { + expect(role).toMatchObject(role); + } +}); + +test("fetchPrimaryGroup", async () => { + const primaryGroup = await preFetchedUser.fetchPrimaryGroup(); + + log(`Primary group: ${primaryGroup?.toString()}`); + + if (primaryGroup) { + expect(primaryGroup).toBeInstanceOf(Group); + } +}); + +test("fetchGroups", async () => { + const groups = await preFetchedUser.fetchGroups(); + + log( + `Fetched [${groups.length}] groups:\n`, + groups.map((data: Group) => data.toString()).join("\n"), + ); + + expect(groups).toBeDefined(); + + for (const group of groups) { + expect(group).toBeInstanceOf(Group); + } +}, 60000); + +/* + Methods related to the Badges API + Docs: https://badges.roblox.com/docs/index.html +*/ + +test("fetchBadges", async () => { + const badges = await preFetchedUser.fetchBadges(1); + + log( + `Fetched [${badges.length}] awarded badges:\n`, + badges.map((data: Badge) => data.toString()).join("\n"), + ); + + expect(badges).toBeDefined(); + + for (const badge of badges) { + expect(badge).toBeInstanceOf(Badge); + } +}); + +test("fetchBadgeAwardDate", async () => { + const date = await preFetchedUser.fetchBadgeAwardDate(2124445684); + + log(`Badge awarded at: ${date?.toDateString()}`); + + if (date) { + expect(date).toBeInstanceOf(Date); + } +}); + +/* + Methods related to the Inventory API + Docs: https://inventory.roblox.com/docs/index.html +*/ + +test("canViewInventory", async () => { + const canView = await preFetchedUser.canViewInventory(); + + log(`Can view inventory: ${canView}`); + + expect(canView).toBeDefined(); +}); + +test("ownsAsset", async () => { + const bool = await preFetchedUser.ownsAsset(ItemTypes.GamePass, 776368); + + log(`Owns asset: ${bool}`); + + expect(bool).toBeDefined(); +}); + +test("ownsBadge", async () => { + const bool = await preFetchedUser.ownsBadge(150538398); + + log(`Owns Badge: ${bool}`); + + expect(bool).toBeDefined(); +}); + +test("ownsGamepass", async () => { + const bool = await preFetchedUser.ownsGamepass(776368); + + log(`Owns Gamepass: ${bool}`); + + expect(bool).toBeDefined(); +}); + +test("ownsBundle", async () => { + const bool = await preFetchedUser.ownsBundle(201); + + log(`Owns Bundle: ${bool}`); + + expect(bool).toBeDefined(); +}); + +test("getOwnedAsset", async () => { + const asset = await preFetchedUser.getOwnedAsset( + ItemTypes.GamePass, + 776368, + ); + + log("Owned asset:\n", asset); + + if (asset) { + expect(asset).toMatchObject(asset); + } +}); + +/* + Methods related to the Avatar API + Docs: https://avatar.roblox.com/docs/index.html +*/ + +test("fetchAvatarV1", async () => { + const avatar = await preFetchedUser.fetchAvatarV1(); + + log("User AvatarV1:\n", avatar); + + expect(avatar).toBeDefined(); + expect(avatar).toMatchObject(avatar); +}); + +test("fetchAvatarV2", async () => { + const avatar = await preFetchedUser.fetchAvatarV2(); + + log("User AvatarV2:\n", avatar); + + expect(avatar).toBeDefined(); + expect(avatar).toMatchObject(avatar); +}); + +/* + Methods related to the Thumbnails API + Docs: https://thumbnails.roblox.com/docs/index.html +*/ + +test("fetchAvatarThumbnailUrl", async () => { + const imageUrl = await preFetchedUser.fetchAvatarThumbnailUrl(); + + log(`Fetched imageUrl for Avatar Thumbnail:\n${imageUrl}`); + + expect(imageUrl).toBeDefined(); +}); + +test("fetchAvatar3D", async () => { + const data = await preFetchedUser.fetchAvatar3D(); + + log("Fetched imageUrl for Avatar 3D:\n", data); + + expect(data).toBeDefined(); + expect(data).toMatchObject(data); +}); + +test("fetchAvatarBustUrl", async () => { + const imageUrl = await preFetchedUser.fetchAvatarBustUrl(); + + log(`Fetched imageUrl for Avatar Bust:\n${imageUrl}`); + + expect(imageUrl).toBeDefined(); +}); + +test("fetchAvatarHeadshotUrl", async () => { + const imageUrl = await preFetchedUser.fetchAvatarHeadshotUrl(); + + log(`Fetched imageUrl for Avatar Headshot:\n${imageUrl}`); + + expect(imageUrl).toBeDefined(); +}); + +/* + Methods related to the Friends API + Docs: https://friends.roblox.com/docs/index.html +*/ + +test("fetchFriendsMetadata", async () => { + const metadata = await preFetchedUser.fetchFriendsMetadata(); + + log("Fetched friends metadata:\n", metadata); + + expect(metadata).toBeDefined(); + expect(metadata).toMatchObject(metadata); +}); + +test("fetchUserFriends", async () => { + if (!authenticated()) return; + + const users = await preFetchedUser.fetchFriends(); + + log( + `Fetched [${users.length}] friends:\n`, + users.map((data: Friend) => data.toString()).join("\n"), + ); + + expect(users).toBeDefined(); + for (const friend of users) { + expect(friend).toBeInstanceOf(Friend); + } +}); + +test("fetchUserFriendCount", async () => { + const count = await preFetchedUser.fetchFriendCount(); + + log(`Fetched friend count: [${count}]`); + + expect(count).toBeDefined(); +}); + +test("fetchUserFollowers", async () => { + const users = await preFetchedUser.fetchFollowers(10); + + log( + `Fetched [${users.length}] followers:\n`, + users.map((data: User) => data.toString()).join("\n"), + ); + + expect(users).toBeDefined(); + for (const user of users) { + expect(user).toBeInstanceOf(User); + } +}); + +test("fetchUserFollowerCount", async () => { + const count = await preFetchedUser.fetchFollowerCount(); + + log(`Fetched follower count: [${count}]`); + + expect(count).toBeDefined(); +}); + +test("fetchUserFollowings", async () => { + const users = await preFetchedUser.fetchFollowings(10); + + log( + `Fetched [${users.length}] followings:\n`, + users.map((data: User) => data.toString()).join("\n"), + ); + + expect(users).toBeDefined(); + for (const user of users) { + expect(user).toBeInstanceOf(User); + } +}); + +test("fetchUserFollowingsCount", async () => { + const count = await preFetchedUser.fetchFollowingsCount(); + + log(`Fetched following count: [${count}]`); + + expect(count).toBeDefined(); +}); + +/* + Methods related to the AccountSettings API + Docs: https://accountsettings.roblox.com/docs/index.html +*/ + +test("block", async () => { + if (!authenticated()) return; + + try { + await preFetchedUser.block(); + } catch (error) { + if (!(error instanceof FetchError)) return; + + log(`Failed to block\n${await error.format()}`); + } +}); + +test("unblock", async () => { + if (!authenticated()) return; + + try { + await preFetchedUser.unblock(); + } catch (error) { + if (!(error instanceof FetchError)) return; + + log(`Failed to unblock\n${await error.format()}`); + } +}); + +/* + Methods related to the PremiumFeatures API + Docs: https://premiumfeatures.roblox.com/docs/index.html +*/ + +test("hasPremium", async () => { + if (!authenticated()) return; + + const hasPremium = await preFetchedUser.hasPremium(); + + log(`hasPremium: ${hasPremium}`); + + expect(hasPremium).toBeDefined(); +}); diff --git a/tsconfig.json b/tsconfig.json index a4fc67a..ab30ec1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -105,5 +105,5 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } } +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts index d1905c7..68a5025 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,11 +1,14 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts"], - target: "esnext", - format: ["esm"], // Build for commonJS and ESmodules - dts: true, // Generate declaration file (.d.ts) - splitting: false, - sourcemap: true, - clean: true, -}); \ No newline at end of file + entry: ["src/index.ts"], + target: "esnext", + format: ["esm"], // Build for commonJS and ESmodules + dts: { + resolve: true, + entry: "src/index.ts", + }, // Generate declaration file (.d.ts) + splitting: true, + sourcemap: true, + clean: true, +});