Extract dashboard room message route
This commit is contained in:
165
src/web-dashboard-room-message-routes.test.ts
Normal file
165
src/web-dashboard-room-message-routes.test.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import type { NewMessage, RegisteredGroup } from './types.js';
|
||||||
|
import {
|
||||||
|
createRoomMessageIdCache,
|
||||||
|
handleRoomMessageRoute,
|
||||||
|
} from './web-dashboard-room-message-routes.js';
|
||||||
|
|
||||||
|
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
||||||
|
return new Response(JSON.stringify(value), {
|
||||||
|
...init,
|
||||||
|
headers: { 'content-type': 'application/json', ...init?.headers },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function roomMessageRequest(
|
||||||
|
roomJid: string,
|
||||||
|
body: unknown,
|
||||||
|
method = 'POST',
|
||||||
|
): Request {
|
||||||
|
return new Request(
|
||||||
|
`http://localhost/api/rooms/${encodeURIComponent(roomJid)}/messages`,
|
||||||
|
{
|
||||||
|
method,
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: method === 'POST' ? JSON.stringify(body) : undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDeps(
|
||||||
|
overrides: {
|
||||||
|
rooms?: Record<string, RegisteredGroup>;
|
||||||
|
existingMessage?: (chatJid: string, id: string) => boolean;
|
||||||
|
messages?: NewMessage[];
|
||||||
|
queued?: Array<{ chatJid: string; groupFolder: string }>;
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const messages = overrides.messages ?? [];
|
||||||
|
const queued = overrides.queued ?? [];
|
||||||
|
return {
|
||||||
|
enqueueMessageCheck: (chatJid: string, groupFolder: string) => {
|
||||||
|
queued.push({ chatJid, groupFolder });
|
||||||
|
},
|
||||||
|
loadRoomBindings: () =>
|
||||||
|
overrides.rooms ?? {
|
||||||
|
'dc:ops': {
|
||||||
|
name: '#ops',
|
||||||
|
folder: 'ops-room',
|
||||||
|
added_at: '2026-04-26T05:00:00.000Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageExists: overrides.existingMessage ?? (() => false),
|
||||||
|
rememberRoomMessageId: createRoomMessageIdCache(),
|
||||||
|
writeChatMetadata: (
|
||||||
|
_chatJid: string,
|
||||||
|
_timestamp: string,
|
||||||
|
_name?: string,
|
||||||
|
_channel?: string,
|
||||||
|
_isGroup?: boolean,
|
||||||
|
) => undefined,
|
||||||
|
writeMessage: (message: NewMessage) => {
|
||||||
|
messages.push(message);
|
||||||
|
},
|
||||||
|
messages,
|
||||||
|
queued,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('web dashboard room message routes', () => {
|
||||||
|
it('injects messages and deduplicates repeated request ids', async () => {
|
||||||
|
const messages: NewMessage[] = [];
|
||||||
|
const queued: Array<{ chatJid: string; groupFolder: string }> = [];
|
||||||
|
const deps = makeDeps({ messages, queued });
|
||||||
|
|
||||||
|
const first = await handleRoomMessageRoute({
|
||||||
|
...deps,
|
||||||
|
jsonResponse,
|
||||||
|
now: () => '2026-04-26T05:10:00.000Z',
|
||||||
|
request: roomMessageRequest('dc:ops', {
|
||||||
|
nickname: ' 눈쟁이 ',
|
||||||
|
requestId: 'compose 1',
|
||||||
|
text: ' run a dashboard check ',
|
||||||
|
}),
|
||||||
|
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||||
|
});
|
||||||
|
const second = await handleRoomMessageRoute({
|
||||||
|
...deps,
|
||||||
|
jsonResponse,
|
||||||
|
request: roomMessageRequest('dc:ops', {
|
||||||
|
requestId: 'compose 1',
|
||||||
|
text: 'second submit',
|
||||||
|
}),
|
||||||
|
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(first?.status).toBe(200);
|
||||||
|
expect(second?.status).toBe(200);
|
||||||
|
await expect(first?.json()).resolves.toMatchObject({
|
||||||
|
id: 'web-compose-1',
|
||||||
|
queued: true,
|
||||||
|
});
|
||||||
|
await expect(second?.json()).resolves.toMatchObject({
|
||||||
|
id: 'web-compose-1',
|
||||||
|
queued: false,
|
||||||
|
duplicate: true,
|
||||||
|
});
|
||||||
|
expect(messages).toHaveLength(1);
|
||||||
|
expect(messages[0]).toMatchObject({
|
||||||
|
chat_jid: 'dc:ops',
|
||||||
|
content: 'run a dashboard check',
|
||||||
|
id: 'web-compose-1',
|
||||||
|
message_source_kind: 'ipc_injected_human',
|
||||||
|
sender: 'web-dashboard',
|
||||||
|
sender_name: '눈쟁이',
|
||||||
|
timestamp: '2026-04-26T05:10:00.000Z',
|
||||||
|
});
|
||||||
|
expect(queued).toEqual([{ chatJid: 'dc:ops', groupFolder: 'ops-room' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles fall-through and invalid room message requests', async () => {
|
||||||
|
const deps = makeDeps({ rooms: {} });
|
||||||
|
|
||||||
|
const fallThrough = await handleRoomMessageRoute({
|
||||||
|
...deps,
|
||||||
|
jsonResponse,
|
||||||
|
request: new Request('http://localhost/api/overview'),
|
||||||
|
url: new URL('http://localhost/api/overview'),
|
||||||
|
});
|
||||||
|
expect(fallThrough).toBeNull();
|
||||||
|
|
||||||
|
const notConfigured = await handleRoomMessageRoute({
|
||||||
|
...deps,
|
||||||
|
enqueueMessageCheck: undefined,
|
||||||
|
jsonResponse,
|
||||||
|
request: roomMessageRequest('dc:ops', { text: 'hello' }),
|
||||||
|
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||||
|
});
|
||||||
|
expect(notConfigured?.status).toBe(503);
|
||||||
|
|
||||||
|
const empty = await handleRoomMessageRoute({
|
||||||
|
...deps,
|
||||||
|
jsonResponse,
|
||||||
|
request: roomMessageRequest('dc:ops', { text: ' ' }),
|
||||||
|
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||||
|
});
|
||||||
|
expect(empty?.status).toBe(400);
|
||||||
|
|
||||||
|
const missing = await handleRoomMessageRoute({
|
||||||
|
...deps,
|
||||||
|
jsonResponse,
|
||||||
|
request: roomMessageRequest('dc:missing', { text: 'hello' }),
|
||||||
|
url: new URL('http://localhost/api/rooms/dc%3Amissing/messages'),
|
||||||
|
});
|
||||||
|
expect(missing?.status).toBe(404);
|
||||||
|
|
||||||
|
const wrongMethod = await handleRoomMessageRoute({
|
||||||
|
...deps,
|
||||||
|
jsonResponse,
|
||||||
|
request: roomMessageRequest('dc:ops', null, 'GET'),
|
||||||
|
url: new URL('http://localhost/api/rooms/dc%3Aops/messages'),
|
||||||
|
});
|
||||||
|
expect(wrongMethod?.status).toBe(405);
|
||||||
|
});
|
||||||
|
});
|
||||||
175
src/web-dashboard-room-message-routes.ts
Normal file
175
src/web-dashboard-room-message-routes.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import type { NewMessage, RegisteredGroup } from './types.js';
|
||||||
|
|
||||||
|
type JsonResponse = (
|
||||||
|
value: unknown,
|
||||||
|
init?: ResponseInit,
|
||||||
|
request?: Request,
|
||||||
|
) => Response;
|
||||||
|
|
||||||
|
export type RoomMessageIdRememberer = (id: string) => boolean;
|
||||||
|
|
||||||
|
export interface RoomMessageRouteDependencies {
|
||||||
|
enqueueMessageCheck?: (chatJid: string, groupFolder: string) => void;
|
||||||
|
loadRoomBindings?: () => Record<string, RegisteredGroup>;
|
||||||
|
messageExists: (chatJid: string, id: string) => boolean;
|
||||||
|
rememberRoomMessageId: RoomMessageIdRememberer;
|
||||||
|
writeChatMetadata: (
|
||||||
|
chatJid: string,
|
||||||
|
timestamp: string,
|
||||||
|
name?: string,
|
||||||
|
channel?: string,
|
||||||
|
isGroup?: boolean,
|
||||||
|
) => void;
|
||||||
|
writeMessage: (message: NewMessage) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoomMessageRouteContext extends RoomMessageRouteDependencies {
|
||||||
|
jsonResponse: JsonResponse;
|
||||||
|
now?: () => string;
|
||||||
|
request: Request;
|
||||||
|
url: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
||||||
|
|
||||||
|
export function createRoomMessageIdCache(
|
||||||
|
limit = ROOM_MESSAGE_ID_CACHE_LIMIT,
|
||||||
|
): RoomMessageIdRememberer {
|
||||||
|
const ids: string[] = [];
|
||||||
|
const idSet = new Set<string>();
|
||||||
|
|
||||||
|
return (id: string): boolean => {
|
||||||
|
if (idSet.has(id)) return false;
|
||||||
|
idSet.add(id);
|
||||||
|
ids.push(id);
|
||||||
|
if (ids.length > limit) {
|
||||||
|
const oldest = ids.shift();
|
||||||
|
if (oldest) idSet.delete(oldest);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRoomMessagePath(pathname: string): string | null {
|
||||||
|
const match = pathname.match(/^\/api\/rooms\/([^/]+)\/messages$/);
|
||||||
|
if (!match) return null;
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(match[1]);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeRoomMessageRequestId(value: unknown): string | null {
|
||||||
|
if (typeof value !== 'string') return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
const safe = trimmed.replace(/[^A-Za-z0-9._:-]/g, '-').slice(0, 120);
|
||||||
|
return safe || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRoomMessageBody(request: Request): Promise<{
|
||||||
|
text: string;
|
||||||
|
requestId: string | null;
|
||||||
|
nickname: string | null;
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
const body = (await request.json()) as {
|
||||||
|
text?: unknown;
|
||||||
|
requestId?: unknown;
|
||||||
|
nickname?: unknown;
|
||||||
|
};
|
||||||
|
if (typeof body.text !== 'string') return null;
|
||||||
|
const text = body.text.trim();
|
||||||
|
if (!text) return null;
|
||||||
|
let nickname: string | null = null;
|
||||||
|
if (typeof body.nickname === 'string') {
|
||||||
|
const trimmed = body.nickname.trim().slice(0, 32);
|
||||||
|
if (trimmed) nickname = trimmed;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: text.length > 8000 ? text.slice(0, 8000) : text,
|
||||||
|
requestId: sanitizeRoomMessageRequestId(body.requestId),
|
||||||
|
nickname,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeWebMessageId(): string {
|
||||||
|
return `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeWebMessageIdFromRequest(requestId: string | null): string {
|
||||||
|
return requestId ? `web-${requestId}` : makeWebMessageId();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleRoomMessageRoute({
|
||||||
|
enqueueMessageCheck,
|
||||||
|
jsonResponse,
|
||||||
|
loadRoomBindings,
|
||||||
|
messageExists,
|
||||||
|
now,
|
||||||
|
rememberRoomMessageId,
|
||||||
|
request,
|
||||||
|
url,
|
||||||
|
writeChatMetadata,
|
||||||
|
writeMessage,
|
||||||
|
}: RoomMessageRouteContext): Promise<Response | null> {
|
||||||
|
const messageRoomJid = parseRoomMessagePath(url.pathname);
|
||||||
|
if (!messageRoomJid) return null;
|
||||||
|
|
||||||
|
if (request.method !== 'POST') {
|
||||||
|
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loadRoomBindings || !enqueueMessageCheck) {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: 'Room message injection is not configured' },
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readRoomMessageBody(request);
|
||||||
|
if (!body) {
|
||||||
|
return jsonResponse({ error: 'Message text is required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const binding = loadRoomBindings()[messageRoomJid];
|
||||||
|
if (!binding) {
|
||||||
|
return jsonResponse({ error: 'Room not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = now?.() ?? new Date().toISOString();
|
||||||
|
const id = makeWebMessageIdFromRequest(body.requestId);
|
||||||
|
if (body.requestId && messageExists(messageRoomJid, id)) {
|
||||||
|
return jsonResponse({ ok: true, id, queued: false, duplicate: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rememberRoomMessageId(`${messageRoomJid}:${id}`)) {
|
||||||
|
return jsonResponse({ ok: true, id, queued: false, duplicate: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeChatMetadata(
|
||||||
|
messageRoomJid,
|
||||||
|
timestamp,
|
||||||
|
binding.name,
|
||||||
|
'web-dashboard',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
writeMessage({
|
||||||
|
id,
|
||||||
|
chat_jid: messageRoomJid,
|
||||||
|
sender: 'web-dashboard',
|
||||||
|
sender_name: body.nickname ?? 'Web Dashboard',
|
||||||
|
content: body.text,
|
||||||
|
timestamp,
|
||||||
|
is_from_me: false,
|
||||||
|
is_bot_message: false,
|
||||||
|
message_source_kind: 'ipc_injected_human',
|
||||||
|
});
|
||||||
|
enqueueMessageCheck(messageRoomJid, binding.folder);
|
||||||
|
|
||||||
|
return jsonResponse({ ok: true, id, queued: true });
|
||||||
|
}
|
||||||
@@ -39,6 +39,10 @@ import type {
|
|||||||
ScheduledTask,
|
ScheduledTask,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { handleOverviewRoute } from './web-dashboard-overview-routes.js';
|
import { handleOverviewRoute } from './web-dashboard-overview-routes.js';
|
||||||
|
import {
|
||||||
|
createRoomMessageIdCache,
|
||||||
|
handleRoomMessageRoute,
|
||||||
|
} from './web-dashboard-room-message-routes.js';
|
||||||
import {
|
import {
|
||||||
handleRoomTimelineRoute,
|
handleRoomTimelineRoute,
|
||||||
startRoomsTimelineCacheRefresh,
|
startRoomsTimelineCacheRefresh,
|
||||||
@@ -53,7 +57,6 @@ import { handleSettingsRoute } from './web-dashboard-settings-routes.js';
|
|||||||
import { handleScheduledTaskRoute } from './web-dashboard-task-routes.js';
|
import { handleScheduledTaskRoute } from './web-dashboard-task-routes.js';
|
||||||
|
|
||||||
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
const DEFAULT_STATUS_MAX_AGE_MS = 10 * 60 * 1000;
|
||||||
const ROOM_MESSAGE_ID_CACHE_LIMIT = 500;
|
|
||||||
const STACK_RESTART_UNIT_NAME = 'ejclaw-stack-restart.service';
|
const STACK_RESTART_UNIT_NAME = 'ejclaw-stack-restart.service';
|
||||||
const MANAGED_SERVICE_CALLER_FALLBACK_MESSAGE =
|
const MANAGED_SERVICE_CALLER_FALLBACK_MESSAGE =
|
||||||
'Stack restart unit is not installed yet. Run setup service from an external shell before retrying from a managed EJClaw service.';
|
'Stack restart unit is not installed yet. Run setup service from an external shell before retrying from a managed EJClaw service.';
|
||||||
@@ -259,16 +262,6 @@ function parseInboxActionPath(pathname: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRoomMessagePath(pathname: string): string | null {
|
|
||||||
const match = pathname.match(/^\/api\/rooms\/([^/]+)\/messages$/);
|
|
||||||
if (!match) return null;
|
|
||||||
try {
|
|
||||||
return decodeURIComponent(match[1]);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePairedInboxTarget(
|
function parsePairedInboxTarget(
|
||||||
inboxId: string,
|
inboxId: string,
|
||||||
): { taskId: string; status: PairedTask['status'] } | null {
|
): { taskId: string; status: PairedTask['status'] } | null {
|
||||||
@@ -323,51 +316,6 @@ async function readInboxAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeRoomMessageRequestId(value: unknown): string | null {
|
|
||||||
if (typeof value !== 'string') return null;
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) return null;
|
|
||||||
const safe = trimmed.replace(/[^A-Za-z0-9._:-]/g, '-').slice(0, 120);
|
|
||||||
return safe || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readRoomMessageBody(request: Request): Promise<{
|
|
||||||
text: string;
|
|
||||||
requestId: string | null;
|
|
||||||
nickname: string | null;
|
|
||||||
} | null> {
|
|
||||||
try {
|
|
||||||
const body = (await request.json()) as {
|
|
||||||
text?: unknown;
|
|
||||||
requestId?: unknown;
|
|
||||||
nickname?: unknown;
|
|
||||||
};
|
|
||||||
if (typeof body.text !== 'string') return null;
|
|
||||||
const text = body.text.trim();
|
|
||||||
if (!text) return null;
|
|
||||||
let nickname: string | null = null;
|
|
||||||
if (typeof body.nickname === 'string') {
|
|
||||||
const trimmed = body.nickname.trim().slice(0, 32);
|
|
||||||
if (trimmed) nickname = trimmed;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
text: text.length > 8000 ? text.slice(0, 8000) : text,
|
|
||||||
requestId: sanitizeRoomMessageRequestId(body.requestId),
|
|
||||||
nickname,
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeWebMessageId(): string {
|
|
||||||
return `web-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeWebMessageIdFromRequest(requestId: string | null): string {
|
|
||||||
return requestId ? `web-${requestId}` : makeWebMessageId();
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeWebRunId(prefix: string): string {
|
function makeWebRunId(prefix: string): string {
|
||||||
return `web-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
return `web-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
}
|
}
|
||||||
@@ -528,23 +476,11 @@ export function createWebDashboardHandler(
|
|||||||
startRoomsTimelineCacheRefresh(roomsTimelineDeps);
|
startRoomsTimelineCacheRefresh(roomsTimelineDeps);
|
||||||
}
|
}
|
||||||
|
|
||||||
const seenRoomMessageIds: string[] = [];
|
const rememberRoomMessageId = createRoomMessageIdCache();
|
||||||
const seenRoomMessageIdSet = new Set<string>();
|
|
||||||
const dismissedInboxKeys = new Set<string>();
|
const dismissedInboxKeys = new Set<string>();
|
||||||
const recentServiceRestarts: ServiceRestartRecord[] = [];
|
const recentServiceRestarts: ServiceRestartRecord[] = [];
|
||||||
const activeServiceRestartTargets = new Set<string>();
|
const activeServiceRestartTargets = new Set<string>();
|
||||||
|
|
||||||
function rememberRoomMessageId(id: string): boolean {
|
|
||||||
if (seenRoomMessageIdSet.has(id)) return false;
|
|
||||||
seenRoomMessageIdSet.add(id);
|
|
||||||
seenRoomMessageIds.push(id);
|
|
||||||
if (seenRoomMessageIds.length > ROOM_MESSAGE_ID_CACHE_LIMIT) {
|
|
||||||
const oldest = seenRoomMessageIds.shift();
|
|
||||||
if (oldest) seenRoomMessageIdSet.delete(oldest);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isInboxItemDismissed(item: {
|
function isInboxItemDismissed(item: {
|
||||||
id: string;
|
id: string;
|
||||||
lastOccurredAt: string;
|
lastOccurredAt: string;
|
||||||
@@ -558,7 +494,6 @@ export function createWebDashboardHandler(
|
|||||||
return async (request: Request): Promise<Response> => {
|
return async (request: Request): Promise<Response> => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const actionInboxId = parseInboxActionPath(url.pathname);
|
const actionInboxId = parseInboxActionPath(url.pathname);
|
||||||
const messageRoomJid = parseRoomMessagePath(url.pathname);
|
|
||||||
|
|
||||||
const scheduledTaskRoute = await handleScheduledTaskRoute({
|
const scheduledTaskRoute = await handleScheduledTaskRoute({
|
||||||
url,
|
url,
|
||||||
@@ -725,63 +660,19 @@ export function createWebDashboardHandler(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageRoomJid) {
|
const roomMessageRoute = await handleRoomMessageRoute({
|
||||||
if (request.method !== 'POST') {
|
url,
|
||||||
return jsonResponse({ error: 'Method not allowed' }, { status: 405 });
|
request,
|
||||||
}
|
jsonResponse,
|
||||||
|
enqueueMessageCheck,
|
||||||
if (!loadRoomBindings || !enqueueMessageCheck) {
|
loadRoomBindings,
|
||||||
return jsonResponse(
|
messageExists,
|
||||||
{ error: 'Room message injection is not configured' },
|
rememberRoomMessageId,
|
||||||
{ status: 503 },
|
writeChatMetadata,
|
||||||
);
|
writeMessage,
|
||||||
}
|
now: opts.now,
|
||||||
|
});
|
||||||
const body = await readRoomMessageBody(request);
|
if (roomMessageRoute) return roomMessageRoute;
|
||||||
if (!body) {
|
|
||||||
return jsonResponse(
|
|
||||||
{ error: 'Message text is required' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const binding = loadRoomBindings()[messageRoomJid];
|
|
||||||
if (!binding) {
|
|
||||||
return jsonResponse({ error: 'Room not found' }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = opts.now?.() ?? new Date().toISOString();
|
|
||||||
const id = makeWebMessageIdFromRequest(body.requestId);
|
|
||||||
if (body.requestId && messageExists(messageRoomJid, id)) {
|
|
||||||
return jsonResponse({ ok: true, id, queued: false, duplicate: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rememberRoomMessageId(`${messageRoomJid}:${id}`)) {
|
|
||||||
return jsonResponse({ ok: true, id, queued: false, duplicate: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
writeChatMetadata(
|
|
||||||
messageRoomJid,
|
|
||||||
timestamp,
|
|
||||||
binding.name,
|
|
||||||
'web-dashboard',
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
writeMessage({
|
|
||||||
id,
|
|
||||||
chat_jid: messageRoomJid,
|
|
||||||
sender: 'web-dashboard',
|
|
||||||
sender_name: body.nickname ?? 'Web Dashboard',
|
|
||||||
content: body.text,
|
|
||||||
timestamp,
|
|
||||||
is_from_me: false,
|
|
||||||
is_bot_message: false,
|
|
||||||
message_source_kind: 'ipc_injected_human',
|
|
||||||
});
|
|
||||||
enqueueMessageCheck(messageRoomJid, binding.folder);
|
|
||||||
|
|
||||||
return jsonResponse({ ok: true, id, queued: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomTimelineRoute = handleRoomTimelineRoute({
|
const roomTimelineRoute = handleRoomTimelineRoute({
|
||||||
url,
|
url,
|
||||||
|
|||||||
Reference in New Issue
Block a user