From db49ac2a98444e6d9dffdfaee80416858709106e Mon Sep 17 00:00:00 2001 From: Will Freeman Date: Mon, 2 Feb 2026 11:41:29 -0700 Subject: [PATCH] separate fe and be, deploy on push to master --- .github/workflows/deflock-api-deploy.yml | 38 ++++ api/README.md | 31 +++ api/bun.lock | 177 ++++++++++++++++++ api/package.json | 21 +++ api/server.ts | 127 +++++++++++++ api/services/GithubClient.ts | 39 ++++ api/services/NominatimClient.ts | 88 +++++++++ shotgun/.gitignore | 70 ------- shotgun/build.sbt | 32 ---- shotgun/project/Dependencies.scala | 5 - shotgun/project/build.properties | 1 - shotgun/project/plugins.sbt | 1 - shotgun/src/main/resources/logback.xml | 12 -- .../me/deflock/shotgun/ShotgunServer.scala | 123 ------------ .../main/scala/services/GithubClient.scala | 69 ------- .../main/scala/services/NominatimClient.scala | 53 ------ .../src/test/scala/example/HelloSpec.scala | 9 - webapp/src/services/apiService.ts | 49 +---- 18 files changed, 525 insertions(+), 420 deletions(-) create mode 100644 .github/workflows/deflock-api-deploy.yml create mode 100644 api/README.md create mode 100644 api/bun.lock create mode 100644 api/package.json create mode 100644 api/server.ts create mode 100644 api/services/GithubClient.ts create mode 100644 api/services/NominatimClient.ts delete mode 100644 shotgun/.gitignore delete mode 100644 shotgun/build.sbt delete mode 100644 shotgun/project/Dependencies.scala delete mode 100644 shotgun/project/build.properties delete mode 100644 shotgun/project/plugins.sbt delete mode 100644 shotgun/src/main/resources/logback.xml delete mode 100644 shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala delete mode 100644 shotgun/src/main/scala/services/GithubClient.scala delete mode 100644 shotgun/src/main/scala/services/NominatimClient.scala delete mode 100644 shotgun/src/test/scala/example/HelloSpec.scala diff --git a/.github/workflows/deflock-api-deploy.yml b/.github/workflows/deflock-api-deploy.yml new file mode 100644 index 0000000..99f6d73 --- /dev/null +++ b/.github/workflows/deflock-api-deploy.yml @@ -0,0 +1,38 @@ +name: DeFlock API Deploy + +on: + push: + branches: [master] + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - run: bun install + working-directory: ./api + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts + + - name: Sync files + run: | + rsync -az --delete \ + --exclude node_modules \ + ./api/ ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/home/nullplate/deflock/api + + - name: Restart services + run: | + ssh ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} " + sudo systemctl restart deflock-api + " diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..2a9f414 --- /dev/null +++ b/api/README.md @@ -0,0 +1,31 @@ +# DeFlock API + +A Fastify-based API service for DeFlock handling non-OSM related backend logic. + +## Endpoints +- `/geocode?query=...` — Geocode a location +- `/sponsors/github?username=...` — Get GitHub sponsors +- `/healthcheck` — Health check + +## Development + +### Prerequisites +- [Bun](https://bun.sh/) installed + +### Install dependencies +```sh +bun install +``` + +### Run locally +```sh +bun server.ts +``` + +## Deployment + +Deployed via GitHub Actions on push to `master`. + +## Environment Variables +Create a `.env` file in this directory with: +- `GITHUB_TOKEN` — Required for GitHub Sponsors endpoint diff --git a/api/bun.lock b/api/bun.lock new file mode 100644 index 0000000..d63ee02 --- /dev/null +++ b/api/bun.lock @@ -0,0 +1,177 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "dependencies": { + "@fastify/cors": "^10.0.0", + "@sinclair/typebox": "^0.34.48", + "cache-manager": "^7.2.8", + "cache-manager-fs-hash": "^3.0.0", + "fastify": "^5.7.2", + "pino-pretty": "^13.1.3", + }, + "devDependencies": { + "@types/cache-manager-fs-hash": "^0.0.5", + "@types/node": "^25.2.0", + }, + }, + }, + "packages": { + "@cacheable/utils": ["@cacheable/utils@2.3.3", "", { "dependencies": { "hashery": "^1.3.0", "keyv": "^5.5.5" } }, "sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A=="], + + "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.5", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A=="], + + "@fastify/cors": ["@fastify/cors@10.1.0", "", { "dependencies": { "fastify-plugin": "^5.0.0", "mnemonist": "0.40.0" } }, "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ=="], + + "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], + + "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="], + + "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="], + + "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], + + "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="], + + "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], + + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], + + "@types/cache-manager": ["@types/cache-manager@3.4.3", "", {}, "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw=="], + + "@types/cache-manager-fs-hash": ["@types/cache-manager-fs-hash@0.0.5", "", { "dependencies": { "@types/cache-manager": "<4" } }, "sha512-mSqk9YisfK/NkB/R5SzGeuSIVtwHhM5m6MLB0VrrFteTphKiQ2Fyz88IRtiX+SYEX6Nw2H3kB9qtpfnVSE/mSQ=="], + + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], + + "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="], + + "cache-manager": ["cache-manager@7.2.8", "", { "dependencies": { "@cacheable/utils": "^2.3.3", "keyv": "^5.5.5" } }, "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ=="], + + "cache-manager-fs-hash": ["cache-manager-fs-hash@3.0.0", "", { "dependencies": { "lockfile": "^1.0.4" } }, "sha512-uFl2EOuIdz5bLIjcRbR5cAxt9JdKQ5jQ6r7w5LXs1V+ls9I232nLVfaEHwK5h+isbV7j2z7ceaMe8lvr17BMVA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "fast-copy": ["fast-copy@4.0.2", "", {}, "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stringify": ["fast-json-stringify@6.2.0", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw=="], + + "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastify": ["fastify@5.7.2", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-dBJolW+hm6N/yJVf6J5E1BxOBNkuXNl405nrfeR8SpvGWG3aCC2XDHyiFBdow8Win1kj7sjawQc257JlYY6M/A=="], + + "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "find-my-way": ["find-my-way@9.4.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w=="], + + "hashery": ["hashery@1.4.0", "", { "dependencies": { "hookified": "^1.14.0" } }, "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="], + + "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "keyv": ["keyv@5.6.0", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw=="], + + "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], + + "lockfile": ["lockfile@1.0.4", "", { "dependencies": { "signal-exit": "^3.0.2" } }, "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mnemonist": ["mnemonist@0.40.0", "", { "dependencies": { "obliterator": "^2.0.4" } }, "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg=="], + + "obliterator": ["obliterator@2.0.5", "", {}, "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "pino": ["pino@10.3.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA=="], + + "pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="], + + "pino-pretty": ["pino-pretty@13.1.3", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^4.0.0", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^3.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg=="], + + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], + + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + + "thread-stream": ["thread-stream@4.0.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA=="], + + "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..4fa13b6 --- /dev/null +++ b/api/package.json @@ -0,0 +1,21 @@ +{ + "name": "deflock-api", + "version": "1.0.0", + "main": "server.ts", + "type": "module", + "scripts": { + "start": "bun server.ts" + }, + "dependencies": { + "@fastify/cors": "^10.0.0", + "@sinclair/typebox": "^0.34.48", + "cache-manager": "^7.2.8", + "cache-manager-fs-hash": "^3.0.0", + "fastify": "^5.7.2", + "pino-pretty": "^13.1.3" + }, + "devDependencies": { + "@types/cache-manager-fs-hash": "^0.0.5", + "@types/node": "^25.2.0" + } +} \ No newline at end of file diff --git a/api/server.ts b/api/server.ts new file mode 100644 index 0000000..c669d56 --- /dev/null +++ b/api/server.ts @@ -0,0 +1,127 @@ + +import Fastify, { FastifyInstance, FastifyError } from 'fastify'; +import cors from '@fastify/cors'; +import { NominatimClient, NominatimResultSchema } from './services/NominatimClient'; +import { GithubClient, SponsorsResponseSchema } from './services/GithubClient'; + +const start = async () => { + const server: FastifyInstance = Fastify({ + logger: { + level: 'error', + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname', + }, + }, + }, + trustProxy: true, + }); + + // Global error handler + server.setErrorHandler((error: FastifyError, request, reply) => { + server.log.error({ + url: request.url, + method: request.method, + error: error.message, + stack: error.stack, + }, 'Request error'); + + reply.status(error.statusCode || 500).send({ + error: 'Internal Server Error', + }); + }); + + // Coors Light Config + await server.register(cors, { + origin: (origin, cb) => { + const allowedOrigins = [ + 'http://localhost:5173', // vite dev server + 'https://deflock.me', + 'https://www.deflock.me', + 'https://deflock.org', // will migrate + 'https://www.deflock.org', // will migrate + ]; + + if (!origin || allowedOrigins.includes(origin) || /^https:\/\/.*\.deflock\.pages\.dev$/.test(origin)) { + cb(null, true); + } else { + cb(null, false); + } + }, + methods: ['GET', 'HEAD'], + }); + + const nominatim = new NominatimClient(); + const githubClient = new GithubClient(); + + const shutdown = async () => { + server.log.info("Shutting down"); + await server.close(); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + + server.get('/geocode', { + schema: { + querystring: { + type: 'object', + properties: { + query: { type: 'string' }, + }, + required: ['query'], + }, + response: { + 200: NominatimResultSchema, + 500: { type: 'object', properties: { error: { type: 'string' } } }, + }, + }, + }, async (request, reply) => { + const { query } = request.query as { query: string }; + reply.header('Cache-Control', 'public, max-age=300, s-maxage=86400'); + const result = await nominatim.geocodeSingleResult(query); + return result; + }); + + server.get('/sponsors/github', { + schema: { + querystring: { + type: 'object', + properties: { + username: { type: 'string', default: 'frillweeman' }, + }, + }, + response: { + 200: SponsorsResponseSchema, + 500: { type: 'object', properties: { error: { type: 'string' } } }, + }, + }, + }, async (request, reply) => { + const { username } = request.query as { username?: string }; + reply.header('Cache-Control', 'public, max-age=60, s-maxage=600'); + const result = await githubClient.getSponsors(username || 'frillweeman'); + return result; + }); + + server.head('/healthcheck', async (request, reply) => { + reply.status(200).send(); + }); + + try { + await server.listen({ host: '0.0.0.0', port: 3000 }); + console.log('Server listening on port 3000'); + } catch (err) { + console.error('Failed to start server:', err); + server.log.error(err); + process.exit(1); + } +}; + +start().catch(err => { + console.error('Fatal error:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/api/services/GithubClient.ts b/api/services/GithubClient.ts new file mode 100644 index 0000000..a268c15 --- /dev/null +++ b/api/services/GithubClient.ts @@ -0,0 +1,39 @@ + +import { Type, Static } from '@sinclair/typebox'; + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; +const graphQLEndpoint = 'https://api.github.com/graphql'; + +export const SponsorSchema = Type.Object({ + sponsor: Type.Object({ + avatarUrl: Type.String(), + login: Type.String(), + name: Type.Union([Type.String(), Type.Null()]), + url: Type.String(), + }), +}); + +export type Sponsor = Static; + +export const SponsorsResponseSchema = Type.Array(SponsorSchema); + +export class GithubClient { + async getSponsors(username: string): Promise { + const query = `query { user(login: \"${username}\") { sponsorshipsAsMaintainer(first: 100) { nodes { sponsor { login name avatarUrl url } } } } }`; + const body = JSON.stringify({ query, variables: '' }); + const response = await fetch(graphQLEndpoint, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${GITHUB_TOKEN}`, + 'User-Agent': 'Shotgun', + 'Content-Type': 'application/json', + }, + body, + }); + if (!response.ok) { + throw new Error(`Failed to get sponsors: ${response.status}`); + } + const json = await response.json(); + return json?.data?.user?.sponsorshipsAsMaintainer?.nodes || []; + } +} diff --git a/api/services/NominatimClient.ts b/api/services/NominatimClient.ts new file mode 100644 index 0000000..d0f377e --- /dev/null +++ b/api/services/NominatimClient.ts @@ -0,0 +1,88 @@ + +import { createCache, Cache } from 'cache-manager'; +import { Type, Static } from '@sinclair/typebox'; +const { DiskStore } = require('cache-manager-fs-hash'); + +export const NominatimResultSchema = Type.Object({ + addresstype: Type.String(), + boundingbox: Type.Tuple([Type.String(), Type.String(), Type.String(), Type.String()]), + class: Type.String(), + display_name: Type.String(), + geojson: Type.Object({ + coordinates: Type.Any(), + type: Type.String(), + }), + importance: Type.Number(), + lat: Type.String(), + licence: Type.String(), + lon: Type.String(), + name: Type.String(), + osm_id: Type.Number(), + osm_type: Type.String(), + place_id: Type.Number(), + place_rank: Type.Number(), + type: Type.String(), +}); + +export type NominatimResult = Static; + +const cache: Cache = createCache({ + stores: [new DiskStore({ + path: '/tmp/nominatim-cache', + ttl: 3600 * 24, // 24 hours + maxsize: 1000 * 1000 * 100, // 100MB + subdirs: true, + zip: false, + })] +}); + +export class NominatimClient { + baseUrl = 'https://nominatim.openstreetmap.org/search'; + + async geocodePhrase(query: string): Promise { + const cacheKey = `geocode:${query}`; + const cached = await cache.get(cacheKey); + if (cached) { + return cached as NominatimResult[]; + } + const url = `${this.baseUrl}?q=${encodeURIComponent(query)}&polygon_geojson=1&format=json`; + const response = await fetch(url, { + headers: { + 'User-Agent': 'DeFlock/1.1', + }, + }); + if (!response.ok) { + throw new Error(`Failed to geocode phrase: ${response.status}`); + } + const json = await response.json(); + await cache.set(cacheKey, json); + return json; + } + + async geocodeSingleResult(query: string): Promise { + const results = await this.geocodePhrase(query); + + if (!results.length) return null; + + const cityStatePattern = /(.+),\s*(\w{2})/; + const postalCodePattern = /\d{5}/; + + if (cityStatePattern.test(query)) { + const cityStateResults = results.filter((result: NominatimResult) => + ["city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", "borough"].includes(result.addresstype) + ); + if (cityStateResults.length) { + return cityStateResults[0]; + } + } + + if (postalCodePattern.test(query)) { + const postalCodeResults = results.filter((result: NominatimResult) => result.addresstype === "postcode"); + if (postalCodeResults.length) { + return postalCodeResults[0]; + } + } + + return results[0]; + } +} diff --git a/shotgun/.gitignore b/shotgun/.gitignore deleted file mode 100644 index 61bf345..0000000 --- a/shotgun/.gitignore +++ /dev/null @@ -1,70 +0,0 @@ -# -# Are you tempted to edit this file? -# -# First consider if the changes make sense for all, -# or if they are specific to your workflow/system. -# If it is the latter, you can augment this list with -# entries in .git/info/excludes -# -# see also test/files/.gitignore -# - -# -# JARs aren't checked in, they are fetched by sbt -# -/lib/*.jar -/test/files/codelib/*.jar -/test/files/lib/*.jar -/test/files/speclib/instrumented.jar -/tools/*.jar - -# Developer specific properties -/build.properties -/buildcharacter.properties - -# might get generated when testing Jenkins scripts locally -/jenkins.properties - -# target directory for build -/build/ - -# other -/out/ -/bin/ -/sandbox/ - -# intellij -/src/intellij*/*.iml -/src/intellij*/*.ipr -/src/intellij*/*.iws -**/.cache -/.idea -/.settings - -# vscode -/.vscode - -# Standard symbolic link to build/quick/bin -/qbin - -# sbt's target directories -/target/ -/project/**/target/ -/test/macro-annot/target/ -/test/files/target/ -/test/target/ -/build-sbt/ -local.sbt -jitwatch.out - -# Used by the restarr/restarrFull commands as target directories -/build-restarr/ -/target-restarr/ - -# metals -.metals -.bloop -project/**/metals.sbt - -.bsp -.history diff --git a/shotgun/build.sbt b/shotgun/build.sbt deleted file mode 100644 index 57a3edf..0000000 --- a/shotgun/build.sbt +++ /dev/null @@ -1,32 +0,0 @@ -import Dependencies._ - -ThisBuild / scalaVersion := "2.12.8" -ThisBuild / version := "0.1.0-SNAPSHOT" -ThisBuild / organization := "me.deflock" -ThisBuild / organizationName := "DeFlock" - -lazy val root = (project in file(".")) - .settings( - name := "shotgun", - libraryDependencies += scalaTest % Test, - ) - -val PekkoVersion = "1.0.3" -val PekkoHttpVersion = "1.0.1" -libraryDependencies ++= Seq( - "ch.qos.logback" % "logback-classic" % "1.5.6", - "org.slf4j" % "slf4j-api" % "2.0.12", - "org.apache.pekko" %% "pekko-actor-typed" % PekkoVersion, - "org.apache.pekko" %% "pekko-stream" % PekkoVersion, - "org.apache.pekko" %% "pekko-http" % PekkoHttpVersion, - "org.apache.pekko" %% "pekko-http-spray-json" % PekkoHttpVersion, - "org.apache.pekko" %% "pekko-http-cors" % PekkoHttpVersion, - "org.apache.pekko" %% "pekko-slf4j" % PekkoVersion, -) - -assembly / assemblyMergeStrategy := { - case PathList("module-info.class") => MergeStrategy.first - case x => - val oldStrategy = (assembly / assemblyMergeStrategy).value - oldStrategy(x) -} diff --git a/shotgun/project/Dependencies.scala b/shotgun/project/Dependencies.scala deleted file mode 100644 index 558929d..0000000 --- a/shotgun/project/Dependencies.scala +++ /dev/null @@ -1,5 +0,0 @@ -import sbt._ - -object Dependencies { - lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" -} diff --git a/shotgun/project/build.properties b/shotgun/project/build.properties deleted file mode 100644 index 3c0b78a..0000000 --- a/shotgun/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.9.1 diff --git a/shotgun/project/plugins.sbt b/shotgun/project/plugins.sbt deleted file mode 100644 index 7bc4622..0000000 --- a/shotgun/project/plugins.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") diff --git a/shotgun/src/main/resources/logback.xml b/shotgun/src/main/resources/logback.xml deleted file mode 100644 index e649a23..0000000 --- a/shotgun/src/main/resources/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - %d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n - - - - - - - diff --git a/shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala b/shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala deleted file mode 100644 index 466e965..0000000 --- a/shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala +++ /dev/null @@ -1,123 +0,0 @@ -package me.deflock.shotgun - -import org.apache.pekko -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.event.Logging -import org.apache.pekko.http.cors.scaladsl.CorsDirectives.cors -import org.apache.pekko.http.cors.scaladsl.model.HttpOriginMatcher -import org.apache.pekko.http.cors.scaladsl.settings.CorsSettings -import org.apache.pekko.http.scaladsl.model.headers.{HttpOrigin, `Access-Control-Allow-Origin`} -import pekko.http.scaladsl.Http -import pekko.http.scaladsl.model._ -import pekko.http.scaladsl.server.Directives.{path, _} -import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ -import org.apache.pekko.http.scaladsl.server.RejectionHandler - -import java.net.URLEncoder -import java.nio.charset.StandardCharsets -import scala.concurrent.ExecutionContextExecutor -import scala.io.StdIn -import org.slf4j.LoggerFactory - -object ShotgunServer { - - val logger = LoggerFactory.getLogger(getClass) - - def main(args: Array[String]): Unit = { - - implicit val system: ActorSystem = ActorSystem("my-system") - implicit val executionContext: ExecutionContextExecutor = system.dispatcher - val logging = Logging(system, getClass) - - val nominatim = new services.NominatimClient() - val githubClient = new services.GithubClient() - - // CORS - val allowedOrigins = List( - "http://localhost:8080", - "http://localhost:5173", - "https://deflock.me", - "https://www.deflock.me", - ).map(HttpOrigin(_)) // TODO: make this a config setting - val corsSettings = CorsSettings.default - .withAllowedOrigins(HttpOriginMatcher(allowedOrigins: _*)) - .withExposedHeaders(List(`Access-Control-Allow-Origin`.name)) - - val rejectionHandler = RejectionHandler.newBuilder() - .handleNotFound { - complete((StatusCodes.NotFound, "The requested resource could not be found.")) - } - .handle { - case corsRejection: org.apache.pekko.http.cors.scaladsl.CorsRejection => - complete((StatusCodes.Forbidden, "CORS rejection: Invalid origin")) - } - .result() - - val apiRoutes = pathPrefix("api") { - concat ( - path("geocode") { - get { - parameters("query".as[String]) { query => - val encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8.toString) - onSuccess(nominatim.geocodePhrase(encodedQuery)) { json => - complete(json) - } - } - } - }, - path("sponsors" / "github") { - get { - onSuccess(githubClient.getSponsors("frillweeman")) { json => - complete(json) - } - } - }, - path("oauth2" / "callback") { - get { - parameters(Symbol("code").?) { (code) => - complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "

