mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
132 lines
3.8 KiB
JavaScript
132 lines
3.8 KiB
JavaScript
import { createDeliveryRpcHandler, DELIVERY_ENDPOINTS } from './delivery-rpc.mjs';
|
|
|
|
export const DELIVERY_HTTP_PATH = '/api/dsh-im/delivery/messages';
|
|
|
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
|
|
const DELIVERY_ERROR_STATUS = Object.freeze({
|
|
'bad-request': 400,
|
|
'unknown-bot': 404,
|
|
'unknown-target': 404,
|
|
'target-conflict': 409,
|
|
'invalid-target': 422,
|
|
'bot-not-connected': 503,
|
|
'target-rejected': 422,
|
|
'delivery-failed': 502,
|
|
cancelled: 408,
|
|
});
|
|
|
|
class HttpRequestError extends Error {
|
|
constructor(status, code) {
|
|
super(code);
|
|
this.status = status;
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
function json(response, status, value, headers = {}) {
|
|
if (response.destroyed || response.writableEnded) return;
|
|
const body = JSON.stringify(value);
|
|
response.writeHead(status, {
|
|
'cache-control': 'no-store',
|
|
'content-type': 'application/json; charset=utf-8',
|
|
'content-length': String(Buffer.byteLength(body)),
|
|
...headers,
|
|
});
|
|
response.end(body);
|
|
}
|
|
|
|
function isJsonContentType(value) {
|
|
return typeof value === 'string'
|
|
&& value.split(';', 1)[0].trim().toLowerCase() === 'application/json';
|
|
}
|
|
|
|
async function readJsonBody(request) {
|
|
const declaredLength = Number(request.headers['content-length']);
|
|
if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {
|
|
throw new HttpRequestError(413, 'payload-too-large');
|
|
}
|
|
|
|
const chunks = [];
|
|
let length = 0;
|
|
for await (const chunk of request) {
|
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
length += buffer.length;
|
|
if (length > MAX_BODY_BYTES) {
|
|
throw new HttpRequestError(413, 'payload-too-large');
|
|
}
|
|
chunks.push(buffer);
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(Buffer.concat(chunks, length).toString('utf8'));
|
|
} catch {
|
|
throw new HttpRequestError(400, 'bad-request');
|
|
}
|
|
}
|
|
|
|
export function createDeliveryHttpHandler(service) {
|
|
const dispatch = createDeliveryRpcHandler(service);
|
|
return async (request, response) => {
|
|
if (request.method !== 'POST') {
|
|
json(response, 405, {
|
|
error: { code: 'method-not-allowed', message: 'method-not-allowed', details: {} },
|
|
}, { allow: 'POST' });
|
|
return;
|
|
}
|
|
if (!isJsonContentType(request.headers['content-type'])) {
|
|
json(response, 415, {
|
|
error: { code: 'unsupported-media-type', message: 'unsupported-media-type', details: {} },
|
|
});
|
|
return;
|
|
}
|
|
|
|
const abort = new AbortController();
|
|
const cancel = () => abort.abort();
|
|
const cancelClosedResponse = () => {
|
|
if (!response.writableEnded) cancel();
|
|
};
|
|
request.once('aborted', cancel);
|
|
response.once('close', cancelClosedResponse);
|
|
try {
|
|
const payload = await readJsonBody(request);
|
|
const result = await dispatch(DELIVERY_ENDPOINTS.send, payload, abort.signal);
|
|
if (result.ok) {
|
|
json(response, 200, result.value);
|
|
return;
|
|
}
|
|
json(response, DELIVERY_ERROR_STATUS[result.error.code] ?? 500, {
|
|
error: result.error,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof HttpRequestError) {
|
|
json(response, error.status, {
|
|
error: { code: error.code, message: error.code, details: {} },
|
|
});
|
|
return;
|
|
}
|
|
json(response, 500, {
|
|
error: { code: 'delivery-failed', message: 'delivery-failed', details: {} },
|
|
});
|
|
} finally {
|
|
request.off('aborted', cancel);
|
|
response.off('close', cancelClosedResponse);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function installDeliveryHttp(ctx, service) {
|
|
if (!ctx?.webServer || typeof ctx.webServer.register !== 'function'
|
|
|| typeof ctx.effect !== 'function') {
|
|
throw new TypeError('DSH Host WebServer is required');
|
|
}
|
|
const route = {
|
|
kind: 'exact',
|
|
path: DELIVERY_HTTP_PATH,
|
|
handler: createDeliveryHttpHandler(service),
|
|
};
|
|
return ctx.effect(
|
|
() => ctx.webServer.register(route),
|
|
`dsh-im: ${DELIVERY_HTTP_PATH}`,
|
|
);
|
|
}
|