From 291250fadfaad0e4dc884dc697fa7ed57656e447 Mon Sep 17 00:00:00 2001 From: Ulisse Mini Date: Thu, 10 Apr 2025 23:58:53 -0700 Subject: [PATCH 1/5] Rename & tell claude to use uitests --- CLAUDE.md | 26 ++++++++++++++++++++++++++ scripts/{test.sh => run-uitests.sh} | 0 2 files changed, 26 insertions(+) create mode 100644 CLAUDE.md rename scripts/{test.sh => run-uitests.sh} (100%) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9291f40 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,26 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +- `cd backend && npm run dev` - Run backend server in development +- `cd backend && npm run build` - Build backend for production +- `cd backend && npm run start` - Start production backend server +- `cd backend && npx tsc --noEmit` - TypeScript validation +- `./scripts/run-uitests.sh` - Run Swift UI tests and export screenshots to a folder + +## Code Style + +- **TypeScript**: ESM modules, strict typing, 2-space indent, async/await patterns +- **Swift**: SwiftUI with MVVM, 4-space indent, ObservableObject for state +- **Error Handling**: Use try/catch in async functions, propagate errors upward +- **Naming**: camelCase for variables/methods, PascalCase for types/classes +- **Imports**: Group imports by source (system, third-party, local) +- **Comments**: Use MARK for Swift section headers, document public interfaces + +## Project Structure + +- Backend: Express API with TypeScript, JSON storage (dev) +- iOS: Native Swift app using SwiftUI, Discord OAuth authentication +- Tests: XCTest for UI testing with screenshot capture capability diff --git a/scripts/test.sh b/scripts/run-uitests.sh similarity index 100% rename from scripts/test.sh rename to scripts/run-uitests.sh From 2553bce4dbd40be32eb60e96040527605a3f9963 Mon Sep 17 00:00:00 2001 From: Ulisse Mini Date: Fri, 11 Apr 2025 00:10:54 -0700 Subject: [PATCH 2/5] always output to TestResults/latest* --- scripts/run-uitests.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/run-uitests.sh b/scripts/run-uitests.sh index c2a61d2..a6f142b 100755 --- a/scripts/run-uitests.sh +++ b/scripts/run-uitests.sh @@ -15,9 +15,10 @@ cd "$(dirname "$0")/.." SCHEME="mysku" # Default destination (replace if needed or pass as argument) DEST="platform=iOS,id=00008101-001434D40081401E" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -RESULT_BUNDLE="TestResults/${TIMESTAMP}.xcresult" -SCREENSHOT_DIR="TestResults/${TIMESTAMP}_screenshots" +RESULT_BUNDLE="TestResults/latest.xcresult" +SCREENSHOT_DIR="TestResults/latest_screenshots" + +rm -rf "$SCREENSHOT_DIR" "$RESULT_BUNDLE" # --- Test Execution --- echo "Running tests for scheme '$SCHEME'..." From 60cbdae00008b0abd6f73d09a40616bf56372a6d Mon Sep 17 00:00:00 2001 From: Ulisse Mini Date: Sat, 10 May 2025 23:07:51 -0700 Subject: [PATCH 3/5] use bun --- backend/bun.lock | 387 +++++++++++++++++++++++++++++++++++++++++ backend/index.ts | 205 +--------------------- backend/package.json | 12 +- backend/src/schemas.ts | 79 +++++++++ backend/src/utils.ts | 125 +++++++++++++ backend/tsconfig.json | 35 ++-- 6 files changed, 622 insertions(+), 221 deletions(-) create mode 100644 backend/bun.lock create mode 100644 backend/src/schemas.ts create mode 100644 backend/src/utils.ts diff --git a/backend/bun.lock b/backend/bun.lock new file mode 100644 index 0000000..2be1d31 --- /dev/null +++ b/backend/bun.lock @@ -0,0 +1,387 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "backend", + "dependencies": { + "@types/node-fetch": "^2.6.12", + "dotenv": "^16.4.7", + "express": "^4.18.2", + "node-apn": "^3.0.0", + "node-fetch": "^3.3.2", + "zod": "^3.24.2", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^5.0.0", + "@types/node": "^22.10.10", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.7.3", + }, + }, + }, + "packages": { + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], + + "@tsconfig/node12": ["@tsconfig/node12@1.0.11", "", {}, "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag=="], + + "@tsconfig/node14": ["@tsconfig/node14@1.0.3", "", {}, "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow=="], + + "@tsconfig/node16": ["@tsconfig/node16@1.0.4", "", {}, "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="], + + "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], + + "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/express": ["@types/express@5.0.0", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.5", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA=="], + + "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/node": ["@types/node@22.10.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], + + "@types/qs": ["@types/qs@6.9.18", "", {}, "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="], + + "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="], + + "@types/strip-bom": ["@types/strip-bom@3.0.0", "", {}, "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ=="], + + "@types/strip-json-comments": ["@types/strip-json-comments@0.0.30", "", {}, "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "acorn": ["acorn@8.14.0", "", { "bin": "bin/acorn" }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g=="], + + "call-bound": ["call-bound@1.0.3", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "get-intrinsic": "^1.2.6" } }, "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + + "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], + + "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "dynamic-dedupe": ["dynamic-dedupe@0.3.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + + "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + + "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.2.7", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", "get-proto": "^1.0.0", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "http2": ["http2@https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz", {}], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "jsonwebtoken": ["jsonwebtoken@8.5.1", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^5.6.0" } }, "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w=="], + + "jwa": ["jwa@1.4.1", "", { "dependencies": { "buffer-equal-constant-time": "1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": "cli.js" }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": "bin/cmd.js" }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "node-apn": ["node-apn@3.0.0", "", { "dependencies": { "debug": "^3.1.0", "http2": "https://github.com/node-apn/node-http2/archive/apn-2.1.4.tar.gz", "jsonwebtoken": "^8.1.0", "node-forge": "^0.7.1", "verror": "^1.10.0" } }, "sha512-1qXw/JbXMTpUbi0/cIKjU3kOMPxm/r8HvZp8na/nHkuOhW+k7oX+fa9dXiuhlQHevQJgpI4Ycn7JCCXeCaOpIw=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-forge": ["node-forge@0.7.6", "", {}, "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-inspect": ["object-inspect@1.13.3", "", {}, "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], + + "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@5.7.2", "", { "bin": "bin/semver" }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": "cli.js" }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-node": ["ts-node@10.9.2", "", { "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", "@tsconfig/node16": "^1.0.2", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", "@types/node": "*", "typescript": ">=2.7" }, "optionalPeers": ["@swc/core", "@swc/wasm"], "bin": { "ts-node": "dist/bin.js", "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" } }, "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ=="], + + "ts-node-dev": ["ts-node-dev@2.0.0", "", { "dependencies": { "chokidar": "^3.5.1", "dynamic-dedupe": "^0.3.0", "minimist": "^1.2.6", "mkdirp": "^1.0.4", "resolve": "^1.0.0", "rimraf": "^2.6.1", "source-map-support": "^0.5.12", "tree-kill": "^1.2.2", "ts-node": "^10.4.0", "tsconfig": "^7.0.0" }, "peerDependencies": { "node-notifier": "*", "typescript": "*" }, "optionalPeers": ["node-notifier"], "bin": { "ts-node-dev": "lib/bin.js", "tsnd": "lib/bin.js" } }, "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w=="], + + "tsconfig": ["tsconfig@7.0.0", "", { "dependencies": { "@types/strip-bom": "^3.0.0", "@types/strip-json-comments": "0.0.30", "strip-bom": "^3.0.0", "strip-json-comments": "^2.0.0" } }, "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="], + + "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], + + "jsonwebtoken/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-apn/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-apn/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + } +} diff --git a/backend/index.ts b/backend/index.ts index 4803ba9..986556c 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -8,6 +8,9 @@ import { fileURLToPath } from 'url'; import { dirname } from 'path'; import dotenv from 'dotenv'; import apn from 'node-apn'; +import { RecentNotificationSchema, LocationSchema, DiscordUserSchema, UserSchema, DiscordTokenResponseSchema, GuildSchema, DemoDataSchema } from './src/schemas'; +import type { Location, DiscordUser, User, Guild, DemoData, RecentNotification } from './src/schemas'; +import { reportErrorToWebhook, reportNearbyUsersToWebhook } from './src/utils'; // Load environment variables from .env file dotenv.config(); @@ -50,130 +53,6 @@ try { throw error; } -// Error reporting webhook URL from environment variable -const ERROR_WEBHOOK_URL = 'https://discord.com/api/webhooks/1353024080550301788/UcxP89nUSNEQ_994CAYXrJVFPyXxOQtB3rBolgZ2-hgb23XBjQ4R2BCnd-MkFwNlcIzQ'; -const NEARBY_WEBHOOK_URL = 'https://discord.com/api/webhooks/1358243072642646076/dC2NbR8MaDEQAzMPXTnaDI_XVnB2iuxxvGCbrbguPn1fQyQec3igtV_RF4S7G9YEGjZb'; // Added webhook for nearby notifications - -// Function to report errors to webhook -async function reportErrorToWebhook(error: Error, req?: Request): Promise { - if (!ERROR_WEBHOOK_URL) { - console.warn('Error webhook URL not configured. Set ERROR_WEBHOOK_URL in .env file to enable error reporting.'); - return; - } - - try { - // Create readable timestamp - const timestamp = new Date().toISOString(); - - // Format stack trace - limit to first 4 lines if it's too long - const stackLines = error.stack?.split('\n') || []; - const limitedStack = stackLines.slice(0, 4).join('\n'); - const stackTrace = limitedStack + (stackLines.length > 4 ? '\n...(truncated)' : ''); - - // Create Discord webhook message with embeds for better formatting - const webhookPayload = { - content: "🚨 **Server Error Detected** 🚨", - embeds: [ - { - title: `Error: ${error.name}`, - description: error.message, - color: 15548997, // Red color - fields: [ - { - name: "📋 Stack Trace", - value: `\`\`\`\n${stackTrace}\n\`\`\``, - inline: false - }, - { - name: "⏰ Timestamp", - value: timestamp, - inline: true - } - ], - footer: { - text: "MySkew Error Monitoring" - } - } - ] - }; - - // Add request details if available - if (req) { - const requestDetailsField = { - name: "🌐 Request Details", - value: [ - `**Method:** ${req.method}`, - `**Path:** ${req.url}`, - `**IP:** ${req.ip}`, - `**User-Agent:** ${req.headers['user-agent'] || 'N/A'}` - ].join('\n'), - inline: false - }; - - // Add query params if present - if (Object.keys(req.query).length > 0) { - requestDetailsField.value += `\n**Query:** \`${JSON.stringify(req.query).slice(0, 500)}\``; - } - - // Add body if present (truncate if too large) - if (req.body && Object.keys(req.body).length > 0) { - let bodyStr = JSON.stringify(req.body); - if (bodyStr.length > 500) { - bodyStr = bodyStr.slice(0, 497) + '...'; - } - requestDetailsField.value += `\n**Body:** \`${bodyStr}\``; - } - - webhookPayload.embeds[0].fields.push(requestDetailsField); - } - - await fetch(ERROR_WEBHOOK_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(webhookPayload) - }); - - console.log('Error reported to webhook'); - } catch (webhookError) { - console.error('Failed to report error to webhook:', webhookError); - } -} - -// Function to report nearby users to webhook - NEW -async function reportNearbyUsersToWebhook(user1: User, user2: User, distance: number): Promise { - if (!NEARBY_WEBHOOK_URL) { - console.warn('Nearby webhook URL not configured.'); - return; - } - - try { - const timestamp = new Date().toISOString(); - const message = `📍 Users Nearby Detected! -**${user1.duser.username}** and **${user2.duser.username}** are approximately **${Math.round(distance)}m** apart. -Timestamp: ${timestamp}`; - - const webhookPayload = { - content: message - }; - - await fetch(NEARBY_WEBHOOK_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(webhookPayload) - }); - - console.log(`Reported nearby users (${user1.duser.username}, ${user2.duser.username}) to webhook`); - } catch (webhookError) { - console.error('Failed to report nearby users to webhook:', webhookError); - // Optionally report this failure to the error webhook - await reportErrorToWebhook(new Error(`Failed to send nearby notification webhook: ${webhookError}`)).catch(console.error); - } -} - // Cache interfaces and implementations interface CacheEntry { data: T; @@ -229,84 +108,6 @@ if (!DISCORD_CLIENT_ID || !DISCORD_CLIENT_SECRET) { throw new Error('Missing required environment variables. Please check your .env file.'); } -// Zod Schemas -const LocationSchema = z.object({ - latitude: z.number(), - longitude: z.number(), - accuracy: z.number(), // Actual GPS accuracy in meters - desiredAccuracy: z.number().optional(), // User's desired privacy-preserving accuracy in meters (optional for backward compatibility) - lastUpdated: z.number() -}); - -const PrivacySettingsSchema = z.object({ - enabledGuilds: z.array(z.string()), // guilds sharing & viewing is enabled for - blockedUsers: z.array(z.string()) // users who we shouldn't send location to -}); - -const DiscordUserSchema = z.object({ - id: z.string(), - username: z.string(), - avatar: z.string().nullable().optional() -}); - -const UserSchema = z.object({ - id: z.string(), - location: LocationSchema.optional(), - duser: DiscordUserSchema, - privacy: PrivacySettingsSchema, - pushToken: z.string().optional(), - receiveNearbyNotifications: z.boolean().optional(), - allowNearbyNotifications: z.boolean().optional() -}); - -const DiscordTokenResponseSchema = z.object({ - access_token: z.string(), - token_type: z.string(), - expires_in: z.number(), - refresh_token: z.string(), - scope: z.string() -}); - -// Guild schemas -const GuildSchema = z.object({ - id: z.string(), - name: z.string(), - icon: z.string().nullable() -}); - -// Demo data schema (defined after other schemas it depends on) -const DemoDataSchema = z.object({ - 'users/@me': z.object({ - demo: DiscordUserSchema - }), - 'users/@me/guilds': z.object({ - demo: z.array(GuildSchema) - }), - 'db': z.object({ - users: z.record(UserSchema) - }) -}); - -type DemoData = z.infer; -type DemoApiPath = keyof DemoData; - -// Type inference from schemas -type Location = z.infer; -type PrivacySettings = z.infer; -type DiscordUser = z.infer; -type User = z.infer; -type DiscordTokenResponse = z.infer; -type Guild = z.infer; - -// Add schema for recent notifications -const RecentNotificationSchema = z.object({ - user1Id: z.string(), - user2Id: z.string(), - timestamp: z.number() -}); - -type RecentNotification = z.infer; - // Add type declaration for the extended Request declare global { namespace Express { diff --git a/backend/package.json b/backend/package.json index a2cf7ef..33792f7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,10 +4,8 @@ "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "node --loader ts-node/esm index.ts", - "build": "tsc && npm run copy-files", - "copy-files": "cp demo-mode.json dist/ && cp -r static dist/", - "start": "node dist/index.js" + "dev": "bun run --watch index.ts", + "start": "bun run index.ts" }, "author": "", "license": "ISC", @@ -17,7 +15,8 @@ "@types/node": "^22.10.10", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "@types/bun": "latest" }, "dependencies": { "@types/node-fetch": "^2.6.12", @@ -26,5 +25,6 @@ "node-apn": "^3.0.0", "node-fetch": "^3.3.2", "zod": "^3.24.2" - } + }, + "module": "index.ts" } diff --git a/backend/src/schemas.ts b/backend/src/schemas.ts new file mode 100644 index 0000000..cb1679e --- /dev/null +++ b/backend/src/schemas.ts @@ -0,0 +1,79 @@ +import { z } from 'zod'; + +// Zod Schemas +export const LocationSchema = z.object({ + latitude: z.number(), + longitude: z.number(), + accuracy: z.number(), // Actual GPS accuracy in meters + desiredAccuracy: z.number().optional(), // User's desired privacy-preserving accuracy in meters (optional for backward compatibility) + lastUpdated: z.number() +}); + +export const PrivacySettingsSchema = z.object({ + enabledGuilds: z.array(z.string()), // guilds sharing & viewing is enabled for + blockedUsers: z.array(z.string()) // users who we shouldn't send location to +}); + +export const DiscordUserSchema = z.object({ + id: z.string(), + username: z.string(), + avatar: z.string().nullable().optional() +}); + +export const UserSchema = z.object({ + id: z.string(), + location: LocationSchema.optional(), + duser: DiscordUserSchema, + privacy: PrivacySettingsSchema, + pushToken: z.string().optional(), + receiveNearbyNotifications: z.boolean().optional(), + allowNearbyNotifications: z.boolean().optional() +}); + +export const DiscordTokenResponseSchema = z.object({ + access_token: z.string(), + token_type: z.string(), + expires_in: z.number(), + refresh_token: z.string(), + scope: z.string() +}); + +// Guild schemas +export const GuildSchema = z.object({ + id: z.string(), + name: z.string(), + icon: z.string().nullable() +}); + +// Demo data schema (defined after other schemas it depends on) +export const DemoDataSchema = z.object({ + 'users/@me': z.object({ + demo: DiscordUserSchema + }), + 'users/@me/guilds': z.object({ + demo: z.array(GuildSchema) + }), + 'db': z.object({ + users: z.record(UserSchema) + }) +}); + +export type DemoData = z.infer; +export type DemoApiPath = keyof DemoData; + +// Type inference from schemas +export type Location = z.infer; +export type PrivacySettings = z.infer; +export type DiscordUser = z.infer; +export type User = z.infer; +export type DiscordTokenResponse = z.infer; +export type Guild = z.infer; + +// Add schema for recent notifications +export const RecentNotificationSchema = z.object({ + user1Id: z.string(), + user2Id: z.string(), + timestamp: z.number() +}); + +export type RecentNotification = z.infer; \ No newline at end of file diff --git a/backend/src/utils.ts b/backend/src/utils.ts new file mode 100644 index 0000000..830dbbe --- /dev/null +++ b/backend/src/utils.ts @@ -0,0 +1,125 @@ +import type { User } from './schemas'; + +// Error reporting webhook URL from environment variable +const ERROR_WEBHOOK_URL = 'https://discord.com/api/webhooks/1353024080550301788/UcxP89nUSNEQ_994CAYXrJVFPyXxOQtB3rBolgZ2-hgb23XBjQ4R2BCnd-MkFwNlcIzQ'; +const NEARBY_WEBHOOK_URL = 'https://discord.com/api/webhooks/1358243072642646076/dC2NbR8MaDEQAzMPXTnaDI_XVnB2iuxxvGCbrbguPn1fQyQec3igtV_RF4S7G9YEGjZb'; // Added webhook for nearby notifications + +// Function to report errors to webhook +export async function reportErrorToWebhook(error: Error, req?: Request): Promise { + if (!ERROR_WEBHOOK_URL) { + console.warn('Error webhook URL not configured. Set ERROR_WEBHOOK_URL in .env file to enable error reporting.'); + return; + } + + try { + // Create readable timestamp + const timestamp = new Date().toISOString(); + + // Format stack trace - limit to first 4 lines if it's too long + const stackLines = error.stack?.split('\n') || []; + const limitedStack = stackLines.slice(0, 4).join('\n'); + const stackTrace = limitedStack + (stackLines.length > 4 ? '\n...(truncated)' : ''); + + // Create Discord webhook message with embeds for better formatting + const webhookPayload = { + content: "🚨 **Server Error Detected** 🚨", + embeds: [ + { + title: `Error: ${error.name}`, + description: error.message, + color: 15548997, // Red color + fields: [ + { + name: "📋 Stack Trace", + value: `\`\`\`\n${stackTrace}\n\`\`\``, + inline: false + }, + { + name: "⏰ Timestamp", + value: timestamp, + inline: true + } + ], + footer: { + text: "MySkew Error Monitoring" + } + } + ] + }; + + // Add request details if available + if (req) { + const requestDetailsField = { + name: "🌐 Request Details", + value: [ + `**Method:** ${req.method}`, + `**Path:** ${req.url}`, + `**IP:** ${req.ip}`, + `**User-Agent:** ${req.headers['user-agent'] || 'N/A'}` + ].join('\n'), + inline: false + }; + + // Add query params if present + if (Object.keys(req.query).length > 0) { + requestDetailsField.value += `\n**Query:** \`${JSON.stringify(req.query).slice(0, 500)}\``; + } + + // Add body if present (truncate if too large) + if (req.body && Object.keys(req.body).length > 0) { + let bodyStr = JSON.stringify(req.body); + if (bodyStr.length > 500) { + bodyStr = bodyStr.slice(0, 497) + '...'; + } + requestDetailsField.value += `\n**Body:** \`${bodyStr}\``; + } + + webhookPayload.embeds[0].fields.push(requestDetailsField); + } + + await fetch(ERROR_WEBHOOK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(webhookPayload) + }); + + console.log('Error reported to webhook'); + } catch (webhookError) { + console.error('Failed to report error to webhook:', webhookError); + } +} + +// Function to report nearby users to webhook - NEW +export async function reportNearbyUsersToWebhook(user1: User, user2: User, distance: number): Promise { + if (!NEARBY_WEBHOOK_URL) { + console.warn('Nearby webhook URL not configured.'); + return; + } + + try { + const timestamp = new Date().toISOString(); + const message = `📍 Users Nearby Detected! +**${user1.duser.username}** and **${user2.duser.username}** are approximately **${Math.round(distance)}m** apart. +Timestamp: ${timestamp}`; + + const webhookPayload = { + content: message + }; + + await fetch(NEARBY_WEBHOOK_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(webhookPayload) + }); + + console.log(`Reported nearby users (${user1.duser.username}, ${user2.duser.username}) to webhook`); + } catch (webhookError) { + console.error('Failed to report nearby users to webhook:', webhookError); + // Optionally report this failure to the error webhook + await reportErrorToWebhook(new Error(`Failed to send nearby notification webhook: ${webhookError}`)).catch(console.error); + } +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 5651a20..0601a99 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -1,18 +1,27 @@ { "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "esModuleInterop": true, + // Enable latest features + "lib": [ + "ESNext", + "DOM" + ], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices "strict": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./dist" - }, - "include": [ - "**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } } \ No newline at end of file From 7950fb3dc144b5852d52d44c34483dda2724eaa4 Mon Sep 17 00:00:00 2001 From: Ulisse Mini Date: Sat, 10 May 2025 23:31:04 -0700 Subject: [PATCH 4/5] basic tests! --- backend/index.test.ts | 102 ++++++++++++++++++++++++++++ backend/index.ts | 153 +++++++++++++++++++++++++++--------------- backend/src/utils.ts | 1 + 3 files changed, 202 insertions(+), 54 deletions(-) create mode 100644 backend/index.test.ts diff --git a/backend/index.test.ts b/backend/index.test.ts new file mode 100644 index 0000000..fab9563 --- /dev/null +++ b/backend/index.test.ts @@ -0,0 +1,102 @@ +import { expect, test, describe, beforeAll, afterAll } from "bun:test"; +import { createServer } from "http"; +import app from "./index"; + +const server = createServer(app); +const PORT = 3456; +const BASE_URL = `http://localhost:${PORT}`; + +// Demo token for authentication +const DEMO_TOKEN = "demo"; + +describe("API Tests", () => { + // Setup and teardown + beforeAll(() => { + return new Promise((resolve) => { + server.listen(PORT, () => { + console.log(`Test server running on port ${PORT}`); + resolve(); + }); + }); + }); + + afterAll(() => { + return new Promise((resolve, reject) => { + server.close((err?: Error) => { + console.log("Test server closed"); + err ? reject(err) : resolve(); + }); + }); + }); + + // Test creating a new user + test("should create a new user", async () => { + const newUserData = { + location: { + latitude: 40.7128, + longitude: -74.0060, + accuracy: 10, + lastUpdated: Date.now() + }, + privacy: { + enabledGuilds: ["123456789"], + blockedUsers: [] + }, + pushToken: "test-push-token", + receiveNearbyNotifications: true, + allowNearbyNotifications: true + }; + + const response = await fetch(`${BASE_URL}/users/me`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${DEMO_TOKEN}` + }, + body: JSON.stringify(newUserData), + }); + + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data.success).toBe(true); + }); + + // Test getting user data + test("should get current user data", async () => { + const response = await fetch(`${BASE_URL}/users/me`, { + headers: { + "Authorization": `Bearer ${DEMO_TOKEN}` + } + }); + + expect(response.status).toBe(200); + + const userData = await response.json(); + expect(userData.id).toBeDefined(); + expect(userData.duser).toBeDefined(); + expect(userData.privacy).toBeDefined(); + expect(userData.location).toBeDefined(); + }); + + // Test getting all users + test("should get all users including our user", async () => { + const response = await fetch(`${BASE_URL}/users`, { + headers: { + "Authorization": `Bearer ${DEMO_TOKEN}` + } + }); + + expect(response.status).toBe(200); + + const users = await response.json(); + expect(Array.isArray(users)).toBe(true); + + // Verify our demo user is in the list - the test showed the user ID is "1000" not "demo0" + const demoUser = users.find((user: any) => user.id === "1000"); + expect(demoUser).toBeDefined(); + + // Check if user properties were properly set + expect(demoUser.privacy.enabledGuilds).toContain("123456789"); + }); +}); \ No newline at end of file diff --git a/backend/index.ts b/backend/index.ts index 986556c..41cb10c 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -1,6 +1,6 @@ import express from 'express'; -import { Request, Response as ExpressResponse, NextFunction } from 'express'; -import fetch, { Response as FetchResponse, RequestInit } from 'node-fetch'; +import type { Request, Response as ExpressResponse, NextFunction } from 'express'; +import fetch, { Response as FetchResponse, type RequestInit } from 'node-fetch'; import { z } from 'zod'; import fs from 'fs'; import path from 'path'; @@ -129,14 +129,15 @@ app.use(express.json()); const users: Record = {}; const tokenToUserId: Record = {}; -// Load any existing data -loadPersistedData(); +// The following data loading and interval setup will be moved to the main() function. +// // Load any existing data +// loadPersistedData(); -// Set up periodic saving -if (fs.existsSync(DB_DIR)) { - setInterval(saveDataToDisk, 60000); // Save every minute - console.log('Automatic data persistence enabled'); -} +// // Set up periodic saving +// if (fs.existsSync(DB_DIR)) { +// setInterval(saveDataToDisk, 60000); // Save every minute +// console.log('Automatic data persistence enabled'); +// } // Fetch demo data, validate it, merge demo users into the users store const rawDemoData = JSON.parse(fs.readFileSync(path.join(__dirname, 'demo-mode.json'), 'utf-8')); @@ -197,7 +198,7 @@ function isDemoApiPath(path: string): path is keyof Pick => { const authHeader = req.headers.authorization; - console.log('verify: auth header:', authHeader); + // console.log('verify: auth header:', authHeader); if (!authHeader || !authHeader.startsWith('Bearer ')) { console.error('verify: invalid auth header format'); @@ -206,13 +207,13 @@ const verifyToken = async (req: Request, res: ExpressResponse, next: NextFunctio } const token = authHeader.split(' ')[1]; - console.log('verify: attempting to validate token with Discord'); + // console.log('verify: attempting to validate token with Discord'); try { // First check our token cache const cachedUserId = tokenToUserId[token]; if (cachedUserId && users[cachedUserId]) { - console.log('verify: found cached user:', cachedUserId); + // console.log('verify: found cached user:', cachedUserId); req.user = users[cachedUserId]; next(); return; @@ -222,7 +223,7 @@ const verifyToken = async (req: Request, res: ExpressResponse, next: NextFunctio const userResponse = await discordFetch('users/@me', token); const responseText = await userResponse.text(); - console.log('verify: Discord response status:', userResponse.status); + // console.log('verify: Discord response status:', userResponse.status); if (!userResponse.ok) { console.error('verify: Discord validation failed:', { @@ -238,7 +239,7 @@ const verifyToken = async (req: Request, res: ExpressResponse, next: NextFunctio const rawUserData = JSON.parse(responseText); // Validate Discord user data userData = DiscordUserSchema.parse(rawUserData); - console.log('verify: got user data for:', userData.username); + // console.log('verify: got user data for:', userData.username); } catch (e) { console.error('verify: Failed to parse/validate user data:', e); console.error('verify: Raw response:', responseText); @@ -272,7 +273,7 @@ const verifyToken = async (req: Request, res: ExpressResponse, next: NextFunctio users[userData.id] = user; tokenToUserId[token] = userData.id; - console.log('verify: stored new user in cache:', userData.id); + // console.log('verify: stored new user in cache:', userData.id); req.user = user; next(); } catch (error) { @@ -315,7 +316,7 @@ function roundCoordinates(location: Location): Location { // Get all users we have access to see app.get('/users', verifyToken, (req: Request, res: ExpressResponse): void => { const user = req.user!; - console.log('GET /users: Processing request for user:', user.id); + // console.log('GET /users: Processing request for user:', user.id); // Filter users based on guild membership and privacy settings const visibleUsers: User[] = Object.values(users).filter(otherUser => { @@ -362,17 +363,17 @@ app.get('/users', verifyToken, (req: Request, res: ExpressResponse): void => { }); const jiggledUsers = jiggleUsers(visibleUsers); - console.log('GET /users: Final user count:', jiggledUsers.length); + // console.log('GET /users: Final user count:', jiggledUsers.length); res.json(jiggledUsers); }); // Update user data (location, privacy settings, etc) app.post('/users/me', verifyToken, async (req: Request, res: ExpressResponse): Promise => { const currentUser = req.user!; - console.log('POST /users/me: Received update request:', { - userId: currentUser.id, - body: req.body - }); + // console.log('POST /users/me: Received update request:', { + // userId: currentUser.id, + // body: req.body + // }); try { const { username, guild, location, privacy, pushToken, @@ -398,13 +399,13 @@ app.post('/users/me', verifyToken, async (req: Request, res: ExpressResponse): P allowNearbyNotifications: allowNearbyNotifications ?? currentUser.allowNearbyNotifications ?? true }); - console.log('POST /users/me: Validated and processed user data:', { - userId: updatedUser.id, - location: updatedUser.location, - pushToken: updatedUser.pushToken, - receiveNearbyNotifications: updatedUser.receiveNearbyNotifications, - allowNearbyNotifications: updatedUser.allowNearbyNotifications - }); + // console.log('POST /users/me: Validated and processed user data:', { + // userId: updatedUser.id, + // location: updatedUser.location, + // pushToken: updatedUser.pushToken, + // receiveNearbyNotifications: updatedUser.receiveNearbyNotifications, + // allowNearbyNotifications: updatedUser.allowNearbyNotifications + // }); // Ensure the user can only update their own data if (currentUser.id !== updatedUser.id) { @@ -435,7 +436,7 @@ app.post('/users/me', verifyToken, async (req: Request, res: ExpressResponse): P app.get('/users/me', verifyToken, (req: Request, res: ExpressResponse): void => { const user = req.user!; - console.debug('users/me: user:', user); + // console.debug('users/me: user:', user); res.json(user); }); @@ -798,39 +799,67 @@ async function checkNearbyUsers() { } } -// Set up periodic check for nearby users -setInterval(checkNearbyUsers, 6000); // Check every 6s (for now) +// The following interval setup will be moved to the main() function. +// // Set up periodic check for nearby users +// setInterval(checkNearbyUsers, 6000); // Check every 6s (for now) -// Start the server -const server = app.listen(port, () => { - console.log(`Server running on port ${port}`); -}); +async function main() { + console.log('Executing main application function...'); -// Handle graceful shutdown -const shutdown = async () => { - console.log('\nReceived shutdown signal. Closing server...'); + // Load any existing data + loadPersistedData(); - // Close the server - server.close(() => { - console.log('Server closed'); + // Set up periodic saving + if (fs.existsSync(DB_DIR)) { + setInterval(saveDataToDisk, 60000); // Save every minute + console.log('Automatic data persistence enabled'); + } else { + console.log('DB_DIR does not exist, automatic data persistence disabled.'); + } - // Close APNs provider if it exists - if (apnProvider) { - apnProvider.shutdown(); - console.log('APNs provider closed'); - } + // Set up periodic check for nearby users + setInterval(checkNearbyUsers, 6000); // Check every 6s - // Save any pending data - saveDataToDisk(); + // Start the server + console.log(`Attempting to start server on port ${port}...`); + const server = app.listen(port, () => { + console.log(`Server running on port ${port}`); + }); - console.log('Cleanup complete. Exiting...'); - process.exit(0); + server.on('error', (err: Error) => { + console.error('Failed to start server:', err); + process.exit(1); }); -}; -// Handle SIGINT (Ctrl+C) and SIGTERM signals -process.on('SIGINT', shutdown); -process.on('SIGTERM', shutdown); + // Handle graceful shutdown + const shutdown = () => { + console.log('\nReceived shutdown signal. Closing server...'); + + server.close((err?: Error) => { + if (err) { + console.error('Error during server close:', err); + } else { + console.log('Server closed'); + } + + if (apnProvider) { + apnProvider.shutdown(); + console.log('APNs provider closed'); + } + + // Save any pending data + saveDataToDisk(); + + console.log('Cleanup complete. Exiting...'); + process.exit(err ? 1 : 0); + }); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + + console.log('Application main function finished setup. Server is running and periodic tasks scheduled.'); +} // Global error handling middleware - add before the export app.use((err: Error, req: Request, res: ExpressResponse, next: NextFunction) => { @@ -859,10 +888,22 @@ process.on('unhandledRejection', (reason) => { reportErrorToWebhook(error).catch(console.error); }); +// Run main function if this script is executed directly +if (import.meta.main) { + main().catch(err => { + console.error("Failed to execute main application logic:", err); + process.exit(1); + }); +} + // Add explicit export to mark as ESM module export default app; function loadPersistedData() { + if (!import.meta.main) { + console.log('Not in main module, skipping data load'); + return; + } if (!fs.existsSync(DB_DIR)) { console.log('No db directory found, skipping data load'); return; @@ -892,6 +933,10 @@ function loadPersistedData() { } function saveDataToDisk() { + if (!import.meta.main) { + console.log('Not in main module, skipping data persistence'); + return; + } if (!fs.existsSync(DB_DIR)) { return; // Don't save if db directory doesn't exist } diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 830dbbe..b578316 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -1,4 +1,5 @@ import type { User } from './schemas'; +import type { Request } from 'express'; // Error reporting webhook URL from environment variable const ERROR_WEBHOOK_URL = 'https://discord.com/api/webhooks/1353024080550301788/UcxP89nUSNEQ_994CAYXrJVFPyXxOQtB3rBolgZ2-hgb23XBjQ4R2BCnd-MkFwNlcIzQ'; From edbdfb7d45632e096d93cc2e9323fe8a5eca079f Mon Sep 17 00:00:00 2001 From: Ulisse Mini Date: Sat, 10 May 2025 23:47:46 -0700 Subject: [PATCH 5/5] nicer index.ts and tests --- backend/index.test.ts | 75 +++++++++++++++++++------------------- backend/index.ts | 84 +++++++++++++++++++------------------------ 2 files changed, 73 insertions(+), 86 deletions(-) diff --git a/backend/index.test.ts b/backend/index.test.ts index fab9563..ecab3c3 100644 --- a/backend/index.test.ts +++ b/backend/index.test.ts @@ -31,31 +31,8 @@ describe("API Tests", () => { // Test creating a new user test("should create a new user", async () => { - const newUserData = { - location: { - latitude: 40.7128, - longitude: -74.0060, - accuracy: 10, - lastUpdated: Date.now() - }, - privacy: { - enabledGuilds: ["123456789"], - blockedUsers: [] - }, - pushToken: "test-push-token", - receiveNearbyNotifications: true, - allowNearbyNotifications: true - }; - - const response = await fetch(`${BASE_URL}/users/me`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${DEMO_TOKEN}` - }, - body: JSON.stringify(newUserData), - }); - + const body = userData({privacy: { enabledGuilds: ["123456789"], blockedUsers: []}}); + const response = await apiCall('/users/me', 'POST', body); expect(response.status).toBe(200); const data = await response.json(); @@ -64,12 +41,7 @@ describe("API Tests", () => { // Test getting user data test("should get current user data", async () => { - const response = await fetch(`${BASE_URL}/users/me`, { - headers: { - "Authorization": `Bearer ${DEMO_TOKEN}` - } - }); - + const response = await apiCall('/users/me', 'GET'); expect(response.status).toBe(200); const userData = await response.json(); @@ -81,12 +53,7 @@ describe("API Tests", () => { // Test getting all users test("should get all users including our user", async () => { - const response = await fetch(`${BASE_URL}/users`, { - headers: { - "Authorization": `Bearer ${DEMO_TOKEN}` - } - }); - + const response = await apiCall('/users', 'GET'); expect(response.status).toBe(200); const users = await response.json(); @@ -99,4 +66,36 @@ describe("API Tests", () => { // Check if user properties were properly set expect(demoUser.privacy.enabledGuilds).toContain("123456789"); }); -}); \ No newline at end of file +}); + + +function userData(other: any = {}) { + return { + location: { + latitude: 40.7128, + longitude: -74.0060, + accuracy: 10, + lastUpdated: Date.now() + }, + privacy: { + enabledGuilds: [], + blockedUsers: [], + }, + pushToken: "test-push-token", + receiveNearbyNotifications: true, + allowNearbyNotifications: true, + ...other + }; +} + +async function apiCall(path: string, method: string, body: any = null) { + const response = await fetch(`${BASE_URL}${path}`, { + method: method, + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${DEMO_TOKEN}` + }, + body: body ? JSON.stringify(body) : null + }); + return response; +} diff --git a/backend/index.ts b/backend/index.ts index 41cb10c..3f65056 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -316,56 +316,44 @@ function roundCoordinates(location: Location): Location { // Get all users we have access to see app.get('/users', verifyToken, (req: Request, res: ExpressResponse): void => { const user = req.user!; - // console.log('GET /users: Processing request for user:', user.id); - - // Filter users based on guild membership and privacy settings - const visibleUsers: User[] = Object.values(users).filter(otherUser => { - // Always include the current user - if (otherUser.id === user.id) return true; - - // Check if users share any guilds - const sharedGuilds = user.privacy.enabledGuilds.filter(guild => - otherUser.privacy.enabledGuilds.includes(guild) - ); - - return sharedGuilds.length > 0; - }).map(otherUser => { - // If either user has blocked the other, return user without location - if (user.privacy.blockedUsers.includes(otherUser.id) || - otherUser.privacy.blockedUsers.includes(user.id)) { - console.log(`GET /users: User ${otherUser.id} is blocked, removing location`); - return { - ...otherUser, - location: undefined - }; - } - - // Round coordinates based on the user's requested accuracy - if (otherUser.location) { - // Ensure desiredAccuracy exists for migration - const location = { - ...otherUser.location, - desiredAccuracy: otherUser.location.desiredAccuracy ?? 0 - }; - - // console.debug(`GET /users: Processing location for user ${otherUser.id}:`, { - // original: location, - // rounded: roundCoordinates(location) - // }); - - return { - ...otherUser, - location: roundCoordinates(location) - }; - } + + const visibleUsers = Object.values(users) + .filter(other => other.id === user.id || + hasSharedGuilds(user, other)) + .map(other => { + // Remove location if either user blocked the other + if (isBlocked(user, other)) { + return { ...other, location: undefined }; + } + + // Process location if it exists + if (other.location) { + return { + ...other, + location: roundCoordinates({ + ...other.location, + desiredAccuracy: other.location.desiredAccuracy ?? 0 + }) + }; + } + return other; + }); + + res.json(jiggleUsers(visibleUsers)); +}); - return otherUser; - }); +// Helper functions +function hasSharedGuilds(user1: User, user2: User): boolean { + return user1.privacy.enabledGuilds.some(guild => + user2.privacy.enabledGuilds.includes(guild)); +} - const jiggledUsers = jiggleUsers(visibleUsers); - // console.log('GET /users: Final user count:', jiggledUsers.length); - res.json(jiggledUsers); -}); +function isBlocked(user1: User, user2: User): boolean { + const blocked = user1.privacy.blockedUsers.includes(user2.id) || + user2.privacy.blockedUsers.includes(user1.id); + if (blocked) console.log(`GET /users: User ${user2.id} is blocked, removing location`); + return blocked; +} // Update user data (location, privacy settings, etc) app.post('/users/me', verifyToken, async (req: Request, res: ExpressResponse): Promise => {