Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 348a006

Browse files
justinbotturt2livet3chguy
authored
Allow integration managers to remove users (#9211)
* Add kick action to Scalar Messaging API * Fix docs * Fix membership check * Update style * Update i18n * Add e2e tests * Fix test flakiness * Fix variable type * Check for when bot has joined * Add missing semicolon * Not a real token Co-authored-by: Travis Ralston <[email protected]> * Improve test description Co-authored-by: Travis Ralston <[email protected]> * Look for room kick message instead of checking room state * Expand event summaries before checking for message Co-authored-by: Travis Ralston <[email protected]> Co-authored-by: Travis Ralston <[email protected]> Co-authored-by: Michael Telatynski <[email protected]>
1 parent b1ceccc commit 348a006

File tree

3 files changed

+315
-1
lines changed

3 files changed

+315
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/// <reference types="cypress" />
18+
19+
import { SynapseInstance } from "../../plugins/synapsedocker";
20+
import { MatrixClient } from "../../global";
21+
import { UserCredentials } from "../../support/login";
22+
23+
const ROOM_NAME = "Integration Manager Test";
24+
const USER_DISPLAY_NAME = "Alice";
25+
const BOT_DISPLAY_NAME = "Bob";
26+
const KICK_REASON = "Goodbye";
27+
28+
const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
29+
const INTEGRATION_MANAGER_HTML = `
30+
<html lang="en">
31+
<head>
32+
<title>Fake Integration Manager</title>
33+
</head>
34+
<body>
35+
<input type="text" id="target-room-id"/>
36+
<input type="text" id="target-user-id"/>
37+
<button name="Send" id="send-action">Press to send action</button>
38+
<button name="Close" id="close">Press to close</button>
39+
<script>
40+
document.getElementById("send-action").onclick = () => {
41+
window.parent.postMessage(
42+
{
43+
action: "kick",
44+
room_id: document.getElementById("target-room-id").value,
45+
user_id: document.getElementById("target-user-id").value,
46+
reason: "${KICK_REASON}",
47+
},
48+
'*',
49+
);
50+
};
51+
document.getElementById("close").onclick = () => {
52+
window.parent.postMessage(
53+
{
54+
action: "close_scalar",
55+
},
56+
'*',
57+
);
58+
};
59+
</script>
60+
</body>
61+
</html>
62+
`;
63+
64+
function openIntegrationManager() {
65+
cy.get(".mx_RightPanel_roomSummaryButton").click();
66+
cy.get(".mx_RoomSummaryCard_appsGroup").within(() => {
67+
cy.contains("Add widgets, bridges & bots").click();
68+
});
69+
}
70+
71+
function closeIntegrationManager(integrationManagerUrl: string) {
72+
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
73+
cy.get("#close").should("exist").click();
74+
});
75+
}
76+
77+
function sendActionFromIntegrationManager(integrationManagerUrl: string, targetRoomId: string, targetUserId: string) {
78+
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
79+
cy.get("#target-room-id").should("exist").type(targetRoomId);
80+
cy.get("#target-user-id").should("exist").type(targetUserId);
81+
cy.get("#send-action").should("exist").click();
82+
});
83+
}
84+
85+
function expectKickedMessage(shouldExist: boolean) {
86+
// Expand any event summaries
87+
cy.get(".mx_RoomView_MessageList").within(roomView => {
88+
if (roomView.find(".mx_GenericEventListSummary_toggle[aria-expanded=false]").length > 0) {
89+
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true });
90+
}
91+
});
92+
93+
// Check for the event message (or lack thereof)
94+
cy.get(".mx_EventTile_line")
95+
.contains(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)
96+
.should(shouldExist ? "exist" : "not.exist");
97+
}
98+
99+
describe("Integration Manager: Kick", () => {
100+
let testUser: UserCredentials;
101+
let synapse: SynapseInstance;
102+
let integrationManagerUrl: string;
103+
104+
beforeEach(() => {
105+
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => {
106+
integrationManagerUrl = url;
107+
});
108+
cy.startSynapse("default").then(data => {
109+
synapse = data;
110+
111+
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
112+
cy.window().then(win => {
113+
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
114+
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
115+
});
116+
}).then(user => {
117+
testUser = user;
118+
});
119+
120+
cy.setAccountData("m.widgets", {
121+
"m.integration_manager": {
122+
content: {
123+
type: "m.integration_manager",
124+
name: "Integration Manager",
125+
url: integrationManagerUrl,
126+
data: {
127+
api_url: integrationManagerUrl,
128+
},
129+
},
130+
id: "integration-manager",
131+
},
132+
}).as("integrationManager");
133+
134+
// Succeed when checking the token is valid
135+
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => {
136+
req.continue(res => {
137+
return res.send(200, {
138+
user_id: testUser.userId,
139+
});
140+
});
141+
});
142+
143+
cy.createRoom({
144+
name: ROOM_NAME,
145+
}).as("roomId");
146+
147+
cy.getBot(synapse, { displayName: BOT_DISPLAY_NAME, autoAcceptInvites: true }).as("bob");
148+
});
149+
});
150+
151+
afterEach(() => {
152+
cy.stopSynapse(synapse);
153+
cy.stopWebServers();
154+
});
155+
156+
it("should kick the target", () => {
157+
cy.all([
158+
cy.get<MatrixClient>("@bob"),
159+
cy.get<string>("@roomId"),
160+
cy.get<{}>("@integrationManager"),
161+
]).then(([targetUser, roomId]) => {
162+
const targetUserId = targetUser.getUserId();
163+
cy.viewRoomByName(ROOM_NAME);
164+
cy.inviteUser(roomId, targetUserId);
165+
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
166+
167+
openIntegrationManager();
168+
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
169+
closeIntegrationManager(integrationManagerUrl);
170+
expectKickedMessage(true);
171+
});
172+
});
173+
174+
it("should not kick the target if lacking permissions", () => {
175+
cy.all([
176+
cy.get<MatrixClient>("@bob"),
177+
cy.get<string>("@roomId"),
178+
cy.get<{}>("@integrationManager"),
179+
]).then(([targetUser, roomId]) => {
180+
const targetUserId = targetUser.getUserId();
181+
cy.viewRoomByName(ROOM_NAME);
182+
cy.inviteUser(roomId, targetUserId);
183+
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
184+
cy.getClient().then(async client => {
185+
await client.sendStateEvent(roomId, 'm.room.power_levels', {
186+
kick: 50,
187+
users: {
188+
[testUser.userId]: 0,
189+
},
190+
});
191+
}).then(() => {
192+
openIntegrationManager();
193+
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
194+
closeIntegrationManager(integrationManagerUrl);
195+
expectKickedMessage(false);
196+
});
197+
});
198+
});
199+
200+
it("should no-op if the target already left", () => {
201+
cy.all([
202+
cy.get<MatrixClient>("@bob"),
203+
cy.get<string>("@roomId"),
204+
cy.get<{}>("@integrationManager"),
205+
]).then(([targetUser, roomId]) => {
206+
const targetUserId = targetUser.getUserId();
207+
cy.viewRoomByName(ROOM_NAME);
208+
cy.inviteUser(roomId, targetUserId);
209+
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist').then(async () => {
210+
await targetUser.leave(roomId);
211+
}).then(() => {
212+
openIntegrationManager();
213+
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
214+
closeIntegrationManager(integrationManagerUrl);
215+
expectKickedMessage(false);
216+
});
217+
});
218+
});
219+
220+
it("should no-op if the target was banned", () => {
221+
cy.all([
222+
cy.get<MatrixClient>("@bob"),
223+
cy.get<string>("@roomId"),
224+
cy.get<{}>("@integrationManager"),
225+
]).then(([targetUser, roomId]) => {
226+
const targetUserId = targetUser.getUserId();
227+
cy.viewRoomByName(ROOM_NAME);
228+
cy.inviteUser(roomId, targetUserId);
229+
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
230+
cy.getClient().then(async client => {
231+
await client.ban(roomId, targetUserId);
232+
}).then(() => {
233+
openIntegrationManager();
234+
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
235+
closeIntegrationManager(integrationManagerUrl);
236+
expectKickedMessage(false);
237+
});
238+
});
239+
});
240+
241+
it("should no-op if the target was never a room member", () => {
242+
cy.all([
243+
cy.get<MatrixClient>("@bob"),
244+
cy.get<string>("@roomId"),
245+
cy.get<{}>("@integrationManager"),
246+
]).then(([targetUser, roomId]) => {
247+
const targetUserId = targetUser.getUserId();
248+
cy.viewRoomByName(ROOM_NAME);
249+
250+
openIntegrationManager();
251+
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
252+
closeIntegrationManager(integrationManagerUrl);
253+
expectKickedMessage(false);
254+
});
255+
});
256+
});

