diff --git a/.gitignore b/.gitignore index ed0d3c8..2ed831c 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,5 @@ typings/ # DynamoDB Local files .dynamodb/ + +*.sqlite \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f490d3c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM denoland/deno:alpine +EXPOSE 8000 +WORKDIR /app +COPY deno.* . +COPY public ./public +COPY src ./src +RUN deno cache src/server.ts +RUN deno eval --unstable-ffi "import '@db/sqlite'" +CMD ["run", "-A", "src/server.ts"] \ No newline at end of file diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..586a833 --- /dev/null +++ b/deno.json @@ -0,0 +1,12 @@ +{ + "tasks": { + "dev": "deno run --watch -A src/server.ts" + }, + "imports": { + "@db/sqlite": "jsr:@db/sqlite@^0.12.0", + "@deno-library/compress": "jsr:@deno-library/compress@^0.5.5", + "@std/assert": "jsr:@std/assert@1", + "h3-js": "npm:h3-js@^4.1.0", + "hono": "npm:hono@^4.6.19" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..7802fb9 --- /dev/null +++ b/deno.lock @@ -0,0 +1,149 @@ +{ + "version": "4", + "specifiers": { + "jsr:@db/sqlite@0.12": "0.12.0", + "jsr:@db/sqlite@0.12.0": "0.12.0", + "jsr:@deno-library/compress@~0.5.5": "0.5.5", + "jsr:@deno-library/crc32@1.0.2": "1.0.2", + "jsr:@denosaurs/plug@1": "1.0.6", + "jsr:@std/assert@0.217": "0.217.0", + "jsr:@std/assert@0.221": "0.221.0", + "jsr:@std/assert@1": "1.0.11", + "jsr:@std/bytes@^1.0.2": "1.0.4", + "jsr:@std/encoding@0.221": "0.221.0", + "jsr:@std/fmt@0.221": "0.221.0", + "jsr:@std/fs@0.221": "0.221.0", + "jsr:@std/fs@1.0.5": "1.0.5", + "jsr:@std/internal@^1.0.5": "1.0.5", + "jsr:@std/io@0.225.0": "0.225.0", + "jsr:@std/path@0.217": "0.217.0", + "jsr:@std/path@0.221": "0.221.0", + "jsr:@std/path@1.0.8": "1.0.8", + "jsr:@std/path@^1.0.7": "1.0.8", + "jsr:@std/streams@^1.0.7": "1.0.8", + "jsr:@std/tar@0.1.3": "0.1.3", + "jsr:@zip-js/zip-js@2.7.53": "2.7.53", + "npm:h3-js@*": "4.1.0", + "npm:h3-js@^4.1.0": "4.1.0", + "npm:hono@^4.6.19": "4.6.19" + }, + "jsr": { + "@db/sqlite@0.12.0": { + "integrity": "dd1ef7f621ad50fc1e073a1c3609c4470bd51edc0994139c5bf9851de7a6d85f", + "dependencies": [ + "jsr:@denosaurs/plug", + "jsr:@std/path@0.217" + ] + }, + "@deno-library/compress@0.5.5": { + "integrity": "18b651a33eac87d96ae8c941487045724a665d654e9d94120da43777393655d9", + "dependencies": [ + "jsr:@deno-library/crc32", + "jsr:@std/fs@1.0.5", + "jsr:@std/io", + "jsr:@std/path@1.0.8", + "jsr:@std/tar", + "jsr:@zip-js/zip-js" + ] + }, + "@deno-library/crc32@1.0.2": { + "integrity": "d2061bfee30c87c97f285dfca0fdc4458e632dc072a33ecfc73ca5177a5a39a0" + }, + "@denosaurs/plug@1.0.6": { + "integrity": "6cf5b9daba7799837b9ffbe89f3450510f588fafef8115ddab1ff0be9cb7c1a7", + "dependencies": [ + "jsr:@std/encoding", + "jsr:@std/fmt", + "jsr:@std/fs@0.221", + "jsr:@std/path@0.221" + ] + }, + "@std/assert@0.217.0": { + "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" + }, + "@std/assert@0.221.0": { + "integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a" + }, + "@std/assert@1.0.11": { + "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/bytes@1.0.4": { + "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" + }, + "@std/encoding@0.221.0": { + "integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45" + }, + "@std/fmt@0.221.0": { + "integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a" + }, + "@std/fs@0.221.0": { + "integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286", + "dependencies": [ + "jsr:@std/assert@0.221", + "jsr:@std/path@0.221" + ] + }, + "@std/fs@1.0.5": { + "integrity": "41806ad6823d0b5f275f9849a2640d87e4ef67c51ee1b8fb02426f55e02fd44e", + "dependencies": [ + "jsr:@std/path@^1.0.7" + ] + }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + }, + "@std/io@0.225.0": { + "integrity": "c1db7c5e5a231629b32d64b9a53139445b2ca640d828c26bf23e1c55f8c079b3", + "dependencies": [ + "jsr:@std/bytes" + ] + }, + "@std/path@0.217.0": { + "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", + "dependencies": [ + "jsr:@std/assert@0.217" + ] + }, + "@std/path@0.221.0": { + "integrity": "0a36f6b17314ef653a3a1649740cc8db51b25a133ecfe838f20b79a56ebe0095", + "dependencies": [ + "jsr:@std/assert@0.221" + ] + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + }, + "@std/streams@1.0.8": { + "integrity": "b41332d93d2cf6a82fe4ac2153b930adf1a859392931e2a19d9fabfb6f154fb3" + }, + "@std/tar@0.1.3": { + "integrity": "531270fc707b37ab9b5f051aa4943e7b16b86905e0398a4ebe062983b0c93115", + "dependencies": [ + "jsr:@std/streams" + ] + }, + "@zip-js/zip-js@2.7.53": { + "integrity": "acea5bd8e01feb3fe4c242cfbde7d33dd5e006549a4eb1d15283bc0c778ed672" + } + }, + "npm": { + "h3-js@4.1.0": { + "integrity": "sha512-LQhmMl1dRQQjMXPzJc7MpZ/CqPOWWuAvVEoVJM9n/s7vHypj+c3Pd5rLQCkAsOgAoAYKbNCsYFE++LF7MvSfCQ==" + }, + "hono@4.6.19": { + "integrity": "sha512-Xw5DwU2cewEsQ1DkDCdy6aBJkEBARl5loovoL1gL3/gw81RdaPbXrNJYp3LoQpzpJ7ECC/1OFi/vn3UZTLHFEw==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@db/sqlite@0.12", + "jsr:@deno-library/compress@~0.5.5", + "jsr:@std/assert@1", + "npm:h3-js@^4.1.0", + "npm:hono@^4.6.19" + ] + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..71cd1d2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,125 @@ + + + + + + NepMap + + + + + + +
+
+

