diff --git a/cypress/e2e/integration-manager/kick.spec.ts b/cypress/e2e/integration-manager/kick.spec.ts new file mode 100644 index 0000000000..6901cd376b --- /dev/null +++ b/cypress/e2e/integration-manager/kick.spec.ts @@ -0,0 +1,256 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import { SynapseInstance } from "../../plugins/synapsedocker"; +import { MatrixClient } from "../../global"; +import { UserCredentials } from "../../support/login"; + +const ROOM_NAME = "Integration Manager Test"; +const USER_DISPLAY_NAME = "Alice"; +const BOT_DISPLAY_NAME = "Bob"; +const KICK_REASON = "Goodbye"; + +const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal"; +const INTEGRATION_MANAGER_HTML = ` + + + Fake Integration Manager + + + + + + + + + +`; + +function openIntegrationManager() { + cy.get(".mx_RightPanel_roomSummaryButton").click(); + cy.get(".mx_RoomSummaryCard_appsGroup").within(() => { + cy.contains("Add widgets, bridges & bots").click(); + }); +} + +function closeIntegrationManager(integrationManagerUrl: string) { + cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => { + cy.get("#close").should("exist").click(); + }); +} + +function sendActionFromIntegrationManager(integrationManagerUrl: string, targetRoomId: string, targetUserId: string) { + cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => { + cy.get("#target-room-id").should("exist").type(targetRoomId); + cy.get("#target-user-id").should("exist").type(targetUserId); + cy.get("#send-action").should("exist").click(); + }); +} + +function expectKickedMessage(shouldExist: boolean) { + // Expand any event summaries + cy.get(".mx_RoomView_MessageList").within(roomView => { + if (roomView.find(".mx_GenericEventListSummary_toggle[aria-expanded=false]").length > 0) { + cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true }); + } + }); + + // Check for the event message (or lack thereof) + cy.get(".mx_EventTile_line") + .contains(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`) + .should(shouldExist ? "exist" : "not.exist"); +} + +describe("Integration Manager: Kick", () => { + let testUser: UserCredentials; + let synapse: SynapseInstance; + let integrationManagerUrl: string; + + beforeEach(() => { + cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => { + integrationManagerUrl = url; + }); + cy.startSynapse("default").then(data => { + synapse = data; + + cy.initTestUser(synapse, USER_DISPLAY_NAME, () => { + cy.window().then(win => { + win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN); + win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN); + }); + }).then(user => { + testUser = user; + }); + + cy.setAccountData("m.widgets", { + "m.integration_manager": { + content: { + type: "m.integration_manager", + name: "Integration Manager", + url: integrationManagerUrl, + data: { + api_url: integrationManagerUrl, + }, + }, + id: "integration-manager", + }, + }).as("integrationManager"); + + // Succeed when checking the token is valid + cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => { + req.continue(res => { + return res.send(200, { + user_id: testUser.userId, + }); + }); + }); + + cy.createRoom({ + name: ROOM_NAME, + }).as("roomId"); + + cy.getBot(synapse, { displayName: BOT_DISPLAY_NAME, autoAcceptInvites: true }).as("bob"); + }); + }); + + afterEach(() => { + cy.stopSynapse(synapse); + cy.stopWebServers(); + }); + + it("should kick the target", () => { + cy.all([ + cy.get("@bob"), + cy.get("@roomId"), + cy.get<{}>("@integrationManager"), + ]).then(([targetUser, roomId]) => { + const targetUserId = targetUser.getUserId(); + cy.viewRoomByName(ROOM_NAME); + cy.inviteUser(roomId, targetUserId); + cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist'); + + openIntegrationManager(); + sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); + closeIntegrationManager(integrationManagerUrl); + expectKickedMessage(true); + }); + }); + + it("should not kick the target if lacking permissions", () => { + cy.all([ + cy.get("@bob"), + cy.get("@roomId"), + cy.get<{}>("@integrationManager"), + ]).then(([targetUser, roomId]) => { + const targetUserId = targetUser.getUserId(); + cy.viewRoomByName(ROOM_NAME); + cy.inviteUser(roomId, targetUserId); + cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist'); + cy.getClient().then(async client => { + await client.sendStateEvent(roomId, 'm.room.power_levels', { + kick: 50, + users: { + [testUser.userId]: 0, + }, + }); + }).then(() => { + openIntegrationManager(); + sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); + closeIntegrationManager(integrationManagerUrl); + expectKickedMessage(false); + }); + }); + }); + + it("should no-op if the target already left", () => { + cy.all([ + cy.get("@bob"), + cy.get("@roomId"), + cy.get<{}>("@integrationManager"), + ]).then(([targetUser, roomId]) => { + const targetUserId = targetUser.getUserId(); + cy.viewRoomByName(ROOM_NAME); + cy.inviteUser(roomId, targetUserId); + cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist').then(async () => { + await targetUser.leave(roomId); + }).then(() => { + openIntegrationManager(); + sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); + closeIntegrationManager(integrationManagerUrl); + expectKickedMessage(false); + }); + }); + }); + + it("should no-op if the target was banned", () => { + cy.all([ + cy.get("@bob"), + cy.get("@roomId"), + cy.get<{}>("@integrationManager"), + ]).then(([targetUser, roomId]) => { + const targetUserId = targetUser.getUserId(); + cy.viewRoomByName(ROOM_NAME); + cy.inviteUser(roomId, targetUserId); + cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist'); + cy.getClient().then(async client => { + await client.ban(roomId, targetUserId); + }).then(() => { + openIntegrationManager(); + sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); + closeIntegrationManager(integrationManagerUrl); + expectKickedMessage(false); + }); + }); + }); + + it("should no-op if the target was never a room member", () => { + cy.all([ + cy.get("@bob"), + cy.get("@roomId"), + cy.get<{}>("@integrationManager"), + ]).then(([targetUser, roomId]) => { + const targetUserId = targetUser.getUserId(); + cy.viewRoomByName(ROOM_NAME); + + openIntegrationManager(); + sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); + closeIntegrationManager(integrationManagerUrl); + expectKickedMessage(false); + }); + }); +}); diff --git a/src/ScalarMessaging.ts b/src/ScalarMessaging.ts index d2d11c71cf..c511d291ce 100644 --- a/src/ScalarMessaging.ts +++ b/src/ScalarMessaging.ts @@ -73,6 +73,29 @@ Example: } } +kick +------ +Kicks a user from a room. The request will no-op if the user is not in the room. + +Request: + - room_id is the room to kick the user from. + - user_id is the user ID to kick. + - reason is an optional string for the kick reason +Response: +{ + success: true +} +Example: +{ + action: "kick", + room_id: "!foo:bar", + user_id: "@target:example.org", + reason: "Removed from room", + response: { + success: true + } +} + set_bot_options --------------- Set the m.room.bot.options state event for a bot user. @@ -254,6 +277,7 @@ import { _t } from './languageHandler'; import { IntegrationManagers } from "./integrations/IntegrationManagers"; import { WidgetType } from "./widgets/WidgetType"; import { objectClone } from "./utils/objects"; +import { EffectiveMembership, getEffectiveMembership } from './utils/membership'; enum Action { CloseScalar = "close_scalar", @@ -266,6 +290,7 @@ enum Action { CanSendEvent = "can_send_event", MembershipState = "membership_state", invite = "invite", + Kick = "kick", BotOptions = "bot_options", SetBotOptions = "set_bot_options", SetBotPower = "set_bot_power", @@ -322,6 +347,35 @@ function inviteUser(event: MessageEvent, roomId: string, userId: string): v }); } +function kickUser(event: MessageEvent, roomId: string, userId: string): void { + logger.log(`Received request to kick ${userId} from room ${roomId}`); + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, _t("You need to be logged in.")); + return; + } + const room = client.getRoom(roomId); + if (room) { + // if they are already not in the room we can resolve immediately. + const member = room.getMember(userId); + if (!member || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) { + sendResponse(event, { + success: true, + }); + return; + } + } + + const reason = event.data.reason; + client.kick(roomId, userId, reason).then(() => { + sendResponse(event, { + success: true, + }); + }).catch((err) => { + sendError(event, _t("You need to be able to kick users to do that."), err); + }); +} + function setWidget(event: MessageEvent, roomId: string): void { const widgetId = event.data.widget_id; let widgetType = event.data.type; @@ -710,6 +764,9 @@ const onMessage = function(event: MessageEvent): void { case Action.invite: inviteUser(event, roomId, userId); break; + case Action.Kick: + kickUser(event, roomId, userId); + break; case Action.BotOptions: botOptions(event, roomId, userId); break; @@ -729,7 +786,7 @@ const onMessage = function(event: MessageEvent): void { }; let listenerCount = 0; -let openManagerUrl: string = null; +let openManagerUrl: string | null = null; export function startListening(): void { if (listenerCount === 0) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e0d68b5a87..df41c83239 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -376,6 +376,7 @@ "Some invites couldn't be sent": "Some invites couldn't be sent", "You need to be logged in.": "You need to be logged in.", "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", + "You need to be able to kick users to do that.": "You need to be able to kick users to do that.", "Unable to create widget.": "Unable to create widget.", "Missing roomId.": "Missing roomId.", "Failed to send request.": "Failed to send request.",