feat: implement webhook merger
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
PRIVATE_API_KEY=key is not needed
|
||||
PUBLIC_API_KEY=key is not needed
|
||||
+2
-1
@@ -42,4 +42,5 @@ package-lock.json
|
||||
**/*.bun
|
||||
|
||||
dist
|
||||
/store/
|
||||
/store/
|
||||
.env
|
||||
@@ -9,6 +9,7 @@ declare module "bun" {
|
||||
interface Env {
|
||||
PUBLIC_API_KEY: string;
|
||||
PRIVATE_API_KEY: string;
|
||||
WH_ROUTE_MAP: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ import { t } from "elysia";
|
||||
// import { endfieldCard } from "@/games/endfield";
|
||||
import { card } from "@/games/genshin";
|
||||
|
||||
import { webhookMerger } from "./merger";
|
||||
|
||||
export function v1routes(app: App) {
|
||||
app.group("/v1", (app) =>
|
||||
app
|
||||
.use(card)
|
||||
.use(webhookMerger)
|
||||
// .use(endfieldCard)
|
||||
.get(
|
||||
"/status/:status",
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { App } from "@";
|
||||
import { env } from "bun";
|
||||
import { status } from "elysia";
|
||||
import { TTLCache } from "enka.ts";
|
||||
|
||||
const routesCache = new TTLCache();
|
||||
type Pending = {
|
||||
data: unknown;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const pending = new Map<string, Pending>();
|
||||
|
||||
export function webhookMerger(app: App) {
|
||||
return app.post("/webhook/:id", async ({ body, params: { id } }) => {
|
||||
const map = await getRoutes();
|
||||
const cur = map.find((e) => e[0] === id);
|
||||
|
||||
if (!cur) return status(404);
|
||||
|
||||
const [, url, bufferTime] = cur;
|
||||
|
||||
const existing = pending.get(id);
|
||||
|
||||
if (existing) {
|
||||
clearTimeout(existing.timer);
|
||||
|
||||
existing.data = merge(existing.data, body);
|
||||
existing.count++;
|
||||
|
||||
existing.timer = setTimeout(async () => {
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(existing.data),
|
||||
});
|
||||
} finally {
|
||||
pending.delete(id);
|
||||
}
|
||||
}, bufferTime);
|
||||
|
||||
return status(202);
|
||||
}
|
||||
|
||||
const state: Pending = {
|
||||
data: body,
|
||||
count: 1,
|
||||
timer: undefined as never,
|
||||
};
|
||||
|
||||
state.timer = setTimeout(async () => {
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(state.data),
|
||||
});
|
||||
} finally {
|
||||
pending.delete(id);
|
||||
}
|
||||
}, bufferTime);
|
||||
|
||||
pending.set(id, state);
|
||||
|
||||
return status(202);
|
||||
});
|
||||
}
|
||||
|
||||
type RouteMap = [string, string, number][];
|
||||
|
||||
async function getRoutes(): Promise<RouteMap> {
|
||||
const cached = routesCache.get(".");
|
||||
|
||||
if (cached) return cached as RouteMap;
|
||||
|
||||
const res = await fetch(
|
||||
env.WH_ROUTE_MAP,
|
||||
).then((r) => r.ok && r.json());
|
||||
|
||||
if (!res) throw new Error("cant get route map");
|
||||
routesCache.set(".", res, 900);
|
||||
return res;
|
||||
}
|
||||
|
||||
const CONCAT_FIELDS = new Set(["content"]);
|
||||
|
||||
function merge(a: unknown, b: unknown, key?: string): unknown {
|
||||
if (
|
||||
key &&
|
||||
CONCAT_FIELDS.has(key) &&
|
||||
typeof a === "string" &&
|
||||
typeof b === "string"
|
||||
) {
|
||||
return `${a}\n${b}`;
|
||||
}
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
return [...a, ...b];
|
||||
}
|
||||
|
||||
if (
|
||||
a &&
|
||||
b &&
|
||||
typeof a === "object" &&
|
||||
typeof b === "object" &&
|
||||
!Array.isArray(a) &&
|
||||
!Array.isArray(b)
|
||||
) {
|
||||
const out: Record<string, unknown> = {
|
||||
...(a as Record<string, unknown>),
|
||||
};
|
||||
|
||||
for (const [k, v] of Object.entries(b)) {
|
||||
out[k] = k in out ? merge(out[k], v, k) : v;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
Reference in New Issue
Block a user