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) <noreply@anthropic.com>
This commit is contained in:
Masato Hoshino
2026-07-29 19:34:54 +09:00
committed by GitHub
parent af70829cdf
commit 46b2bed375
2 changed files with 47 additions and 0 deletions
@@ -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",
@@ -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)",