Say hello to Pekko HTTP

Code: " + code.getOrElse("None") + "

")) - } - } - }, - path("healthcheck") { - get { - complete(HttpResponse(StatusCodes.OK, entity = "Service is healthy")) - } - head { - complete(StatusCodes.OK) - } - } - ) - } - - val spaRoutes = pathEndOrSingleSlash { - getFromFile("../webapp/dist/index.html") - } ~ getFromDirectory("../webapp/dist") ~ - path(Remaining) { _ => - getFromFile("../webapp/dist/index.html") - } - - val routes = handleRejections(rejectionHandler) { - cors(corsSettings) { - concat(apiRoutes, spaRoutes) - } - } - - val bindingFuture = Http().newServerAt("0.0.0.0", 8080).bind(routes) - - // Handle the binding future properly - bindingFuture.foreach { binding => - println(s"Server online at http://localhost:${binding.localAddress.getPort}/") - println("Press RETURN to stop...") - } - - StdIn.readLine() - - bindingFuture - .flatMap(_.unbind()) - .onComplete { _ => - println("Server shutting down...") - system.terminate() - } - } -} diff --git a/shotgun/src/main/scala/services/GithubClient.scala b/shotgun/src/main/scala/services/GithubClient.scala deleted file mode 100644 index fbd7cb9..0000000 --- a/shotgun/src/main/scala/services/GithubClient.scala +++ /dev/null @@ -1,69 +0,0 @@ -package services - -import org.apache.pekko.actor.ActorSystem -import org.apache.pekko.http.javadsl.model.headers.{Authorization, HttpCredentials, UserAgent} -import org.apache.pekko.http.scaladsl.Http -import org.apache.pekko.http.scaladsl.model.{HttpMethods, HttpRequest, StatusCodes} -import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshal -import spray.json.JsValue -import spray.json._ -import org.apache.pekko.http.scaladsl.model.ContentTypes -import org.apache.pekko.http.scaladsl.model.HttpEntity -import org.slf4j.LoggerFactory - -import scala.concurrent.{ExecutionContextExecutor, Future} - -class GithubClient(implicit val system: ActorSystem, implicit val executionContext: ExecutionContextExecutor) { - val logger = LoggerFactory.getLogger(getClass) - val graphQLEndpoint = "https://api.github.com/graphql" - private val githubApiToken = sys.env("GITHUB_TOKEN") - - def getSponsors(username: String): Future[JsArray] = { - - val query = s""" - |query { - | user(login: "$username") { - | sponsorshipsAsMaintainer(first: 100) { - | nodes { - | sponsor { - | login - | name - | avatarUrl - | url - | } - | } - | } - | } - |} - |""".stripMargin.replace("\n", " ").replace("\"", "\\\"") - - val jsonRequest = s"""{"query": "$query", "variables": ""}""" - val jsonEntity = HttpEntity(ContentTypes.`application/json`, jsonRequest) - val request = HttpRequest( - headers = List( - UserAgent.create("Shotgun"), - Authorization.create(HttpCredentials.create("Bearer", githubApiToken)) - ), - method = HttpMethods.POST, - uri = graphQLEndpoint, - entity = jsonEntity - ) - - Http().singleRequest(request).flatMap { response => - response.status match { - case StatusCodes.OK => - Unmarshal(response.entity).to[String].map { jsonString => - jsonString.parseJson.asJsObject - .fields("data").asJsObject - .fields("user").asJsObject - .fields("sponsorshipsAsMaintainer") - .asJsObject.fields("nodes") - .asInstanceOf[JsArray] - } - case _ => - response.discardEntityBytes() - Future.failed(new Exception(s"Failed to get sponsors: ${response.status}")) - } - } - } -} diff --git a/shotgun/src/main/scala/services/NominatimClient.scala b/shotgun/src/main/scala/services/NominatimClient.scala deleted file mode 100644 index 2ec6fcf..0000000 --- a/shotgun/src/main/scala/services/NominatimClient.scala +++ /dev/null @@ -1,53 +0,0 @@ -package services - -import org.apache.pekko -import org.apache.pekko.actor.ActorSystem -import pekko.http.scaladsl.Http -import pekko.http.scaladsl.model._ -import pekko.http.scaladsl.unmarshalling.Unmarshal -import spray.json._ - -import scala.collection.mutable -import scala.concurrent.{ExecutionContextExecutor, Future} - -class NominatimClient(implicit val system: ActorSystem, implicit val executionContext: ExecutionContextExecutor) { - val baseUrl = "https://nominatim.openstreetmap.org/search" - private val cache: mutable.LinkedHashMap[String, JsValue] = new mutable.LinkedHashMap[String, JsValue]() - private val maxCacheSize = 300 - - private def cleanUpCache(): Unit = { - if (cache.size > maxCacheSize) { - val oldest = cache.head - cache.remove(oldest._1) - } - } - - def geocodePhrase(query: String): Future[JsValue] = { - cleanUpCache() - cache.get(query) match { - case Some(cachedResult) => - println(s"Cache hit for $query") - Future.successful(cachedResult) - case _ => - println(s"Cache miss for $query") - val request = HttpRequest( - uri = s"$baseUrl?q=$query&polygon_geojson=1&format=json", - headers = List(headers.`User-Agent`("DeFlock/1.0")) - ) - - Http().singleRequest(request).flatMap { response => - response.status match { - case StatusCodes.OK => - Unmarshal(response.entity).to[String].map { jsonString => - val json = jsonString.parseJson - cache.put(query, json) - json - } - case _ => - response.discardEntityBytes() - Future.failed(new Exception(s"Failed to geocode phrase: ${response.status}")) - } - } - } - } -} diff --git a/shotgun/src/test/scala/example/HelloSpec.scala b/shotgun/src/test/scala/example/HelloSpec.scala deleted file mode 100644 index 56f5e66..0000000 --- a/shotgun/src/test/scala/example/HelloSpec.scala +++ /dev/null @@ -1,9 +0,0 @@ -package example - -import org.scalatest._ - -class HelloSpec extends FlatSpec with Matchers { - "The Hello object" should "say hello" in { - Hello.greeting shouldEqual "hello" - } -} diff --git a/webapp/src/services/apiService.ts b/webapp/src/services/apiService.ts index 6941987..c46e0f0 100644 --- a/webapp/src/services/apiService.ts +++ b/webapp/src/services/apiService.ts @@ -48,7 +48,7 @@ export class BoundingBox implements BoundingBoxLiteral { } const apiService = axios.create({ - baseURL: window.location.hostname === "localhost" ? "http://localhost:8080/api" : "/api", + baseURL: window.location.hostname === "localhost" ? "http://localhost:3000" : "https://api.deflock.org", headers: { "Content-Type": "application/json", }, @@ -82,49 +82,8 @@ export const getCities = async () => { return response.data; } -export const geocodeQuery = async (query: string, currentLocation: any) => { +export const geocodeQuery = async (query: string) => { const encodedQuery = encodeURIComponent(query); - const results = (await apiService.get(`/geocode?query=${encodedQuery}`)).data; - - function findNearestResult(results: any, currentLocation: any) { - let nearestResult = results[0]; - let nearestDistance = Number.MAX_VALUE; - for (const result of results) { - const distance = Math.sqrt( - Math.pow(result.lat - currentLocation.lat, 2) + - Math.pow(result.lon - currentLocation.lng, 2) - ); - if (distance < nearestDistance) { - nearestResult = result; - nearestDistance = distance; - } - } - return nearestResult; - } - - if (!results.length) return null; - - const cityStatePattern = /(.+),\s*(\w{2})/; - const postalCodePattern = /\d{5}/; - - if (cityStatePattern.test(query)) { - console.debug("cityStatePattern"); - const cityStateResults = results.filter((result: any) => - ["city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", "borough"].includes(result.addresstype) - ); - if (cityStateResults.length) { - return findNearestResult(cityStateResults, currentLocation); - } - } - - if (postalCodePattern.test(query)) { - console.debug("postalCodePattern"); - const postalCodeResults = results.filter((result: any) => result.addresstype === "postcode"); - if (postalCodeResults.length) { - return findNearestResult(postalCodeResults, currentLocation); - } - } - - console.debug("defaultPattern"); - return findNearestResult(results, currentLocation); + const result = (await apiService.get(`/geocode?query=${encodedQuery}`)).data; + return result; }