commit a28f9a90c461354198d00eedd3f32cf35d777635 Author: Ruthenic Date: Mon Nov 14 19:20:13 2022 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93e7aa6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config.json +deno.lock diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5010254 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "deno.enable": true, + "deno.unstable": true, + "editor.formatOnSave": true, + "deno.importMap": "import_map.json", + "[typescript]": { + "editor.tabSize": 4, + "editor.detectIndentation": false, + "editor.defaultFormatter": "denoland.vscode-deno" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3acaf70 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# Wackylytics Server + +The server for the wackiest (and jankiest) analytics suite ever. + +## FAQ + +**Q**: Should I use this?\ +**A**: No. (seriously, don't) ((your cartoon bendy privileges will be taken away if you do)) diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..f7037de --- /dev/null +++ b/deno.json @@ -0,0 +1,8 @@ +{ + "fmt": { + "options": { + "indentWidth": 4 + } + }, + "importMap": "import_map.json" +} diff --git a/import_map.json b/import_map.json new file mode 100644 index 0000000..ac7b2ea --- /dev/null +++ b/import_map.json @@ -0,0 +1,7 @@ +{ + "imports": { + "@oak/": "https://deno.land/x/oak@v11.1.0/", + "@std/": "https://deno.land/std@0.163.0/", + "@surrealdb/": "https://deno.land/x/surrealdb@v0.5.0/" + } +} diff --git a/main.ts b/main.ts new file mode 100644 index 0000000..99f18c5 --- /dev/null +++ b/main.ts @@ -0,0 +1,18 @@ +import init from "./src/index.ts"; + +const app = await init(); + +app.addEventListener("listen", ({ + hostname, + port, +}) => { + console.log( + `Wackylytics server listening on http://${ + hostname ?? "localhost" + }:${port}`, + ); +}); + +await app.listen({ + port: Number(Deno.env.get("PORT")) ?? 8080, +}); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..392a618 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,13 @@ +import config from "../config.json" assert { type: "json" }; + +export type configType = typeof config; + +export interface Site { + metadata: { + description: string; + }; +} + +export { config }; + +export default config; diff --git a/src/database.ts b/src/database.ts new file mode 100644 index 0000000..42ab555 --- /dev/null +++ b/src/database.ts @@ -0,0 +1,24 @@ +import Surreal from "@surrealdb/mod.ts"; +import config from "./config.ts"; +import { Location } from "./utils.ts"; + +export interface IP { + location: Location; + history: { + timestamp: Date; + path: string; + }[]; +} + +export const db = new Surreal(`${config.db.url}/rpc`); + +export default async function init() { + try { + await db.signin({ + user: config.db.user, + pass: config.db.pass, + }); + } catch (e) { + throw new Error(e); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2906e3a --- /dev/null +++ b/src/index.ts @@ -0,0 +1,92 @@ +import { Application, Router } from "@oak/mod.ts"; +import initDB, { db, IP } from "./database.ts"; +import { deviousLick, hash } from "./utils.ts"; +import config, { Site } from "./config.ts"; + +interface Value { + site: string; + id: string; + ip: string; + path: string; +} + +export default async function init() { + await initDB(); + + const router = new Router(); + + router.post("/add", async (ctx) => { + const value: Value = await ctx.request.body({ type: "json" }).value; + const hashedIP = await hash(value.ip); + const site: Site = (config.sites as Record)[value.site]; + + if (!site) { + ctx.response.status = 406; + ctx.response.body = "Unknown site!"; + + return; + } + + let ipDB: IP; + + await db.use(config.db.namespace, value.site); + + try { + ipDB = (await db.select("ip:" + hashedIP))[0] as IP; + } catch { + const geolocation = await deviousLick(value.ip); + + await db.create("ip:" + hashedIP, { + location: geolocation, + history: [], + }); + + ipDB = (await db.select("ip:" + hashedIP))[0] as IP; + } + + ipDB.history.push({ + timestamp: new Date(), + path: value.path, + }); + + console.log(`${hashedIP} ${value.path}`); + + // deno-lint-ignore no-explicit-any + await db.update(`ip:${hashedIP}`, ipDB as Record); + + ctx.response.status = 200; + ctx.response.body = "Successfully added request to history!"; + }); + + router.get("/sites", (ctx) => { + ctx.response.status = 200; + ctx.response.type = "application/json"; + ctx.response.body = JSON.stringify(config.sites); + }); + + router.get("/site/:site", (ctx) => { + const siteName: string = ctx.params.site; + const site = (config.sites as Record)[siteName]; + + ctx.response.status = 200; + ctx.response.type = "application/json"; + ctx.response.body = JSON.stringify(site); + }); + + router.get("/site/:site/analytics", async (ctx) => { + const siteName: string = ctx.params.site; + + await db.use(config.db.namespace, siteName); + + const database = await db.select("ip"); + + ctx.response.status = 200; + ctx.response.type = "application/json"; + ctx.response.body = JSON.stringify(database); + }); + + const app = new Application(); + app.use(router.routes()); + + return app; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..51623c3 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,43 @@ +import { crypto, toHashString } from "@std/crypto/mod.ts"; + +export interface Location { + country: string; + region: string; +} + +interface InternalLocation { + status: string; + country: string; + countryCode: string; + region: string; + regionName: string; + city: string; + zip: string; + lat: number; + lon: number; + timezone: string; + isp: string; + org: string; + as: string; + query: string; +} + +/** deviously lick (an ip's geolocation) */ +export async function deviousLick(ip: string): Promise { + const res = await fetch(`http://ip-api.com/json/${ip}`); + + const data: InternalLocation = await res.json(); + + return { + country: data.country, + region: data.regionName, + } as Location; +} + +export async function hash(str: string): Promise { + const digest = await crypto.subtle.digest( + "SHA3-256", + new TextEncoder().encode(str), + ); + return toHashString(digest, "hex"); +}