import { IContent, MatrixClient, Method } from 'matrix-js-sdk'; import type { ScheduledMessage } from '../state/scheduledMessages'; /** * Schedule a message via MSC4140 (delayed messages). * Synapse implementation: pass org.matrix.msc4140.delay on the standard * /v3/rooms send endpoint. The server stores the event and returns delay_id * instead of event_id. */ export async function scheduleMessage( mx: MatrixClient, roomId: string, content: IContent, sendAtMs: number, ): Promise { // MSC4140 delayed events are PUT straight to /send/m.room.message, bypassing // the SDK's encryptEventIfNeeded pipeline — the body would land on the server // (and later in the timeline) in the clear. Refuse rather than leak; the // composer also hides the Schedule button in encrypted rooms. if (mx.getRoom?.(roomId)?.hasEncryptionStateEvent()) { throw new Error('Scheduled messages are not supported in encrypted rooms.'); } // A past/near target floors at 1000ms (send ~immediately) — an intentional, // tested contract; the ScheduleMessageModal already guards ≥60s in the future. const delayMs = Math.max(1000, Math.round(sendAtMs - Date.now())); const txnId = `sched_${Date.now()}_${Math.random().toString(36).slice(2)}`; const path = `/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`; // Use standard v3 prefix with the MSC4140 delay query param. const res = (await mx.http.authedRequest( Method.Put, path, { 'org.matrix.msc4140.delay': delayMs, }, content, )) as { delay_id: string }; return res.delay_id; } /** * Cancel a scheduled message via MSC4140. */ export async function cancelScheduledMessage(mx: MatrixClient, delayId: string): Promise { const path = `/delayed_events/${encodeURIComponent(delayId)}`; await mx.http.authedRequest( Method.Post, path, undefined, { action: 'cancel' }, { prefix: '/_matrix/client/unstable/org.matrix.msc4140' }, ); } export async function restartScheduledMessage(mx: MatrixClient, delayId: string): Promise { const path = `/delayed_events/${encodeURIComponent(delayId)}`; await mx.http.authedRequest( Method.Post, path, undefined, { action: 'restart' }, { prefix: '/_matrix/client/unstable/org.matrix.msc4140' }, ); } /** * Send a scheduled message immediately via MSC4140 (`action: 'send'`) — the * server dispatches the stored delayed event now, as a normal timeline event, * instead of waiting for its timeout. */ export async function sendScheduledMessageNow(mx: MatrixClient, delayId: string): Promise { const path = `/delayed_events/${encodeURIComponent(delayId)}`; await mx.http.authedRequest( Method.Post, path, undefined, { action: 'send' }, { prefix: '/_matrix/client/unstable/org.matrix.msc4140' }, ); } type DelayedEventItem = { delay_id?: unknown; room_id?: unknown; type?: unknown; state_key?: unknown; content?: unknown; delay?: unknown; delay_ms?: unknown; running_since?: unknown; delayed_since_ts?: unknown; }; /** * [Gitea #322] Pure: the user's pending scheduled messages from an MSC4140 * `GET /delayed_events` response. Synapse answers `{ delayed_events: [...] }`; * the current MSC shape is `{ scheduled: [...] }`. Call-membership delayed * events share the endpoint, so only non-state `m.room.message`s are kept. */ export function parseDelayedEvents(res: unknown): ScheduledMessage[] { if (!res || typeof res !== 'object') return []; const r = res as { delayed_events?: unknown; scheduled?: unknown }; const list = Array.isArray(r.delayed_events) ? r.delayed_events : r.scheduled; if (!Array.isArray(list)) return []; const out: ScheduledMessage[] = []; list.forEach((raw: DelayedEventItem) => { if (!raw || typeof raw !== 'object') return; if (raw.type !== 'm.room.message' || raw.state_key !== undefined) return; if (typeof raw.delay_id !== 'string' || typeof raw.room_id !== 'string') return; if (!raw.content || typeof raw.content !== 'object') return; const delay = Number(raw.delay ?? raw.delay_ms); const since = Number(raw.running_since ?? raw.delayed_since_ts); if (!Number.isFinite(delay) || !Number.isFinite(since)) return; out.push({ delayId: raw.delay_id, roomId: raw.room_id, content: raw.content as IContent, sendAt: since + delay, }); }); return out; } /** The user's pending scheduled messages, as the server has them. */ export async function fetchScheduledMessages(mx: MatrixClient): Promise { const res = await mx.http.authedRequest(Method.Get, '/delayed_events', undefined, undefined, { prefix: '/_matrix/client/unstable/org.matrix.msc4140', }); return parseDelayedEvents(res); } /** Group by room, the shape of `scheduledMessagesAtom`. */ export function groupScheduledByRoom(msgs: ScheduledMessage[]): Map { const map = new Map(); msgs.forEach((m) => map.set(m.roomId, [...(map.get(m.roomId) ?? []), m])); map.forEach((list) => list.sort((a, b) => a.sendAt - b.sendAt)); return map; }