diff --git a/extensions/matrix/src/matrix/actions/limits.ts b/extensions/matrix/src/matrix/actions/limits.ts new file mode 100644 index 0000000000..f18d9e2c05 --- /dev/null +++ b/extensions/matrix/src/matrix/actions/limits.ts @@ -0,0 +1,6 @@ +export function resolveMatrixActionLimit(raw: unknown, fallback: number): number { + if (typeof raw !== "number" || !Number.isFinite(raw)) { + return fallback; + } + return Math.max(1, Math.floor(raw)); +} diff --git a/extensions/matrix/src/matrix/actions/messages.ts b/extensions/matrix/src/matrix/actions/messages.ts index d9cfe37225..c32053a0e4 100644 --- a/extensions/matrix/src/matrix/actions/messages.ts +++ b/extensions/matrix/src/matrix/actions/messages.ts @@ -1,5 +1,6 @@ import { resolveMatrixRoomId, sendMessageMatrix } from "../send.js"; import { resolveActionClient } from "./client.js"; +import { resolveMatrixActionLimit } from "./limits.js"; import { summarizeMatrixRawEvent } from "./summary.js"; import { EventType, @@ -95,10 +96,7 @@ export async function readMatrixMessages( const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); - const limit = - typeof opts.limit === "number" && Number.isFinite(opts.limit) - ? Math.max(1, Math.floor(opts.limit)) - : 20; + const limit = resolveMatrixActionLimit(opts.limit, 20); const token = opts.before?.trim() || opts.after?.trim() || undefined; const dir = opts.after ? "f" : "b"; // @vector-im/matrix-bot-sdk uses doRequest for room messages diff --git a/extensions/matrix/src/matrix/actions/pins.ts b/extensions/matrix/src/matrix/actions/pins.ts index 7d466db652..52baf69fd1 100644 --- a/extensions/matrix/src/matrix/actions/pins.ts +++ b/extensions/matrix/src/matrix/actions/pins.ts @@ -4,28 +4,52 @@ import { fetchEventSummary, readPinnedEvents } from "./summary.js"; import { EventType, type MatrixActionClientOpts, + type MatrixActionClient, type MatrixMessageSummary, type RoomPinnedEventsEventContent, } from "./types.js"; +type ActionClient = MatrixActionClient["client"]; + +async function withResolvedPinRoom( + roomId: string, + opts: MatrixActionClientOpts, + run: (client: ActionClient, resolvedRoom: string) => Promise, +): Promise { + const { client, stopOnDone } = await resolveActionClient(opts); + try { + const resolvedRoom = await resolveMatrixRoomId(client, roomId); + return await run(client, resolvedRoom); + } finally { + if (stopOnDone) { + client.stop(); + } + } +} + +async function updateMatrixPins( + roomId: string, + messageId: string, + opts: MatrixActionClientOpts, + update: (current: string[]) => string[], +): Promise<{ pinned: string[] }> { + return await withResolvedPinRoom(roomId, opts, async (client, resolvedRoom) => { + const current = await readPinnedEvents(client, resolvedRoom); + const next = update(current); + const payload: RoomPinnedEventsEventContent = { pinned: next }; + await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload); + return { pinned: next }; + }); +} + export async function pinMatrixMessage( roomId: string, messageId: string, opts: MatrixActionClientOpts = {}, ): Promise<{ pinned: string[] }> { - const { client, stopOnDone } = await resolveActionClient(opts); - try { - const resolvedRoom = await resolveMatrixRoomId(client, roomId); - const current = await readPinnedEvents(client, resolvedRoom); - const next = current.includes(messageId) ? current : [...current, messageId]; - const payload: RoomPinnedEventsEventContent = { pinned: next }; - await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload); - return { pinned: next }; - } finally { - if (stopOnDone) { - client.stop(); - } - } + return await updateMatrixPins(roomId, messageId, opts, (current) => + current.includes(messageId) ? current : [...current, messageId], + ); } export async function unpinMatrixMessage( @@ -33,28 +57,16 @@ export async function unpinMatrixMessage( messageId: string, opts: MatrixActionClientOpts = {}, ): Promise<{ pinned: string[] }> { - const { client, stopOnDone } = await resolveActionClient(opts); - try { - const resolvedRoom = await resolveMatrixRoomId(client, roomId); - const current = await readPinnedEvents(client, resolvedRoom); - const next = current.filter((id) => id !== messageId); - const payload: RoomPinnedEventsEventContent = { pinned: next }; - await client.sendStateEvent(resolvedRoom, EventType.RoomPinnedEvents, "", payload); - return { pinned: next }; - } finally { - if (stopOnDone) { - client.stop(); - } - } + return await updateMatrixPins(roomId, messageId, opts, (current) => + current.filter((id) => id !== messageId), + ); } export async function listMatrixPins( roomId: string, opts: MatrixActionClientOpts = {}, ): Promise<{ pinned: string[]; events: MatrixMessageSummary[] }> { - const { client, stopOnDone } = await resolveActionClient(opts); - try { - const resolvedRoom = await resolveMatrixRoomId(client, roomId); + return await withResolvedPinRoom(roomId, opts, async (client, resolvedRoom) => { const pinned = await readPinnedEvents(client, resolvedRoom); const events = ( await Promise.all( @@ -68,9 +80,5 @@ export async function listMatrixPins( ) ).filter((event): event is MatrixMessageSummary => Boolean(event)); return { pinned, events }; - } finally { - if (stopOnDone) { - client.stop(); - } - } + }); } diff --git a/extensions/matrix/src/matrix/actions/reactions.ts b/extensions/matrix/src/matrix/actions/reactions.ts index fe80239609..e3d22c3fe0 100644 --- a/extensions/matrix/src/matrix/actions/reactions.ts +++ b/extensions/matrix/src/matrix/actions/reactions.ts @@ -1,5 +1,6 @@ import { resolveMatrixRoomId } from "../send.js"; import { resolveActionClient } from "./client.js"; +import { resolveMatrixActionLimit } from "./limits.js"; import { EventType, RelationType, @@ -9,6 +10,23 @@ import { type ReactionEventContent, } from "./types.js"; +function getReactionsPath(roomId: string, messageId: string): string { + return `/_matrix/client/v1/rooms/${encodeURIComponent(roomId)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`; +} + +async function listReactionEvents( + client: NonNullable, + roomId: string, + messageId: string, + limit: number, +): Promise { + const res = (await client.doRequest("GET", getReactionsPath(roomId, messageId), { + dir: "b", + limit, + })) as { chunk: MatrixRawEvent[] }; + return res.chunk; +} + export async function listMatrixReactions( roomId: string, messageId: string, @@ -17,18 +35,10 @@ export async function listMatrixReactions( const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); - const limit = - typeof opts.limit === "number" && Number.isFinite(opts.limit) - ? Math.max(1, Math.floor(opts.limit)) - : 100; - // @vector-im/matrix-bot-sdk uses doRequest for relations - const res = (await client.doRequest( - "GET", - `/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`, - { dir: "b", limit }, - )) as { chunk: MatrixRawEvent[] }; + const limit = resolveMatrixActionLimit(opts.limit, 100); + const chunk = await listReactionEvents(client, resolvedRoom, messageId, limit); const summaries = new Map(); - for (const event of res.chunk) { + for (const event of chunk) { const content = event.content as ReactionEventContent; const key = content["m.relates_to"]?.key; if (!key) { @@ -62,17 +72,13 @@ export async function removeMatrixReactions( const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); - const res = (await client.doRequest( - "GET", - `/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`, - { dir: "b", limit: 200 }, - )) as { chunk: MatrixRawEvent[] }; + const chunk = await listReactionEvents(client, resolvedRoom, messageId, 200); const userId = await client.getUserId(); if (!userId) { return { removed: 0 }; } const targetEmoji = opts.emoji?.trim(); - const toRemove = res.chunk + const toRemove = chunk .filter((event) => event.sender === userId) .filter((event) => { if (!targetEmoji) {