dsh-im-ops/plugin-src/host/delivery-rpc.mjs
2026-08-30 11:24:53 +08:00

158 lines
5.7 KiB
JavaScript

import { resolveRpcAuthority } from './rpc-authority.mjs';
export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
export const DELIVERY_TEST_MESSAGE = 'DSH-IM 主动投递测试成功。';
export const DELIVERY_ENDPOINTS = Object.freeze({
send: 'message.send',
listTargets: 'target.list',
listSuggestions: 'target.suggestion.list',
createTarget: 'target.create',
updateTarget: 'target.update',
deleteTarget: 'target.delete',
testTarget: 'target.test',
});
const ENDPOINTS = new Set(Object.values(DELIVERY_ENDPOINTS));
const PUBLIC_ERRORS = new Set([
'bad-request',
'unknown-bot',
'unknown-target',
'target-conflict',
'invalid-target',
'bot-not-connected',
'target-rejected',
'delivery-failed',
'cancelled',
]);
function isRecord(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function exactKeys(value, keys) {
return isRecord(value)
&& Object.keys(value).length === keys.length
&& Object.keys(value).every((key) => keys.includes(key));
}
function validBotId(value) {
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
}
function validTargetId(value) {
return typeof value === 'string' && /^[A-Za-z0-9._:@-]{1,128}$/.test(value);
}
function validTarget(value, { targetId }) {
const keys = targetId ? ['targetId', 'name', 'kind', 'route'] : ['name', 'kind', 'route'];
if (!isRecord(value) || Object.keys(value).some((key) => !keys.includes(key))) return false;
if (targetId && !validTargetId(value.targetId)) return false;
if (value.name !== undefined
&& (typeof value.name !== 'string' || !value.name.trim() || value.name.trim().length > 80)) return false;
return typeof value.kind === 'string' && /^[a-z][a-z0-9-]{0,31}$/.test(value.kind)
&& isRecord(value.route);
}
function validDraftTarget(value) {
return exactKeys(value, ['kind', 'route'])
&& typeof value.kind === 'string' && /^[a-z][a-z0-9-]{0,31}$/.test(value.kind)
&& isRecord(value.route);
}
function validPayload(endpoint, payload) {
if (!ENDPOINTS.has(endpoint) || !isRecord(payload)) return false;
if (endpoint === DELIVERY_ENDPOINTS.send) {
return exactKeys(payload, ['botId', 'targetId', 'text'])
&& validBotId(payload.botId) && validTargetId(payload.targetId)
&& typeof payload.text === 'string' && Boolean(payload.text.trim());
}
if (endpoint === DELIVERY_ENDPOINTS.listTargets
|| endpoint === DELIVERY_ENDPOINTS.listSuggestions) {
return exactKeys(payload, ['botId']) && validBotId(payload.botId);
}
if (endpoint === DELIVERY_ENDPOINTS.createTarget) {
return exactKeys(payload, ['botId', 'target'])
&& validBotId(payload.botId) && validTarget(payload.target, { targetId: true });
}
if (endpoint === DELIVERY_ENDPOINTS.updateTarget) {
return exactKeys(payload, ['botId', 'targetId', 'target'])
&& validBotId(payload.botId) && validTargetId(payload.targetId)
&& validTarget(payload.target, { targetId: false });
}
if (endpoint === DELIVERY_ENDPOINTS.testTarget) {
return (exactKeys(payload, ['botId', 'targetId'])
&& validBotId(payload.botId) && validTargetId(payload.targetId))
|| (exactKeys(payload, ['botId', 'target'])
&& validBotId(payload.botId) && validDraftTarget(payload.target));
}
return exactKeys(payload, ['botId', 'targetId'])
&& validBotId(payload.botId) && validTargetId(payload.targetId);
}
function publicError(error) {
const code = PUBLIC_ERRORS.has(error?.code) ? error.code : 'delivery-failed';
return { code, message: code, details: {} };
}
export function createDeliveryRpcHandler(service) {
for (const method of [
'send',
'listTargets',
'listSuggestions',
'createTarget',
'updateTarget',
'deleteTarget',
]) {
if (typeof service?.[method] !== 'function') {
throw new TypeError(`A complete delivery service is required (${method})`);
}
}
return async (endpoint, payload, signal) => {
if (!validPayload(endpoint, payload)) {
return {
ok: false,
error: { code: 'bad-request', message: 'Invalid delivery request.', details: {} },
};
}
if (signal?.aborted) {
return { ok: false, error: { code: 'cancelled', message: 'cancelled', details: {} } };
}
try {
let value;
if (endpoint === DELIVERY_ENDPOINTS.send) {
value = await service.send(payload.botId, payload.targetId, payload.text, { signal });
} else if (endpoint === DELIVERY_ENDPOINTS.listTargets) {
value = await service.listTargets(payload.botId);
} else if (endpoint === DELIVERY_ENDPOINTS.listSuggestions) {
value = await service.listSuggestions(payload.botId);
} else if (endpoint === DELIVERY_ENDPOINTS.createTarget) {
value = await service.createTarget(payload.botId, payload.target);
} else if (endpoint === DELIVERY_ENDPOINTS.updateTarget) {
value = await service.updateTarget(payload.botId, payload.targetId, payload.target);
} else if (endpoint === DELIVERY_ENDPOINTS.deleteTarget) {
value = await service.deleteTarget(payload.botId, payload.targetId);
} else {
value = await service.send(
payload.botId,
Object.hasOwn(payload, 'target') ? payload.target : payload.targetId,
DELIVERY_TEST_MESSAGE,
{ signal },
);
}
return { ok: true, value };
} catch (error) {
return { ok: false, error: publicError(error) };
}
};
}
export function installDeliveryRpc(ctx, service, { authority } = {}) {
if (!ctx?.connection?.rpc || typeof ctx.connection.rpc.handle !== 'function') {
throw new TypeError('DSH Host Connection RPC is required');
}
return ctx.connection.rpc.handle(
DELIVERY_RPC_CHANNEL,
createDeliveryRpcHandler(service),
{ authority: resolveRpcAuthority(authority) },
);
}