src/ScalarMessaging.ts

+58-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ Example:
7373
}
7474
}
7575
76+
kick
77+
------
78+
Kicks a user from a room. The request will no-op if the user is not in the room.
79+
80+
Request:
81+
- room_id is the room to kick the user from.
82+
- user_id is the user ID to kick.
83+
- reason is an optional string for the kick reason
84+
Response:
85+
{
86+
success: true
87+
}
88+
Example:
89+
{
90+
action: "kick",
91+
room_id: "!foo:bar",
92+
user_id: "@target:example.org",
93+
reason: "Removed from room",
94+
response: {
95+
success: true
96+
}
97+
}
98+
7699
set_bot_options
77100
---------------
78101
Set the m.room.bot.options state event for a bot user.
@@ -254,6 +277,7 @@ import { _t } from './languageHandler';
254277
import { IntegrationManagers } from "./integrations/IntegrationManagers";
255278
import { WidgetType } from "./widgets/WidgetType";
256279
import { objectClone } from "./utils/objects";
280+
import { EffectiveMembership, getEffectiveMembership } from './utils/membership';
257281

258282
enum Action {
259283
CloseScalar = "close_scalar",
@@ -266,6 +290,7 @@ enum Action {
266290
CanSendEvent = "can_send_event",
267291
MembershipState = "membership_state",
268292
invite = "invite",
293+
Kick = "kick",
269294
BotOptions = "bot_options",
270295
SetBotOptions = "set_bot_options",
271296
SetBotPower = "set_bot_power",
@@ -322,6 +347,35 @@ function inviteUser(event: MessageEvent<any>, roomId: string, userId: string): v
322347
});
323348
}
324349

