├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── demo.png ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public └── happy.png ├── scripts └── console-snippet.js ├── src ├── App.tsx ├── index.css ├── main.tsx └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build Vite app 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | - run: corepack enable 17 | - run: pnpm install 18 | - run: pnpm build 19 | - uses: actions/upload-artifact@v4 20 | with: 21 | name: dist 22 | path: ./dist 23 | 24 | deploy: 25 | name: Deploy Vite app 26 | needs: build 27 | runs-on: ubuntu-latest 28 | if: github.ref == 'refs/heads/main' 29 | 30 | steps: 31 | - uses: actions/download-artifact@v4 32 | with: 33 | name: dist 34 | path: ./dist 35 | 36 | - name: Deploy to GitHub Pages 37 | uses: peaceiris/actions-gh-pages@v3 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | publish_dir: ./dist 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | friends*.json 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Benjamin Richeson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Friend Graph 2 | 3 | Create a graph of your friends and mutual friends. 4 | 5 | ![demo](demo.png) 6 | 7 | ### Getting Started 8 | Run [this console script](scripts/console-snippet.js) in your Discord dev tools console. Paste the output into a `.json` file, and select it on the website 9 | 10 | > [!CAUTION] 11 | > Use at your own risk. 12 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | 4 | "files": { 5 | "ignore": ["./build", "./dist", "./node_modules"] 6 | }, 7 | 8 | "organizeImports": { 9 | "enabled": true 10 | }, 11 | 12 | "linter": { 13 | "enabled": true, 14 | "rules": { 15 | "recommended": true, 16 | "a11y": {}, 17 | 18 | "complexity": { 19 | "noForEach": "warn", 20 | "noWith": "error", 21 | "noUselessLabel": "warn", 22 | "noUselessRename": "warn", 23 | "useArrowFunction": "warn", 24 | "noUselessConstructor": "warn", 25 | "noMultipleSpacesInRegularExpressionLiterals": "warn" 26 | }, 27 | 28 | "correctness": { 29 | "noUnusedVariables": "warn" 30 | }, 31 | 32 | "nursery": {}, 33 | 34 | "performance": {}, 35 | 36 | "security": {}, 37 | 38 | "style": { 39 | "useEnumInitializers": "off", 40 | "noNonNullAssertion": "off" 41 | }, 42 | 43 | "suspicious": {} 44 | }, 45 | "ignore": ["./scripts/console-snippet.js"] 46 | }, 47 | 48 | "formatter": { 49 | "formatWithErrors": true, 50 | "indentStyle": "space", 51 | "indentWidth": 2, 52 | "lineEnding": "lf", 53 | "lineWidth": 80 54 | }, 55 | 56 | "javascript": { 57 | "formatter": { 58 | "arrowParentheses": "asNeeded", 59 | "bracketSameLine": false, 60 | "bracketSpacing": false, 61 | "jsxQuoteStyle": "single", 62 | "quoteProperties": "asNeeded", 63 | "quoteStyle": "single", 64 | "semicolons": "always", 65 | "trailingComma": "es5" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benricheson101/discord-friend-graph/ba3ff4ab93ead288423cb08ff7fa18b2930fe801/demo.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Frens :3 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-friend-graph", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "biome lint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-force-graph-2d": "^1.25.4", 16 | "react-force-graph-3d": "^1.24.2" 17 | }, 18 | "devDependencies": { 19 | "@biomejs/biome": "^1.7.0", 20 | "@types/react": "^18.2.66", 21 | "@types/react-dom": "^18.2.22", 22 | "@vitejs/plugin-react-swc": "^3.5.0", 23 | "typescript": "^5.2.2", 24 | "vite": "^5.2.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | react: 9 | specifier: ^18.2.0 10 | version: 18.2.0 11 | react-dom: 12 | specifier: ^18.2.0 13 | version: 18.2.0(react@18.2.0) 14 | react-force-graph-2d: 15 | specifier: ^1.25.4 16 | version: 1.25.4(react@18.2.0) 17 | react-force-graph-3d: 18 | specifier: ^1.24.2 19 | version: 1.24.2(react@18.2.0) 20 | 21 | devDependencies: 22 | '@biomejs/biome': 23 | specifier: ^1.7.0 24 | version: 1.7.0 25 | '@types/react': 26 | specifier: ^18.2.66 27 | version: 18.2.79 28 | '@types/react-dom': 29 | specifier: ^18.2.22 30 | version: 18.2.25 31 | '@vitejs/plugin-react-swc': 32 | specifier: ^3.5.0 33 | version: 3.6.0(vite@5.2.8) 34 | typescript: 35 | specifier: ^5.2.2 36 | version: 5.4.5 37 | vite: 38 | specifier: ^5.2.0 39 | version: 5.2.8 40 | 41 | packages: 42 | 43 | /3d-force-graph@1.73.3: 44 | resolution: {integrity: sha512-azb65Lwn2yr/fJ4+qrxjmstVxogjzwJIZL/fdboCKBg6ph/FLW+xdvYFEBZW92XxBn1C8yRKS3d2VkVT3BzLSw==} 45 | engines: {node: '>=12'} 46 | dependencies: 47 | accessor-fn: 1.5.0 48 | kapsule: 1.14.5 49 | three: 0.163.0 50 | three-forcegraph: 1.41.13(three@0.163.0) 51 | three-render-objects: 1.29.3(three@0.163.0) 52 | dev: false 53 | 54 | /@babel/runtime@7.24.4: 55 | resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==} 56 | engines: {node: '>=6.9.0'} 57 | dependencies: 58 | regenerator-runtime: 0.14.1 59 | dev: false 60 | 61 | /@biomejs/biome@1.7.0: 62 | resolution: {integrity: sha512-mejiRhnAq6UrXtYvjWJUKdstcT58n0/FfKemFf3d2Ou0HxOdS88HQmWtQ/UgyZvOEPD572YbFTb6IheyROpqkw==} 63 | engines: {node: '>=14.21.3'} 64 | hasBin: true 65 | requiresBuild: true 66 | optionalDependencies: 67 | '@biomejs/cli-darwin-arm64': 1.7.0 68 | '@biomejs/cli-darwin-x64': 1.7.0 69 | '@biomejs/cli-linux-arm64': 1.7.0 70 | '@biomejs/cli-linux-arm64-musl': 1.7.0 71 | '@biomejs/cli-linux-x64': 1.7.0 72 | '@biomejs/cli-linux-x64-musl': 1.7.0 73 | '@biomejs/cli-win32-arm64': 1.7.0 74 | '@biomejs/cli-win32-x64': 1.7.0 75 | dev: true 76 | 77 | /@biomejs/cli-darwin-arm64@1.7.0: 78 | resolution: {integrity: sha512-12TaeaKHU4SAZt0fQJ2bYk1jUb4foope7LmgDE5p3c0uMxd3mFkg1k7G721T+K6UHYULcSOQDsNNM8DhYi8Irg==} 79 | engines: {node: '>=14.21.3'} 80 | cpu: [arm64] 81 | os: [darwin] 82 | requiresBuild: true 83 | dev: true 84 | optional: true 85 | 86 | /@biomejs/cli-darwin-x64@1.7.0: 87 | resolution: {integrity: sha512-6Qq1BSIB0cpp0cQNqO/+EiUV7FE3jMpF6w7+AgIBXp0oJxUWb2Ff0RDZdO9bfzkimXD58j0vGpNHMGnCcjDV2Q==} 88 | engines: {node: '>=14.21.3'} 89 | cpu: [x64] 90 | os: [darwin] 91 | requiresBuild: true 92 | dev: true 93 | optional: true 94 | 95 | /@biomejs/cli-linux-arm64-musl@1.7.0: 96 | resolution: {integrity: sha512-pwIY80nU7SAxrVVZ6HD9ah1pruwh9ZqlSR0Nvbg4ZJqQa0POhiB+RJx7+/1Ml2mTZdrl8kb/YiwQpD16uwb5wg==} 97 | engines: {node: '>=14.21.3'} 98 | cpu: [arm64] 99 | os: [linux] 100 | requiresBuild: true 101 | dev: true 102 | optional: true 103 | 104 | /@biomejs/cli-linux-arm64@1.7.0: 105 | resolution: {integrity: sha512-GwSci7xBJ2j1CrdDXDUVXnUtrvypEz/xmiYPpFeVdlX5p95eXx+7FekPPbJfhGGw5WKSsKZ+V8AAlbN+kUwJWw==} 106 | engines: {node: '>=14.21.3'} 107 | cpu: [arm64] 108 | os: [linux] 109 | requiresBuild: true 110 | dev: true 111 | optional: true 112 | 113 | /@biomejs/cli-linux-x64-musl@1.7.0: 114 | resolution: {integrity: sha512-KzCA0mW4LSbCd7XZWaEJvTOTTBjfJoVEXkfq1fsXxww1HB+ww5PGMbhbIcbYCsj2CTJUifeD5hOkyuBVppU1xQ==} 115 | engines: {node: '>=14.21.3'} 116 | cpu: [x64] 117 | os: [linux] 118 | requiresBuild: true 119 | dev: true 120 | optional: true 121 | 122 | /@biomejs/cli-linux-x64@1.7.0: 123 | resolution: {integrity: sha512-1y+odKQsyHcw0JCGRuqhbx7Y6jxOVSh4lGIVDdJxW1b55yD22DY1kcMEfhUte6f95OIc2uqfkwtiI6xQAiZJdw==} 124 | engines: {node: '>=14.21.3'} 125 | cpu: [x64] 126 | os: [linux] 127 | requiresBuild: true 128 | dev: true 129 | optional: true 130 | 131 | /@biomejs/cli-win32-arm64@1.7.0: 132 | resolution: {integrity: sha512-AvLDUYZBpOUFgS/mni4VruIoVV3uSGbKSkZQBPXsHgL0w4KttLll3NBrVanmWxOHsom6C6ocHLyfAY8HUc8TXg==} 133 | engines: {node: '>=14.21.3'} 134 | cpu: [arm64] 135 | os: [win32] 136 | requiresBuild: true 137 | dev: true 138 | optional: true 139 | 140 | /@biomejs/cli-win32-x64@1.7.0: 141 | resolution: {integrity: sha512-Pylm00BAAuLVb40IH9PC17432BTsY8K4pSUvhvgR1eaalnMaD6ug9SYJTTzKDbT6r24MPAGCTiSZERyhGkGzFQ==} 142 | engines: {node: '>=14.21.3'} 143 | cpu: [x64] 144 | os: [win32] 145 | requiresBuild: true 146 | dev: true 147 | optional: true 148 | 149 | /@esbuild/aix-ppc64@0.20.2: 150 | resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} 151 | engines: {node: '>=12'} 152 | cpu: [ppc64] 153 | os: [aix] 154 | requiresBuild: true 155 | dev: true 156 | optional: true 157 | 158 | /@esbuild/android-arm64@0.20.2: 159 | resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} 160 | engines: {node: '>=12'} 161 | cpu: [arm64] 162 | os: [android] 163 | requiresBuild: true 164 | dev: true 165 | optional: true 166 | 167 | /@esbuild/android-arm@0.20.2: 168 | resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} 169 | engines: {node: '>=12'} 170 | cpu: [arm] 171 | os: [android] 172 | requiresBuild: true 173 | dev: true 174 | optional: true 175 | 176 | /@esbuild/android-x64@0.20.2: 177 | resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} 178 | engines: {node: '>=12'} 179 | cpu: [x64] 180 | os: [android] 181 | requiresBuild: true 182 | dev: true 183 | optional: true 184 | 185 | /@esbuild/darwin-arm64@0.20.2: 186 | resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} 187 | engines: {node: '>=12'} 188 | cpu: [arm64] 189 | os: [darwin] 190 | requiresBuild: true 191 | dev: true 192 | optional: true 193 | 194 | /@esbuild/darwin-x64@0.20.2: 195 | resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} 196 | engines: {node: '>=12'} 197 | cpu: [x64] 198 | os: [darwin] 199 | requiresBuild: true 200 | dev: true 201 | optional: true 202 | 203 | /@esbuild/freebsd-arm64@0.20.2: 204 | resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} 205 | engines: {node: '>=12'} 206 | cpu: [arm64] 207 | os: [freebsd] 208 | requiresBuild: true 209 | dev: true 210 | optional: true 211 | 212 | /@esbuild/freebsd-x64@0.20.2: 213 | resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} 214 | engines: {node: '>=12'} 215 | cpu: [x64] 216 | os: [freebsd] 217 | requiresBuild: true 218 | dev: true 219 | optional: true 220 | 221 | /@esbuild/linux-arm64@0.20.2: 222 | resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} 223 | engines: {node: '>=12'} 224 | cpu: [arm64] 225 | os: [linux] 226 | requiresBuild: true 227 | dev: true 228 | optional: true 229 | 230 | /@esbuild/linux-arm@0.20.2: 231 | resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} 232 | engines: {node: '>=12'} 233 | cpu: [arm] 234 | os: [linux] 235 | requiresBuild: true 236 | dev: true 237 | optional: true 238 | 239 | /@esbuild/linux-ia32@0.20.2: 240 | resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} 241 | engines: {node: '>=12'} 242 | cpu: [ia32] 243 | os: [linux] 244 | requiresBuild: true 245 | dev: true 246 | optional: true 247 | 248 | /@esbuild/linux-loong64@0.20.2: 249 | resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} 250 | engines: {node: '>=12'} 251 | cpu: [loong64] 252 | os: [linux] 253 | requiresBuild: true 254 | dev: true 255 | optional: true 256 | 257 | /@esbuild/linux-mips64el@0.20.2: 258 | resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} 259 | engines: {node: '>=12'} 260 | cpu: [mips64el] 261 | os: [linux] 262 | requiresBuild: true 263 | dev: true 264 | optional: true 265 | 266 | /@esbuild/linux-ppc64@0.20.2: 267 | resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} 268 | engines: {node: '>=12'} 269 | cpu: [ppc64] 270 | os: [linux] 271 | requiresBuild: true 272 | dev: true 273 | optional: true 274 | 275 | /@esbuild/linux-riscv64@0.20.2: 276 | resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} 277 | engines: {node: '>=12'} 278 | cpu: [riscv64] 279 | os: [linux] 280 | requiresBuild: true 281 | dev: true 282 | optional: true 283 | 284 | /@esbuild/linux-s390x@0.20.2: 285 | resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} 286 | engines: {node: '>=12'} 287 | cpu: [s390x] 288 | os: [linux] 289 | requiresBuild: true 290 | dev: true 291 | optional: true 292 | 293 | /@esbuild/linux-x64@0.20.2: 294 | resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} 295 | engines: {node: '>=12'} 296 | cpu: [x64] 297 | os: [linux] 298 | requiresBuild: true 299 | dev: true 300 | optional: true 301 | 302 | /@esbuild/netbsd-x64@0.20.2: 303 | resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} 304 | engines: {node: '>=12'} 305 | cpu: [x64] 306 | os: [netbsd] 307 | requiresBuild: true 308 | dev: true 309 | optional: true 310 | 311 | /@esbuild/openbsd-x64@0.20.2: 312 | resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} 313 | engines: {node: '>=12'} 314 | cpu: [x64] 315 | os: [openbsd] 316 | requiresBuild: true 317 | dev: true 318 | optional: true 319 | 320 | /@esbuild/sunos-x64@0.20.2: 321 | resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} 322 | engines: {node: '>=12'} 323 | cpu: [x64] 324 | os: [sunos] 325 | requiresBuild: true 326 | dev: true 327 | optional: true 328 | 329 | /@esbuild/win32-arm64@0.20.2: 330 | resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} 331 | engines: {node: '>=12'} 332 | cpu: [arm64] 333 | os: [win32] 334 | requiresBuild: true 335 | dev: true 336 | optional: true 337 | 338 | /@esbuild/win32-ia32@0.20.2: 339 | resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} 340 | engines: {node: '>=12'} 341 | cpu: [ia32] 342 | os: [win32] 343 | requiresBuild: true 344 | dev: true 345 | optional: true 346 | 347 | /@esbuild/win32-x64@0.20.2: 348 | resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} 349 | engines: {node: '>=12'} 350 | cpu: [x64] 351 | os: [win32] 352 | requiresBuild: true 353 | dev: true 354 | optional: true 355 | 356 | /@rollup/rollup-android-arm-eabi@4.14.3: 357 | resolution: {integrity: sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==} 358 | cpu: [arm] 359 | os: [android] 360 | requiresBuild: true 361 | dev: true 362 | optional: true 363 | 364 | /@rollup/rollup-android-arm64@4.14.3: 365 | resolution: {integrity: sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==} 366 | cpu: [arm64] 367 | os: [android] 368 | requiresBuild: true 369 | dev: true 370 | optional: true 371 | 372 | /@rollup/rollup-darwin-arm64@4.14.3: 373 | resolution: {integrity: sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==} 374 | cpu: [arm64] 375 | os: [darwin] 376 | requiresBuild: true 377 | dev: true 378 | optional: true 379 | 380 | /@rollup/rollup-darwin-x64@4.14.3: 381 | resolution: {integrity: sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==} 382 | cpu: [x64] 383 | os: [darwin] 384 | requiresBuild: true 385 | dev: true 386 | optional: true 387 | 388 | /@rollup/rollup-linux-arm-gnueabihf@4.14.3: 389 | resolution: {integrity: sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==} 390 | cpu: [arm] 391 | os: [linux] 392 | requiresBuild: true 393 | dev: true 394 | optional: true 395 | 396 | /@rollup/rollup-linux-arm-musleabihf@4.14.3: 397 | resolution: {integrity: sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==} 398 | cpu: [arm] 399 | os: [linux] 400 | requiresBuild: true 401 | dev: true 402 | optional: true 403 | 404 | /@rollup/rollup-linux-arm64-gnu@4.14.3: 405 | resolution: {integrity: sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==} 406 | cpu: [arm64] 407 | os: [linux] 408 | requiresBuild: true 409 | dev: true 410 | optional: true 411 | 412 | /@rollup/rollup-linux-arm64-musl@4.14.3: 413 | resolution: {integrity: sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==} 414 | cpu: [arm64] 415 | os: [linux] 416 | requiresBuild: true 417 | dev: true 418 | optional: true 419 | 420 | /@rollup/rollup-linux-powerpc64le-gnu@4.14.3: 421 | resolution: {integrity: sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==} 422 | cpu: [ppc64] 423 | os: [linux] 424 | requiresBuild: true 425 | dev: true 426 | optional: true 427 | 428 | /@rollup/rollup-linux-riscv64-gnu@4.14.3: 429 | resolution: {integrity: sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==} 430 | cpu: [riscv64] 431 | os: [linux] 432 | requiresBuild: true 433 | dev: true 434 | optional: true 435 | 436 | /@rollup/rollup-linux-s390x-gnu@4.14.3: 437 | resolution: {integrity: sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==} 438 | cpu: [s390x] 439 | os: [linux] 440 | requiresBuild: true 441 | dev: true 442 | optional: true 443 | 444 | /@rollup/rollup-linux-x64-gnu@4.14.3: 445 | resolution: {integrity: sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==} 446 | cpu: [x64] 447 | os: [linux] 448 | requiresBuild: true 449 | dev: true 450 | optional: true 451 | 452 | /@rollup/rollup-linux-x64-musl@4.14.3: 453 | resolution: {integrity: sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==} 454 | cpu: [x64] 455 | os: [linux] 456 | requiresBuild: true 457 | dev: true 458 | optional: true 459 | 460 | /@rollup/rollup-win32-arm64-msvc@4.14.3: 461 | resolution: {integrity: sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==} 462 | cpu: [arm64] 463 | os: [win32] 464 | requiresBuild: true 465 | dev: true 466 | optional: true 467 | 468 | /@rollup/rollup-win32-ia32-msvc@4.14.3: 469 | resolution: {integrity: sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==} 470 | cpu: [ia32] 471 | os: [win32] 472 | requiresBuild: true 473 | dev: true 474 | optional: true 475 | 476 | /@rollup/rollup-win32-x64-msvc@4.14.3: 477 | resolution: {integrity: sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==} 478 | cpu: [x64] 479 | os: [win32] 480 | requiresBuild: true 481 | dev: true 482 | optional: true 483 | 484 | /@swc/core-darwin-arm64@1.4.14: 485 | resolution: {integrity: sha512-8iPfLhYNspBl836YYsfv6ErXwDUqJ7IMieddV3Ey/t/97JAEAdNDUdtTKDtbyP0j/Ebyqyn+fKcqwSq7rAof0g==} 486 | engines: {node: '>=10'} 487 | cpu: [arm64] 488 | os: [darwin] 489 | requiresBuild: true 490 | dev: true 491 | optional: true 492 | 493 | /@swc/core-darwin-x64@1.4.14: 494 | resolution: {integrity: sha512-9CqSj8uRZ92cnlgAlVaWMaJJBdxtNvCzJxaGj5KuIseeG6Q0l1g+qk8JcU7h9dAsH9saHTNwNFBVGKQo0W0ujg==} 495 | engines: {node: '>=10'} 496 | cpu: [x64] 497 | os: [darwin] 498 | requiresBuild: true 499 | dev: true 500 | optional: true 501 | 502 | /@swc/core-linux-arm-gnueabihf@1.4.14: 503 | resolution: {integrity: sha512-mfd5JArPITTzMjcezH4DwMw+BdjBV1y25Khp8itEIpdih9ei+fvxOOrDYTN08b466NuE2dF2XuhKtRLA7fXArQ==} 504 | engines: {node: '>=10'} 505 | cpu: [arm] 506 | os: [linux] 507 | requiresBuild: true 508 | dev: true 509 | optional: true 510 | 511 | /@swc/core-linux-arm64-gnu@1.4.14: 512 | resolution: {integrity: sha512-3Lqlhlmy8MVRS9xTShMaPAp0oyUt0KFhDs4ixJsjdxKecE0NJSV/MInuDmrkij1C8/RQ2wySRlV9np5jK86oWw==} 513 | engines: {node: '>=10'} 514 | cpu: [arm64] 515 | os: [linux] 516 | requiresBuild: true 517 | dev: true 518 | optional: true 519 | 520 | /@swc/core-linux-arm64-musl@1.4.14: 521 | resolution: {integrity: sha512-n0YoCa64TUcJrbcXIHIHDWQjdUPdaXeMHNEu7yyBtOpm01oMGTKP3frsUXIABLBmAVWtKvqit4/W1KVKn5gJzg==} 522 | engines: {node: '>=10'} 523 | cpu: [arm64] 524 | os: [linux] 525 | requiresBuild: true 526 | dev: true 527 | optional: true 528 | 529 | /@swc/core-linux-x64-gnu@1.4.14: 530 | resolution: {integrity: sha512-CGmlwLWbfG1dB4jZBJnp2IWlK5xBMNLjN7AR5kKA3sEpionoccEnChOEvfux1UdVJQjLRKuHNV9yGyqGBTpxfQ==} 531 | engines: {node: '>=10'} 532 | cpu: [x64] 533 | os: [linux] 534 | requiresBuild: true 535 | dev: true 536 | optional: true 537 | 538 | /@swc/core-linux-x64-musl@1.4.14: 539 | resolution: {integrity: sha512-xq4npk8YKYmNwmr8fbvF2KP3kUVdZYfXZMQnW425gP3/sn+yFQO8Nd0bGH40vOVQn41kEesSe0Z5O/JDor2TgQ==} 540 | engines: {node: '>=10'} 541 | cpu: [x64] 542 | os: [linux] 543 | requiresBuild: true 544 | dev: true 545 | optional: true 546 | 547 | /@swc/core-win32-arm64-msvc@1.4.14: 548 | resolution: {integrity: sha512-imq0X+gU9uUe6FqzOQot5gpKoaC00aCUiN58NOzwp0QXEupn8CDuZpdBN93HiZswfLruu5jA1tsc15x6v9p0Yg==} 549 | engines: {node: '>=10'} 550 | cpu: [arm64] 551 | os: [win32] 552 | requiresBuild: true 553 | dev: true 554 | optional: true 555 | 556 | /@swc/core-win32-ia32-msvc@1.4.14: 557 | resolution: {integrity: sha512-cH6QpXMw5D3t+lpx6SkErHrxN0yFzmQ0lgNAJxoDRiaAdDbqA6Col8UqUJwUS++Ul6aCWgNhCdiEYehPaoyDPA==} 558 | engines: {node: '>=10'} 559 | cpu: [ia32] 560 | os: [win32] 561 | requiresBuild: true 562 | dev: true 563 | optional: true 564 | 565 | /@swc/core-win32-x64-msvc@1.4.14: 566 | resolution: {integrity: sha512-FmZ4Tby4wW65K/36BKzmuu7mlq7cW5XOxzvufaSNVvQ5PN4OodAlqPjToe029oma4Av+ykJiif64scMttyNAzg==} 567 | engines: {node: '>=10'} 568 | cpu: [x64] 569 | os: [win32] 570 | requiresBuild: true 571 | dev: true 572 | optional: true 573 | 574 | /@swc/core@1.4.14: 575 | resolution: {integrity: sha512-tHXg6OxboUsqa/L7DpsCcFnxhLkqN/ht5pCwav1HnvfthbiNIJypr86rNx4cUnQDJepETviSqBTIjxa7pSpGDQ==} 576 | engines: {node: '>=10'} 577 | requiresBuild: true 578 | peerDependencies: 579 | '@swc/helpers': ^0.5.0 580 | peerDependenciesMeta: 581 | '@swc/helpers': 582 | optional: true 583 | dependencies: 584 | '@swc/counter': 0.1.3 585 | '@swc/types': 0.1.6 586 | optionalDependencies: 587 | '@swc/core-darwin-arm64': 1.4.14 588 | '@swc/core-darwin-x64': 1.4.14 589 | '@swc/core-linux-arm-gnueabihf': 1.4.14 590 | '@swc/core-linux-arm64-gnu': 1.4.14 591 | '@swc/core-linux-arm64-musl': 1.4.14 592 | '@swc/core-linux-x64-gnu': 1.4.14 593 | '@swc/core-linux-x64-musl': 1.4.14 594 | '@swc/core-win32-arm64-msvc': 1.4.14 595 | '@swc/core-win32-ia32-msvc': 1.4.14 596 | '@swc/core-win32-x64-msvc': 1.4.14 597 | dev: true 598 | 599 | /@swc/counter@0.1.3: 600 | resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} 601 | dev: true 602 | 603 | /@swc/types@0.1.6: 604 | resolution: {integrity: sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==} 605 | dependencies: 606 | '@swc/counter': 0.1.3 607 | dev: true 608 | 609 | /@tweenjs/tween.js@23.1.1: 610 | resolution: {integrity: sha512-ZpboH7pCPPeyBWKf8c7TJswtCEQObFo3bOBYalm99NzZarATALYCo5OhbCa/n4RQyJyHfhkdx+hNrdL5ByFYDw==} 611 | dev: false 612 | 613 | /@types/estree@1.0.5: 614 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 615 | dev: true 616 | 617 | /@types/prop-types@15.7.12: 618 | resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} 619 | dev: true 620 | 621 | /@types/react-dom@18.2.25: 622 | resolution: {integrity: sha512-o/V48vf4MQh7juIKZU2QGDfli6p1+OOi5oXx36Hffpc9adsHeXjVp8rHuPkjd8VT8sOJ2Zp05HR7CdpGTIUFUA==} 623 | dependencies: 624 | '@types/react': 18.2.79 625 | dev: true 626 | 627 | /@types/react@18.2.79: 628 | resolution: {integrity: sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==} 629 | dependencies: 630 | '@types/prop-types': 15.7.12 631 | csstype: 3.1.3 632 | dev: true 633 | 634 | /@vitejs/plugin-react-swc@3.6.0(vite@5.2.8): 635 | resolution: {integrity: sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==} 636 | peerDependencies: 637 | vite: ^4 || ^5 638 | dependencies: 639 | '@swc/core': 1.4.14 640 | vite: 5.2.8 641 | transitivePeerDependencies: 642 | - '@swc/helpers' 643 | dev: true 644 | 645 | /accessor-fn@1.5.0: 646 | resolution: {integrity: sha512-dml7D96DY/K5lt4Ra2jMnpL9Bhw5HEGws4p1OAIxFFj9Utd/RxNfEO3T3f0QIWFNwQU7gNxH9snUfqF/zNkP/w==} 647 | engines: {node: '>=12'} 648 | dev: false 649 | 650 | /bezier-js@6.1.4: 651 | resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==} 652 | dev: false 653 | 654 | /canvas-color-tracker@1.2.1: 655 | resolution: {integrity: sha512-i5clg2pEdaWqHuEM/B74NZNLkHh5+OkXbA/T4iaBiaNDagkOCXkLNrhqUfdUugsRwuaNRU20e/OygzxWRor3yg==} 656 | engines: {node: '>=12'} 657 | dependencies: 658 | tinycolor2: 1.6.0 659 | dev: false 660 | 661 | /csstype@3.1.3: 662 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 663 | dev: true 664 | 665 | /d3-array@3.2.4: 666 | resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} 667 | engines: {node: '>=12'} 668 | dependencies: 669 | internmap: 2.0.3 670 | dev: false 671 | 672 | /d3-binarytree@1.0.2: 673 | resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} 674 | dev: false 675 | 676 | /d3-color@3.1.0: 677 | resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} 678 | engines: {node: '>=12'} 679 | dev: false 680 | 681 | /d3-dispatch@3.0.1: 682 | resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} 683 | engines: {node: '>=12'} 684 | dev: false 685 | 686 | /d3-drag@3.0.0: 687 | resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} 688 | engines: {node: '>=12'} 689 | dependencies: 690 | d3-dispatch: 3.0.1 691 | d3-selection: 3.0.0 692 | dev: false 693 | 694 | /d3-ease@3.0.1: 695 | resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} 696 | engines: {node: '>=12'} 697 | dev: false 698 | 699 | /d3-force-3d@3.0.5: 700 | resolution: {integrity: sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==} 701 | engines: {node: '>=12'} 702 | dependencies: 703 | d3-binarytree: 1.0.2 704 | d3-dispatch: 3.0.1 705 | d3-octree: 1.0.2 706 | d3-quadtree: 3.0.1 707 | d3-timer: 3.0.1 708 | dev: false 709 | 710 | /d3-format@3.1.0: 711 | resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} 712 | engines: {node: '>=12'} 713 | dev: false 714 | 715 | /d3-interpolate@3.0.1: 716 | resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} 717 | engines: {node: '>=12'} 718 | dependencies: 719 | d3-color: 3.1.0 720 | dev: false 721 | 722 | /d3-octree@1.0.2: 723 | resolution: {integrity: sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==} 724 | dev: false 725 | 726 | /d3-quadtree@3.0.1: 727 | resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} 728 | engines: {node: '>=12'} 729 | dev: false 730 | 731 | /d3-scale-chromatic@3.1.0: 732 | resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} 733 | engines: {node: '>=12'} 734 | dependencies: 735 | d3-color: 3.1.0 736 | d3-interpolate: 3.0.1 737 | dev: false 738 | 739 | /d3-scale@4.0.2: 740 | resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} 741 | engines: {node: '>=12'} 742 | dependencies: 743 | d3-array: 3.2.4 744 | d3-format: 3.1.0 745 | d3-interpolate: 3.0.1 746 | d3-time: 3.1.0 747 | d3-time-format: 4.1.0 748 | dev: false 749 | 750 | /d3-selection@3.0.0: 751 | resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} 752 | engines: {node: '>=12'} 753 | dev: false 754 | 755 | /d3-time-format@4.1.0: 756 | resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} 757 | engines: {node: '>=12'} 758 | dependencies: 759 | d3-time: 3.1.0 760 | dev: false 761 | 762 | /d3-time@3.1.0: 763 | resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} 764 | engines: {node: '>=12'} 765 | dependencies: 766 | d3-array: 3.2.4 767 | dev: false 768 | 769 | /d3-timer@3.0.1: 770 | resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} 771 | engines: {node: '>=12'} 772 | dev: false 773 | 774 | /d3-transition@3.0.1(d3-selection@3.0.0): 775 | resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} 776 | engines: {node: '>=12'} 777 | peerDependencies: 778 | d3-selection: 2 - 3 779 | dependencies: 780 | d3-color: 3.1.0 781 | d3-dispatch: 3.0.1 782 | d3-ease: 3.0.1 783 | d3-interpolate: 3.0.1 784 | d3-selection: 3.0.0 785 | d3-timer: 3.0.1 786 | dev: false 787 | 788 | /d3-zoom@3.0.0: 789 | resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} 790 | engines: {node: '>=12'} 791 | dependencies: 792 | d3-dispatch: 3.0.1 793 | d3-drag: 3.0.0 794 | d3-interpolate: 3.0.1 795 | d3-selection: 3.0.0 796 | d3-transition: 3.0.1(d3-selection@3.0.0) 797 | dev: false 798 | 799 | /data-joint@1.3.1: 800 | resolution: {integrity: sha512-tMK0m4OVGqiA3zkn8JmO6YAqD8UwJqIAx4AAwFl1SKTtKAqcXePuT+n2aayiX9uITtlN3DFtKKTOxJRUc2+HvQ==} 801 | engines: {node: '>=12'} 802 | dependencies: 803 | index-array-by: 1.4.1 804 | dev: false 805 | 806 | /esbuild@0.20.2: 807 | resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} 808 | engines: {node: '>=12'} 809 | hasBin: true 810 | requiresBuild: true 811 | optionalDependencies: 812 | '@esbuild/aix-ppc64': 0.20.2 813 | '@esbuild/android-arm': 0.20.2 814 | '@esbuild/android-arm64': 0.20.2 815 | '@esbuild/android-x64': 0.20.2 816 | '@esbuild/darwin-arm64': 0.20.2 817 | '@esbuild/darwin-x64': 0.20.2 818 | '@esbuild/freebsd-arm64': 0.20.2 819 | '@esbuild/freebsd-x64': 0.20.2 820 | '@esbuild/linux-arm': 0.20.2 821 | '@esbuild/linux-arm64': 0.20.2 822 | '@esbuild/linux-ia32': 0.20.2 823 | '@esbuild/linux-loong64': 0.20.2 824 | '@esbuild/linux-mips64el': 0.20.2 825 | '@esbuild/linux-ppc64': 0.20.2 826 | '@esbuild/linux-riscv64': 0.20.2 827 | '@esbuild/linux-s390x': 0.20.2 828 | '@esbuild/linux-x64': 0.20.2 829 | '@esbuild/netbsd-x64': 0.20.2 830 | '@esbuild/openbsd-x64': 0.20.2 831 | '@esbuild/sunos-x64': 0.20.2 832 | '@esbuild/win32-arm64': 0.20.2 833 | '@esbuild/win32-ia32': 0.20.2 834 | '@esbuild/win32-x64': 0.20.2 835 | dev: true 836 | 837 | /force-graph@1.43.5: 838 | resolution: {integrity: sha512-HveLELh9yhZXO/QOfaFS38vlwJZ/3sKu+jarfXzRmbmihSOH/BbRWnUvmg8wLFiYy6h4HlH4lkRfZRccHYmXgA==} 839 | engines: {node: '>=12'} 840 | dependencies: 841 | '@tweenjs/tween.js': 23.1.1 842 | accessor-fn: 1.5.0 843 | bezier-js: 6.1.4 844 | canvas-color-tracker: 1.2.1 845 | d3-array: 3.2.4 846 | d3-drag: 3.0.0 847 | d3-force-3d: 3.0.5 848 | d3-scale: 4.0.2 849 | d3-scale-chromatic: 3.1.0 850 | d3-selection: 3.0.0 851 | d3-zoom: 3.0.0 852 | index-array-by: 1.4.1 853 | kapsule: 1.14.5 854 | lodash-es: 4.17.21 855 | dev: false 856 | 857 | /fromentries@1.3.2: 858 | resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} 859 | dev: false 860 | 861 | /fsevents@2.3.3: 862 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 863 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 864 | os: [darwin] 865 | requiresBuild: true 866 | dev: true 867 | optional: true 868 | 869 | /index-array-by@1.4.1: 870 | resolution: {integrity: sha512-Zu6THdrxQdyTuT2uA5FjUoBEsFHPzHcPIj18FszN6yXKHxSfGcR4TPLabfuT//E25q1Igyx9xta2WMvD/x9P/g==} 871 | engines: {node: '>=12'} 872 | dev: false 873 | 874 | /internmap@2.0.3: 875 | resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} 876 | engines: {node: '>=12'} 877 | dev: false 878 | 879 | /jerrypick@1.1.1: 880 | resolution: {integrity: sha512-XTtedPYEyVp4t6hJrXuRKr/jHj8SC4z+4K0b396PMkov6muL+i8IIamJIvZWe3jUspgIJak0P+BaWKawMYNBLg==} 881 | engines: {node: '>=12'} 882 | dev: false 883 | 884 | /js-tokens@4.0.0: 885 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 886 | dev: false 887 | 888 | /kapsule@1.14.5: 889 | resolution: {integrity: sha512-H0iSpTynUzZw3tgraDmReprpFRmH5oP5GPmaNsurSwLx2H5iCpOMIkp5q+sfhB4Tz/UJd1E1IbEE9Z6ksnJ6RA==} 890 | engines: {node: '>=12'} 891 | dependencies: 892 | lodash-es: 4.17.21 893 | dev: false 894 | 895 | /lodash-es@4.17.21: 896 | resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} 897 | dev: false 898 | 899 | /loose-envify@1.4.0: 900 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 901 | hasBin: true 902 | dependencies: 903 | js-tokens: 4.0.0 904 | dev: false 905 | 906 | /nanoid@3.3.7: 907 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 908 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 909 | hasBin: true 910 | dev: true 911 | 912 | /ngraph.events@1.2.2: 913 | resolution: {integrity: sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==} 914 | dev: false 915 | 916 | /ngraph.forcelayout@3.3.1: 917 | resolution: {integrity: sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==} 918 | dependencies: 919 | ngraph.events: 1.2.2 920 | ngraph.merge: 1.0.0 921 | ngraph.random: 1.1.0 922 | dev: false 923 | 924 | /ngraph.graph@20.0.1: 925 | resolution: {integrity: sha512-VFsQ+EMkT+7lcJO1QP8Ik3w64WbHJl27Q53EO9hiFU9CRyxJ8HfcXtfWz/U8okuoYKDctbciL6pX3vG5dt1rYA==} 926 | dependencies: 927 | ngraph.events: 1.2.2 928 | dev: false 929 | 930 | /ngraph.merge@1.0.0: 931 | resolution: {integrity: sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==} 932 | dev: false 933 | 934 | /ngraph.random@1.1.0: 935 | resolution: {integrity: sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==} 936 | dev: false 937 | 938 | /object-assign@4.1.1: 939 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 940 | engines: {node: '>=0.10.0'} 941 | dev: false 942 | 943 | /picocolors@1.0.0: 944 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 945 | dev: true 946 | 947 | /polished@4.3.1: 948 | resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} 949 | engines: {node: '>=10'} 950 | dependencies: 951 | '@babel/runtime': 7.24.4 952 | dev: false 953 | 954 | /postcss@8.4.38: 955 | resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} 956 | engines: {node: ^10 || ^12 || >=14} 957 | dependencies: 958 | nanoid: 3.3.7 959 | picocolors: 1.0.0 960 | source-map-js: 1.2.0 961 | dev: true 962 | 963 | /prop-types@15.8.1: 964 | resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} 965 | dependencies: 966 | loose-envify: 1.4.0 967 | object-assign: 4.1.1 968 | react-is: 16.13.1 969 | dev: false 970 | 971 | /react-dom@18.2.0(react@18.2.0): 972 | resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} 973 | peerDependencies: 974 | react: ^18.2.0 975 | dependencies: 976 | loose-envify: 1.4.0 977 | react: 18.2.0 978 | scheduler: 0.23.0 979 | dev: false 980 | 981 | /react-force-graph-2d@1.25.4(react@18.2.0): 982 | resolution: {integrity: sha512-Y1xwa79PHVZUedfa/TO+Cboq2hIc1flA1z4o1oraOu6qMS0r421vNpfjWhJPR6qJonNme3tzeVt5boEA7Ue8sg==} 983 | engines: {node: '>=12'} 984 | peerDependencies: 985 | react: '*' 986 | dependencies: 987 | force-graph: 1.43.5 988 | prop-types: 15.8.1 989 | react: 18.2.0 990 | react-kapsule: 2.4.0(react@18.2.0) 991 | dev: false 992 | 993 | /react-force-graph-3d@1.24.2(react@18.2.0): 994 | resolution: {integrity: sha512-/tZ0BywYuj35Q84AH2WN+Cx0RIygnN5F1+EvsdAqsAMoIJ0xl4L/9aD/pwjCoWfFqi3w5wR2DQuitDXeTayZnQ==} 995 | engines: {node: '>=12'} 996 | peerDependencies: 997 | react: '*' 998 | dependencies: 999 | 3d-force-graph: 1.73.3 1000 | prop-types: 15.8.1 1001 | react: 18.2.0 1002 | react-kapsule: 2.4.0(react@18.2.0) 1003 | dev: false 1004 | 1005 | /react-is@16.13.1: 1006 | resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} 1007 | dev: false 1008 | 1009 | /react-kapsule@2.4.0(react@18.2.0): 1010 | resolution: {integrity: sha512-w4Yv9CgWdj8kWGQEPNWFGJJ08dYEZHZpiaFR/DgZjCMBNqv9wus2Gy1qvHVJmJbzvAZbq6jdvFC+NYzEqAlNhQ==} 1011 | engines: {node: '>=12'} 1012 | peerDependencies: 1013 | react: '>=16.13.1' 1014 | dependencies: 1015 | fromentries: 1.3.2 1016 | jerrypick: 1.1.1 1017 | react: 18.2.0 1018 | dev: false 1019 | 1020 | /react@18.2.0: 1021 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 1022 | engines: {node: '>=0.10.0'} 1023 | dependencies: 1024 | loose-envify: 1.4.0 1025 | dev: false 1026 | 1027 | /regenerator-runtime@0.14.1: 1028 | resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} 1029 | dev: false 1030 | 1031 | /rollup@4.14.3: 1032 | resolution: {integrity: sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==} 1033 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1034 | hasBin: true 1035 | dependencies: 1036 | '@types/estree': 1.0.5 1037 | optionalDependencies: 1038 | '@rollup/rollup-android-arm-eabi': 4.14.3 1039 | '@rollup/rollup-android-arm64': 4.14.3 1040 | '@rollup/rollup-darwin-arm64': 4.14.3 1041 | '@rollup/rollup-darwin-x64': 4.14.3 1042 | '@rollup/rollup-linux-arm-gnueabihf': 4.14.3 1043 | '@rollup/rollup-linux-arm-musleabihf': 4.14.3 1044 | '@rollup/rollup-linux-arm64-gnu': 4.14.3 1045 | '@rollup/rollup-linux-arm64-musl': 4.14.3 1046 | '@rollup/rollup-linux-powerpc64le-gnu': 4.14.3 1047 | '@rollup/rollup-linux-riscv64-gnu': 4.14.3 1048 | '@rollup/rollup-linux-s390x-gnu': 4.14.3 1049 | '@rollup/rollup-linux-x64-gnu': 4.14.3 1050 | '@rollup/rollup-linux-x64-musl': 4.14.3 1051 | '@rollup/rollup-win32-arm64-msvc': 4.14.3 1052 | '@rollup/rollup-win32-ia32-msvc': 4.14.3 1053 | '@rollup/rollup-win32-x64-msvc': 4.14.3 1054 | fsevents: 2.3.3 1055 | dev: true 1056 | 1057 | /scheduler@0.23.0: 1058 | resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} 1059 | dependencies: 1060 | loose-envify: 1.4.0 1061 | dev: false 1062 | 1063 | /source-map-js@1.2.0: 1064 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 1065 | engines: {node: '>=0.10.0'} 1066 | dev: true 1067 | 1068 | /three-forcegraph@1.41.13(three@0.163.0): 1069 | resolution: {integrity: sha512-tVBEnGSf0H5bL5dnebANFsjgLUDrwXXfYRXv3RfPgzuymDoo7sJRPdWIPyrkEgN0e09Hzvr4RLXkQ5FUlWpUzw==} 1070 | engines: {node: '>=12'} 1071 | peerDependencies: 1072 | three: '>=0.118.3' 1073 | dependencies: 1074 | accessor-fn: 1.5.0 1075 | d3-array: 3.2.4 1076 | d3-force-3d: 3.0.5 1077 | d3-scale: 4.0.2 1078 | d3-scale-chromatic: 3.1.0 1079 | data-joint: 1.3.1 1080 | kapsule: 1.14.5 1081 | ngraph.forcelayout: 3.3.1 1082 | ngraph.graph: 20.0.1 1083 | three: 0.163.0 1084 | tinycolor2: 1.6.0 1085 | dev: false 1086 | 1087 | /three-render-objects@1.29.3(three@0.163.0): 1088 | resolution: {integrity: sha512-CVE1w0ZvEyW3eMskmcdvZrTiCJZWeZv1BlZQ1X/FIRm0dGLfY/flzg+VH3vPYDXQQdumzj5nSqj5cSQuH4Y39g==} 1089 | engines: {node: '>=12'} 1090 | peerDependencies: 1091 | three: '*' 1092 | dependencies: 1093 | '@tweenjs/tween.js': 23.1.1 1094 | accessor-fn: 1.5.0 1095 | kapsule: 1.14.5 1096 | polished: 4.3.1 1097 | three: 0.163.0 1098 | dev: false 1099 | 1100 | /three@0.163.0: 1101 | resolution: {integrity: sha512-HlMgCb2TF/dTLRtknBnjUTsR8FsDqBY43itYop2+Zg822I+Kd0Ua2vs8CvfBVefXkBdNDrLMoRTGCIIpfCuDew==} 1102 | dev: false 1103 | 1104 | /tinycolor2@1.6.0: 1105 | resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} 1106 | dev: false 1107 | 1108 | /typescript@5.4.5: 1109 | resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} 1110 | engines: {node: '>=14.17'} 1111 | hasBin: true 1112 | dev: true 1113 | 1114 | /vite@5.2.8: 1115 | resolution: {integrity: sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==} 1116 | engines: {node: ^18.0.0 || >=20.0.0} 1117 | hasBin: true 1118 | peerDependencies: 1119 | '@types/node': ^18.0.0 || >=20.0.0 1120 | less: '*' 1121 | lightningcss: ^1.21.0 1122 | sass: '*' 1123 | stylus: '*' 1124 | sugarss: '*' 1125 | terser: ^5.4.0 1126 | peerDependenciesMeta: 1127 | '@types/node': 1128 | optional: true 1129 | less: 1130 | optional: true 1131 | lightningcss: 1132 | optional: true 1133 | sass: 1134 | optional: true 1135 | stylus: 1136 | optional: true 1137 | sugarss: 1138 | optional: true 1139 | terser: 1140 | optional: true 1141 | dependencies: 1142 | esbuild: 0.20.2 1143 | postcss: 8.4.38 1144 | rollup: 4.14.3 1145 | optionalDependencies: 1146 | fsevents: 2.3.3 1147 | dev: true 1148 | -------------------------------------------------------------------------------- /public/happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benricheson101/discord-friend-graph/ba3ff4ab93ead288423cb08ff7fa18b2930fe801/public/happy.png -------------------------------------------------------------------------------- /scripts/console-snippet.js: -------------------------------------------------------------------------------- 1 | const getMutualFriendsData = async () => { 2 | console.log( 3 | '%cOnly run this once, it may take a minute or two to finish depending on the size of your friends list', 4 | 'color:skyblue;font-size:15px' 5 | ); 6 | 7 | let wpRequire; 8 | webpackChunkdiscord_app.push([[Date.now()], {}, e => (wpRequire = e)]); 9 | const mods = Object.values(wpRequire.c); 10 | const allExports = mods 11 | .flatMap( 12 | c => 13 | (c.exports && 14 | !c.exports[Symbol.toStringTag]?.includes('DOM') && 15 | Object.values(c.exports)) || 16 | [] 17 | ) 18 | .filter(Boolean); 19 | 20 | const _hasEveryProp = (a, props) => 21 | a && typeof a === 'object' && props.every(p => p in a); 22 | const findByProps = (...props) => 23 | allExports.find(e => _hasEveryProp(e, props)) || 24 | mods.find(m => m?.exports && _hasEveryProp(m.exports, props))?.exports; 25 | 26 | const stores = mods 27 | .find(m => m?.exports?.ZP?.Store) 28 | .exports.ZP.Store.getAll() 29 | .reduce((a, c) => ((a[c.getName()] = c), a), {}); 30 | const HTTP = findByProps('get', 'put', 'patch'); 31 | const Endpoints = findByProps('USER_RELATIONSHIPS'); 32 | 33 | const {have, need} = stores.RelationshipStore.getFriendIDs().reduce( 34 | (a, f) => { 35 | const mutualFriends = stores.UserProfileStore.getMutualFriends(f)?.map( 36 | m => m.user.id 37 | ); 38 | const user = stores.UserStore.getUser(f); 39 | if (mutualFriends) { 40 | a.have.push({ 41 | id: f, 42 | username: user.username, 43 | avatar: user.avatar, 44 | mutualFriends, 45 | }); 46 | } else { 47 | a.need.push(f); 48 | } 49 | return a; 50 | }, 51 | {have: [], need: []} 52 | ); 53 | 54 | let i = 0; 55 | for (const r of need) { 56 | await new Promise(r => setTimeout(r, 450)); 57 | const {body} = await HTTP.get({ 58 | url: Endpoints.USER_RELATIONSHIPS(r), 59 | }); 60 | 61 | const user = stores.UserStore.getUser(r); 62 | const o = { 63 | id: r, 64 | username: user.username, 65 | avatar: user.avatar, 66 | mutualFriends: body.map(b => b.id), 67 | }; 68 | console.log(`[${i++}/${need.length}]`, r, o); 69 | have.push(o); 70 | } 71 | 72 | console.log('finished'); 73 | const user = stores.UserStore.getCurrentUser(); 74 | return { 75 | id: user.id, 76 | avatar: user.avatar, 77 | username: user.username, 78 | friends: have, 79 | }; 80 | }; 81 | 82 | const c = copy; 83 | const friends = await getMutualFriendsData(); 84 | try { 85 | c(JSON.stringify(friends)); 86 | } catch (e) { 87 | console.error(e); 88 | } 89 | 90 | console.log('%c' + JSON.stringify(friends), 'color:skyblue'); 91 | console.log( 92 | "%cFriends list data should be copied to your clipboard. If not, manually copy the wall of text above (there's a copy button at the end)", 93 | 'color:red;font-size:20px' 94 | ); 95 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type ChangeEventHandler, 3 | type MouseEventHandler, 4 | useEffect, 5 | useRef, 6 | useState, 7 | } from 'react'; 8 | import ForceGraph2D from 'react-force-graph-2d'; 9 | import ForceGraph3D from 'react-force-graph-3d'; 10 | 11 | interface User { 12 | id: string; 13 | username: string; 14 | avatar: string | null; 15 | mutualFriends: string[]; 16 | } 17 | 18 | interface Friends { 19 | id: string; 20 | username: string; 21 | avatar: string | null; 22 | friends: User[]; 23 | } 24 | 25 | interface Node { 26 | id: string; 27 | label: string; 28 | avatar: string | null; 29 | edges: Set; 30 | } 31 | 32 | interface Edge { 33 | source: string; 34 | target: string; 35 | id: string; 36 | color: string; 37 | } 38 | 39 | type Data = {nodes: Node[]; links: Edge[]}; 40 | 41 | type DataWithUser = Data & {id: string}; 42 | 43 | const hash = (str: string) => { 44 | let hash = 0; 45 | for (let i = 0, len = str.length; i < len; i++) { 46 | const chr = str.charCodeAt(i); 47 | hash = (hash << 5) - hash + chr; 48 | hash |= 0; 49 | } 50 | return Math.abs(hash); 51 | }; 52 | 53 | // colors borrowed from https://raw.githubusercontent.com/tailwindlabs/tailwindcss/next/packages/tailwindcss/theme.css 54 | const colors = [ 55 | '#f87171', 56 | '#ef4444', 57 | '#dc2626', 58 | '#b91c1c', 59 | '#fb923c', 60 | '#f97316', 61 | '#ea580c', 62 | '#c2410c', 63 | '#fbbf24', 64 | '#f59e0b', 65 | '#d97706', 66 | '#b45309', 67 | '#facc15', 68 | '#eab308', 69 | '#ca8a04', 70 | '#a16207', 71 | '#a3e635', 72 | '#84cc16', 73 | '#65a30d', 74 | '#4d7c0f', 75 | '#4ade80', 76 | '#22c55e', 77 | '#16a34a', 78 | '#15803d', 79 | '#34d399', 80 | '#10b981', 81 | '#059669', 82 | '#047857', 83 | '#2dd4bf', 84 | '#14b8a6', 85 | '#0d9488', 86 | '#0f766e', 87 | '#22d3ee', 88 | '#06b6d4', 89 | '#0891b2', 90 | '#0e7490', 91 | '#38bdf8', 92 | '#0ea5e9', 93 | '#0284c7', 94 | '#0369a1', 95 | '#60a5fa', 96 | '#3b82f6', 97 | '#2563eb', 98 | '#1d4ed8', 99 | '#818cf8', 100 | '#6366f1', 101 | '#4f46e5', 102 | '#4338ca', 103 | '#a78bfa', 104 | '#8b5cf6', 105 | '#7c3aed', 106 | '#6d28d9', 107 | '#c084fc', 108 | '#a855f7', 109 | '#9333ea', 110 | '#7e22ce', 111 | '#e879f9', 112 | '#d946ef', 113 | '#c026d3', 114 | '#a21caf', 115 | '#f472b6', 116 | '#ec4899', 117 | '#db2777', 118 | '#be185d', 119 | '#fb7185', 120 | '#f43f5e', 121 | '#e11d48', 122 | '#be123c', 123 | ]; 124 | 125 | const randomColorFor = (s: string) => colors[hash(s) % colors.length]; 126 | 127 | const makeData = (user: Friends, hideNonAdjacent: boolean): DataWithUser => { 128 | const nodes: Node[] = user.friends 129 | .map(f => ({ 130 | id: f.id, 131 | label: f.username, 132 | avatar: f.avatar, 133 | edges: new Set( 134 | hideNonAdjacent ? [user.id] : f.mutualFriends.concat(user.id) 135 | ), 136 | })) 137 | .concat({ 138 | id: user.id, 139 | avatar: user.avatar, 140 | label: user.username, 141 | edges: new Set(user.friends.map(f => f.id)), 142 | }); 143 | 144 | const edges: Edge[] = user.friends.flatMap( 145 | f => 146 | f.mutualFriends 147 | .map(mf => 148 | hideNonAdjacent 149 | ? null 150 | : { 151 | id: `${f.id}-${mf}`, 152 | source: f.id, 153 | target: mf, 154 | color: '#9ca3af70', 155 | } 156 | ) 157 | .concat({ 158 | id: `${user.id}-${f.id}`, 159 | source: user.id, 160 | target: f.id, 161 | color: randomColorFor(user.id), 162 | }) 163 | .filter(Boolean) as Edge[] 164 | ); 165 | 166 | return {id: user.id, nodes, links: edges}; 167 | }; 168 | 169 | function App() { 170 | const [data, setData] = useState({nodes: [], links: []}); 171 | const [inputData, setInputData] = useState([]); 172 | 173 | const [show3D, setShow3D] = useState(false); 174 | const [hideNonAdjacent, setHideNonAdjacent] = useState(false); 175 | 176 | useEffect(() => { 177 | const data: DataWithUser[] = inputData.map(u => 178 | makeData(u, hideNonAdjacent) 179 | ); 180 | 181 | const seenNodes = new Map(); 182 | const seenEdges = new Map(); 183 | 184 | for (const person of data) { 185 | for (const node of person.nodes) { 186 | const existingNode = seenNodes.get(node.id); 187 | if (existingNode) { 188 | node.edges = new Set([...node.edges, ...existingNode.edges]); 189 | } 190 | 191 | seenNodes.set(node.id, node); 192 | } 193 | 194 | for (const edge of person.links) { 195 | if (edge.source === person.id) { 196 | seenEdges.delete(`${edge.source}-${edge.target}`) || 197 | seenEdges.delete(`${edge.target}-${edge.source}`); 198 | seenEdges.set(`${edge.source}-${edge.target}`, edge); 199 | } 200 | 201 | if ( 202 | !( 203 | seenEdges.has(`${edge.source}-${edge.target}`) || 204 | seenEdges.has(`${edge.target}-${edge.source}`) 205 | ) 206 | ) { 207 | seenEdges.set(`${edge.source}-${edge.target}`, edge); 208 | } 209 | } 210 | } 211 | 212 | const merged: {nodes: Node[]; links: Edge[]} = { 213 | nodes: [...seenNodes.values()], 214 | links: [...seenEdges.values()], 215 | }; 216 | 217 | setData(merged); 218 | }, [inputData, hideNonAdjacent]); 219 | 220 | const fgRef = useRef(); 221 | 222 | // biome-ignore lint: i don't even know why it's complaining 223 | useEffect(() => { 224 | if (fgRef?.current) { 225 | // @ts-expect-error i can't figure out how to type this 226 | fgRef.current.d3Force?.('charge')?.strength(-2000).distanceMax(1000); 227 | } 228 | }, [fgRef.current]); 229 | 230 | const [showLabels, setShowLabels] = useState(false); 231 | 232 | const onSubmitFiles: ChangeEventHandler = async e => { 233 | const files = []; 234 | for (let i = 0; i < (e.target.files?.length || 0); i++) { 235 | const file = e.target.files?.item(i)!; 236 | files.push(file); 237 | } 238 | 239 | const usrs: Friends[] = await Promise.all( 240 | files.map(f => f.text().then(t => JSON.parse(t))) 241 | ); 242 | 243 | setInputData(usrs); 244 | }; 245 | 246 | const parseClipboard: MouseEventHandler = async () => { 247 | const clipboardData = await navigator.clipboard.readText(); 248 | const users: Friends[] = clipboardData.split('\n').map(d => JSON.parse(d)); 249 | setInputData(users); 250 | }; 251 | 252 | const ForceGraph = show3D ? ForceGraph3D : ForceGraph2D; 253 | 254 | return ( 255 | <> 256 | {inputData.length === 0 ? ( 257 |
265 |

