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
|
**/*.bun
|
||||||
|
|
||||||
dist
|
dist
|
||||||
/store/
|
/store/
|
||||||
|
.env
|
||||||
@@ -9,6 +9,7 @@ declare module "bun" {
|
|||||||
interface Env {
|
interface Env {
|
||||||
PUBLIC_API_KEY: string;
|
PUBLIC_API_KEY: string;
|
||||||
PRIVATE_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 { endfieldCard } from "@/games/endfield";
|
||||||
import { card } from "@/games/genshin";
|
import { card } from "@/games/genshin";
|
||||||
|
|
||||||
|
import { webhookMerger } from "./merger";
|
||||||
|
|
||||||
export function v1routes(app: App) {
|
export function v1routes(app: App) {
|
||||||
app.group("/v1", (app) =>
|
app.group("/v1", (app) =>
|
||||||
app
|
app
|
||||||
.use(card)
|
.use(card)
|
||||||
|
.use(webhookMerger)
|
||||||
// .use(endfieldCard)
|
// .use(endfieldCard)
|
||||||
.get(
|
.get(
|
||||||
"/status/:status",
|
"/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