diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b54b78cf7e2..c5c50025a11 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -735,6 +735,8 @@ "Not a valid %(brand)s keyfile": "Not a valid %(brand)s keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", "Unrecognised address": "Unrecognised address", + "Unban": "Unban", + "User cannot be invited until they are unbanned": "User cannot be invited until they are unbanned", "You do not have permission to invite people to this space.": "You do not have permission to invite people to this space.", "You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.", "User is already invited to the space": "User is already invited to the space", @@ -1706,7 +1708,6 @@ "Upload custom sound": "Upload custom sound", "Browse": "Browse", "Failed to unban": "Failed to unban", - "Unban": "Unban", "Banned by %(displayName)s": "Banned by %(displayName)s", "Reason": "Reason", "Error changing power level requirement": "Error changing power level requirement", diff --git a/src/utils/MultiInviter.ts b/src/utils/MultiInviter.ts index 31ca6f87b54..0066da81f31 100644 --- a/src/utils/MultiInviter.ts +++ b/src/utils/MultiInviter.ts @@ -26,6 +26,7 @@ import { _t } from "../languageHandler"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog"; +import ConfirmUserActionDialog from "../components/views/dialogs/ConfirmUserActionDialog"; export enum InviteState { Invited = "invited", @@ -48,6 +49,7 @@ export type CompletionStates = Record; const USER_ALREADY_JOINED = "IO.ELEMENT.ALREADY_JOINED"; const USER_ALREADY_INVITED = "IO.ELEMENT.ALREADY_INVITED"; +const USER_BANNED = "IO.ELEMENT.BANNED"; /** * Invites multiple addresses to a room, handling rate limiting from the server @@ -170,6 +172,34 @@ export default class MultiInviter { errcode: USER_ALREADY_INVITED, error: "Member already invited", }); + } else if (member?.membership === "ban") { + let proceed = false; + // Check if we can unban the invitee. + // See https://spec.matrix.org/v1.7/rooms/v10/#authorization-rules, particularly 4.5.3 and 4.5.4. + const ourMember = room.getMember(this.matrixClient.getSafeUserId()); + if ( + !!ourMember && + member.powerLevel < ourMember.powerLevel && + room.currentState.hasSufficientPowerLevelFor("ban", ourMember.powerLevel) && + room.currentState.hasSufficientPowerLevelFor("kick", ourMember.powerLevel) + ) { + const { finished } = Modal.createDialog(ConfirmUserActionDialog, { + member, + action: _t("Unban"), + title: _t("User cannot be invited until they are unbanned"), + }); + [proceed = false] = await finished; + if (proceed) { + await this.matrixClient.unban(roomId, member.userId); + } + } + + if (!proceed) { + throw new MatrixError({ + errcode: USER_BANNED, + error: "Member is banned", + }); + } } if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) { @@ -268,6 +298,7 @@ export default class MultiInviter { } break; case "M_BAD_STATE": + case USER_BANNED: errorText = _t("The user must be unbanned before they can be invited."); break; case "M_UNSUPPORTED_ROOM_VERSION": diff --git a/test/utils/MultiInviter-test.ts b/test/utils/MultiInviter-test.ts index 4e63407119b..d92710bd2a0 100644 --- a/test/utils/MultiInviter-test.ts +++ b/test/utils/MultiInviter-test.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { mocked } from "jest-mock"; -import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixError, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import Modal, { ComponentType, ComponentProps } from "../../src/Modal"; @@ -23,6 +23,7 @@ import SettingsStore from "../../src/settings/SettingsStore"; import MultiInviter, { CompletionStates } from "../../src/utils/MultiInviter"; import * as TestUtilsMatrix from "../test-utils"; import AskInviteAnywayDialog from "../../src/components/views/dialogs/AskInviteAnywayDialog"; +import ConfirmUserActionDialog from "../../src/components/views/dialogs/ConfirmUserActionDialog"; const ROOMID = "!room:server"; @@ -89,6 +90,7 @@ describe("MultiInviter", () => { client.getProfileInfo.mockImplementation((userId: string) => { return MXID_PROFILE_STATES[userId] || Promise.reject(); }); + client.unban = jest.fn(); inviter = new MultiInviter(client, ROOMID); }); @@ -154,5 +156,36 @@ describe("MultiInviter", () => { `"Cannot invite user by email without an identity server. You can connect to one under "Settings"."`, ); }); + + it("should ask if user wants to unban user if they have permission", async () => { + mocked(Modal.createDialog).mockImplementation( + (Element: ComponentType, props?: ComponentProps): any => { + // We stub out the modal with an immediate affirmative (proceed) return + return { finished: Promise.resolve([true]) }; + }, + ); + + const room = new Room(ROOMID, client, client.getSafeUserId()); + mocked(client.getRoom).mockReturnValue(room); + const ourMember = new RoomMember(ROOMID, client.getSafeUserId()); + ourMember.membership = "join"; + ourMember.powerLevel = 100; + const member = new RoomMember(ROOMID, MXID1); + member.membership = "ban"; + member.powerLevel = 0; + room.getMember = (userId: string) => { + if (userId === client.getSafeUserId()) return ourMember; + if (userId === MXID1) return member; + return null; + }; + + await inviter.invite([MXID1]); + expect(Modal.createDialog).toHaveBeenCalledWith(ConfirmUserActionDialog, { + member, + title: "User cannot be invited until they are unbanned", + action: "Unban", + }); + expect(client.unban).toHaveBeenCalledWith(ROOMID, MXID1); + }); }); });