fix(commands): /kick and /ban failures are no longer silent (#216)
rateLimitedActions now collects non-429 failures (and a 429 that exhausted its retries) and returns them instead of swallowing them; existing callers ignore the return. /kick and /ban turn the list into a CommandError whose message names who and why, using the server's own sentence (MatrixError.data.error), never the URL-bearing toString(); RoomInput's toast shows it verbatim. Verified headless as a non-moderator: '/kick @alice' → "Could not kick @alice:localhost: You cannot kick user @alice:localhost."; '/ban @nobody @alice' → "Could not ban @nobody:localhost, @alice:localhost: You don't have permission to ban". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
isServerName,
|
||||
isUserId,
|
||||
rateLimitedActions,
|
||||
RateLimitedFailure,
|
||||
removeRoomIdFromMDirect,
|
||||
} from '../utils/matrix';
|
||||
import { useRoomNavigate } from './useRoomNavigate';
|
||||
@@ -138,6 +139,28 @@ export const parseTimestampFlag = (input: string): number | undefined => {
|
||||
|
||||
export type CommandExe = (payload: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* [Gitea #216] A command that partially or fully failed. `message` is written
|
||||
* for the user (the RoomInput toast shows it verbatim).
|
||||
*/
|
||||
export class CommandError extends Error {}
|
||||
|
||||
const throwCommandFailures = (
|
||||
verb: 'kick' | 'ban',
|
||||
failures: RateLimitedFailure<string>[],
|
||||
): void => {
|
||||
if (failures.length === 0) return;
|
||||
const who = failures.map((f) => f.item).join(', ');
|
||||
// MatrixError.data.error is the server's sentence ("You don't have
|
||||
// permission to kick"); fall back to the errcode, never the URL-bearing
|
||||
// toString().
|
||||
const reason =
|
||||
(failures[0].error.data as { error?: string } | undefined)?.error ??
|
||||
failures[0].error.errcode ??
|
||||
'unknown error';
|
||||
throw new CommandError(`Could not ${verb} ${who}: ${reason}`);
|
||||
};
|
||||
|
||||
export enum Command {
|
||||
Me = 'me',
|
||||
Notice = 'notice',
|
||||
@@ -292,11 +315,13 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: rateLimitedActions' to() helper (utils/matrix.ts) swallows every
|
||||
// non-429 error, so a failed kick (e.g. insufficient power level) resolves
|
||||
// silently here — the RoomInput exe() catch cannot surface it. Propagating
|
||||
// would require refactoring rateLimitedActions' shared error handling.
|
||||
await rateLimitedActions(users, (id) => mx.kick(room.roomId, id, reason));
|
||||
// [Gitea #216] rateLimitedActions collects per-user failures (it
|
||||
// used to swallow them); turn them into one CommandError so the
|
||||
// RoomInput toast can say who and why.
|
||||
const { failures } = await rateLimitedActions(users, (id) =>
|
||||
mx.kick(room.roomId, id, reason),
|
||||
);
|
||||
throwCommandFailures('kick', failures);
|
||||
},
|
||||
},
|
||||
[Command.Ban]: {
|
||||
@@ -318,9 +343,10 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
|
||||
});
|
||||
}
|
||||
|
||||
// See the /kick note: rateLimitedActions swallows non-429 errors, so a
|
||||
// failed ban resolves silently and can't be surfaced by the exe() catch.
|
||||
await rateLimitedActions(users, (id) => mx.ban(room.roomId, id, reason));
|
||||
const { failures } = await rateLimitedActions(users, (id) =>
|
||||
mx.ban(room.roomId, id, reason),
|
||||
);
|
||||
throwCommandFailures('ban', failures);
|
||||
},
|
||||
},
|
||||
[Command.UnBan]: {
|
||||
|
||||
Reference in New Issue
Block a user