Czas aktualizacji

+ +
+ +
+

Filtry sygnałów

+ + + + + +

(Zaznacz jakie sygnały mają być obecne)

+
+
+ +
+ + + diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..6f535e5 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,4 @@ +import { Database } from "jsr:@db/sqlite@0.12.0"; + +export const db = await new Database("./neomap.sqlite"); +db.prepare("CREATE TABLE IF NOT EXISTS hexes (hex_id TEXT PRIMARY KEY NOT NULL CHECK(hex_id GLOB '[0-9a-f]*'), wifi INTEGER DEFAULT 0 NOT NULL, gsm INTEGER DEFAULT 0 NOT NULL, wcdma INTEGER DEFAULT 0 NOT NULL, lte INTEGER DEFAULT 0 NOT NULL, ble INTEGER DEFAULT 0 NOT NULL, created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL, last_update INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL);").run(); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..74c1743 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,94 @@ +import h3 from "npm:h3-js"; +import { gunzip, gzip } from "@deno-library/compress"; +import { Hono } from "hono"; +import { serveStatic } from "hono/deno"; +import { logger } from "hono/logger"; + +import { db } from "./db.ts"; +import { Geosubmit } from "./types.d.ts"; + +const app = new Hono(); + +app.use(logger()); +app.use("/", serveStatic({ root: "./public" })); + +app.post("/api/v1/geosubmit", async (c) => { + const enconding = await c.req.header("Content-Encoding"); + if (enconding !== "gzip") { + return c.json({ status: 400, message: "Bad Request" }); + } + const body = await c.req.arrayBuffer(); + + const ftch = await fetch("https://api.beacondb.net/v2/geosubmit", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Content-Encoding": "gzip", + }, + body: body, + }); + if (ftch.status !== 200) { + return c.json({ status: ftch.status, message: "Bad Request" }); + } + + let json: Geosubmit; + const arr = new Uint8Array(body); + const data = await gunzip(arr); + json = JSON.parse(new TextDecoder().decode(data)); + json.items.forEach((item) => { + const timestamp = Math.floor(item.timestamp / 1000); + const hex = h3.latLngToCell(item.position.latitude, item.position.longitude, 11); + let hasGsm = false, + hasWcdma = false, + hasLte = false, + hasWifi = false, + hasBle = false; + if (item.cellTowers) { + item.cellTowers.forEach((cell) => { + if (hasGsm && hasWcdma && hasLte) return; + if (cell.radioType === "gsm") hasGsm = true; + if (cell.radioType === "wcdma") hasWcdma = true; + if (cell.radioType === "lte") hasLte = true; + }); + } + if (item.wifiAccessPoints) { + item.wifiAccessPoints.forEach((wifi) => { + if (hasWifi) return; + hasWifi = true; + }); + } + if (item.bluetoothBeacons) { + item.bluetoothBeacons.forEach((ble) => { + if (hasBle) return; + hasBle = true; + }); + } + + const isHexInDb = db.prepare("SELECT hex_id FROM hexes WHERE hex_id = ?").get(hex); + if (isHexInDb) { + db.prepare( + ` + UPDATE hexes + SET + wifi = MAX(wifi, ?), + gsm = MAX(gsm, ?), + wcdma = MAX(wcdma, ?), + lte = MAX(lte, ?), + ble = MAX(ble, ?), + last_update = ? + WHERE hex_id = ? + `, + ).run(hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp, hex); + } else { + db.prepare("INSERT INTO hexes (hex_id, wifi, gsm, wcdma, lte, ble, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)").run(hex, hasWifi, hasGsm, hasWcdma, hasLte, hasBle, timestamp); + } + }); + return c.json({ status: 200, message: "OK" }); +}); + +app.get("/api/v1/hexes", async (c) => { + const hexes = db.prepare("SELECT hex_id, wifi, gsm, wcdma, lte, ble, last_update FROM hexes").all(); + return c.json(hexes); +}); + +Deno.serve(app.fetch); diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..2f8f1ad --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,39 @@ +export type Geosubmit = { + items: { + timestamp: number; + position: { + latitude: number; + longitude: number; + accuracy: number; + age: number; + altitude: number; + altitudeAccuracy: number; + heading: number; + speed: number; + source: string; + }; + cellTowers?: { + radioType: "gsm" | "wcdma" | "lte"; + mobileCountryCode: number; + mobileNetworkCode: number; + age: number; + asu: number; + primaryScramblingCode: number; + serving: number; + signalStrength: number; + arfcn: number; + }[]; + wifiAccessPoints?: { + macAddress: string; + signalStrength: number; + channel: number; + ssid: string; + }[]; + bluetoothBeacons?: { + macAddress: string; + signalStrength: number; + age: number; + name: string; + }[]; + }[]; +};