350+
function kickUser(event: MessageEvent<any>, roomId: string, userId: string): void {
351+
logger.log(`Received request to kick ${userId} from room ${roomId}`);
352+
const client = MatrixClientPeg.get();
353+
if (!client) {
354+
sendError(event, _t("You need to be logged in."));
355+
return;
356+
}
357+
const room = client.getRoom(roomId);
358+
if (room) {
359+
// if they are already not in the room we can resolve immediately.
360+
const member = room.getMember(userId);
361+
if (!member || getEffectiveMembership(member.membership) === EffectiveMembership.Leave) {
362+
sendResponse(event, {
363+
success: true,
364+
});
365+
return;
366+
}
367+
}
368+
369+
const reason = event.data.reason;
370+
client.kick(roomId, userId, reason).then(() => {
371+
sendResponse(event, {
372+
success: true,
373+
});
374+
}).catch((err) => {
375+
sendError(event, _t("You need to be able to kick users to do that."), err);
376+
});
377+
}
378+
325379
function setWidget(event: MessageEvent<any>, roomId: string): void {
326380
const widgetId = event.data.widget_id;
327381
let widgetType = event.data.type;
@@ -710,6 +764,9 @@ const onMessage = function(event: MessageEvent<any>): void {
710764
case Action.invite:
711765
inviteUser(event, roomId, userId);
712766
break;
767+
case Action.Kick:
768+
kickUser(event, roomId, userId);
769+
break;
713770
case Action.BotOptions:
714771
botOptions(event, roomId, userId);
715772
break;
@@ -729,7 +786,7 @@ const onMessage = function(event: MessageEvent<any>): void {
729786
};
730787

731788
let listenerCount = 0;
732-
let openManagerUrl: string = null;
789+
let openManagerUrl: string | null = null;
733790

734791
export function startListening(): void {
735792
if (listenerCount === 0) {

src/i18n/strings/en_EN.json

+1
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@
376376
"Some invites couldn't be sent": "Some invites couldn't be sent",
377377
"You need to be logged in.": "You need to be logged in.",
378378
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
379+
"You need to be able to kick users to do that.": "You need to be able to kick users to do that.",
379380
"Unable to create widget.": "Unable to create widget.",
380381
"Missing roomId.": "Missing roomId.",
381382
"Failed to send request.": "Failed to send request.",

0 commit comments

Comments
 (0)