diff --git a/cypress/e2e/integration-manager/kick.spec.ts b/cypress/e2e/integration-manager/kick.spec.ts
new file mode 100644
index 00000000000..6901cd376b1
--- /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 d2d11c71cfa..c511d291ce2 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 e0d68b5a873..df41c832399 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.",