Add one or more JSON files to get started.

266 |

267 | Paste{' '} 268 | 269 | this script 270 | {' '} 271 | into your Discord dev console to get the data 272 |

273 | 280 | 283 |
284 | ) : ( 285 | <> 286 |
292 | 299 | 300 | 307 | 308 | 317 | 318 | 325 | 326 |
Nodes: {data.nodes.length}
327 |
Edges: {data.links.length}
328 | 329 | {inputData.map(u => ( 330 |
331 | {u.username} 332 |
333 | ))} 334 |
335 | edge.color} 339 | linkWidth={show3D ? 2.5 : 0.5} 340 | nodeLabel={(n: Node) => `${n.label} (${n.edges.size})`} 341 | nodeVal={(n: Node) => n.edges.size} 342 | nodeCanvasObject={ 343 | showLabels 344 | ? (node, ctx, globalScale) => { 345 | const label = `${node.label} (${node.edges.size})`; 346 | const fontSize = 12 / globalScale; 347 | ctx.font = `${fontSize}px Sans-Serif`; 348 | ctx.textAlign = 'center'; 349 | ctx.textBaseline = 'middle'; 350 | ctx.fillStyle = window.matchMedia( 351 | '(prefers-color-scheme: light)' 352 | ).matches 353 | ? '#213547' 354 | : 'lightgray'; 355 | // @ts-expect-error i have no idea how to type this 356 | ctx.fillText(label, node.x, node.y + 6); 357 | } 358 | : undefined 359 | } 360 | nodeCanvasObjectMode={() => 'after'} 361 | /> 362 | 363 | )} 364 | 365 | ); 366 | } 367 | 368 | export default App; 369 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | margin: 0; 4 | } 5 | 6 | input[type=file] { 7 | color: transparent; 8 | } 9 | 10 | :root { 11 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 12 | color-scheme: light dark; 13 | color: rgba(255, 255, 255, 0.87); 14 | background-color: #1A1F29; 15 | 16 | font-synthesis: none; 17 | text-rendering: optimizeLegibility; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | @media (prefers-color-scheme: light) { 23 | :root { 24 | color: #213547; 25 | background-color: #ffffff; 26 | } 27 | a:hover { 28 | color: #747bff; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.tsx'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: './', 8 | }); 9 | --------------------------------------------------------------------------------