From 46b2bed37547b85a4535eecba49d1e6db5d03073 Mon Sep 17 00:00:00 2001 From: Masato Hoshino Date: Wed, 29 Jul 2026 19:34:54 +0900 Subject: [PATCH] fix(nextcloud-talk): reject reactions for disabled or unconfigured accounts (#112675) The react message-action handler dispatched straight to the sender without checking the resolved account, so a disabled Nextcloud Talk account (`enabled:false`) that still had a baseUrl/botSecret in config could keep emitting reactions. `describeMessageTool` already hides the tool for unconfigured accounts, but an explicit accountId can reach `handleAction` directly. Enforce the same enabled+configured gate at dispatch, mirroring the Signal reaction fix (#112607). Co-authored-by: Claude Opus 4.8 (1M context) --- .../src/message-actions.test.ts | 35 +++++++++++++++++++ .../nextcloud-talk/src/message-actions.ts | 12 +++++++ 2 files changed, 47 insertions(+) diff --git a/extensions/nextcloud-talk/src/message-actions.test.ts b/extensions/nextcloud-talk/src/message-actions.test.ts index 6bdbd97fa84e..9e4bd8dd3c2a 100644 --- a/extensions/nextcloud-talk/src/message-actions.test.ts +++ b/extensions/nextcloud-talk/src/message-actions.test.ts @@ -144,6 +144,41 @@ describe("nextcloudTalkMessageActions", () => { describe("handleAction", () => { const cfg = {} as CoreConfig; + beforeEach(() => { + // Dispatch now resolves the account and enforces the same enabled+configured + // gate as describeMessageTool, so react tests need a configured account. + hoisted.resolveNextcloudTalkAccount.mockReturnValue(configuredAccount); + }); + + it("rejects a disabled account before reaching the sender", async () => { + hoisted.resolveNextcloudTalkAccount.mockReturnValue(disabledAccount); + + await expect( + nextcloudTalkMessageActions.handleAction?.({ + channel: "nextcloud-talk", + action: "react", + params: { to: "room:abc123", messageId: "1", emoji: "👍" }, + cfg, + accountId: "work", + }), + ).rejects.toThrow(/is disabled or not configured/); + expect(hoisted.sendReactionNextcloudTalk).not.toHaveBeenCalled(); + }); + + it("rejects an unconfigured account before reaching the sender", async () => { + hoisted.resolveNextcloudTalkAccount.mockReturnValue(unconfiguredAccount); + + await expect( + nextcloudTalkMessageActions.handleAction?.({ + channel: "nextcloud-talk", + action: "react", + params: { to: "room:abc123", messageId: "1", emoji: "👍" }, + cfg, + }), + ).rejects.toThrow(/is disabled or not configured/); + expect(hoisted.sendReactionNextcloudTalk).not.toHaveBeenCalled(); + }); + it("invokes sendReactionNextcloudTalk with normalized params for the react action", async () => { const result = await nextcloudTalkMessageActions.handleAction?.({ channel: "nextcloud-talk", diff --git a/extensions/nextcloud-talk/src/message-actions.ts b/extensions/nextcloud-talk/src/message-actions.ts index 9cb23b8a7a05..0771bbc29659 100644 --- a/extensions/nextcloud-talk/src/message-actions.ts +++ b/extensions/nextcloud-talk/src/message-actions.ts @@ -49,6 +49,18 @@ export const nextcloudTalkMessageActions: ChannelMessageActionAdapter = { } if (action === "react") { + // describeMessageTool only offers `react` for enabled, configured accounts. + // An explicit accountId can still target a disabled or uncredentialed + // account, so enforce the same gate at dispatch before it reaches the + // sender — otherwise a disabled account with leftover credentials still + // emits reactions. + const account = resolveNextcloudTalkAccount({ cfg: cfg as CoreConfig, accountId }); + if (!isAccountConfigured(account)) { + throw new Error( + `Nextcloud Talk account "${account.accountId}" is disabled or not configured.`, + ); + } + const target = readStringParam(params, "to", { required: true, label: "to (room token)",