mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
8bafa16495
If you add `"no-floating-promises": true` it's very easy to find where this is done accidentally. Not sure we can keep that on all the time yet though..
736 lines
36 KiB
TypeScript
736 lines
36 KiB
TypeScript
import { strict as assert } from "assert";
|
|
import { randomUUID } from "crypto";
|
|
import { RoomMemberManager } from "../../src/RoomMembers";
|
|
import { newTestUser } from "./clientHelper";
|
|
import { getFirstReply, getNthReply } from "./commands/commandUtils";
|
|
|
|
describe("Test: Testing RoomMemberManager", function() {
|
|
it("RoomMemberManager counts correctly when we call handleEvent manually", async function() {
|
|
let manager: RoomMemberManager = this.mjolnir.roomJoins;
|
|
let start = new Date(Date.now() - 100_000_000);
|
|
const ROOMS = [
|
|
"!room_0@localhost",
|
|
"!room_1@localhost"
|
|
];
|
|
for (let room of ROOMS) {
|
|
manager.addRoom(room);
|
|
}
|
|
|
|
let joinDate = (i: number) => new Date(start.getTime() + i * 100_000);
|
|
let userId = (i: number) => `@sender_${i}:localhost`;
|
|
|
|
// First, add a number of joins.
|
|
const SAMPLE_SIZE = 100;
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
const event = {
|
|
type: 'm.room.member',
|
|
state_key: userId(i),
|
|
sender: userId(i),
|
|
content: {
|
|
membership: "join"
|
|
}
|
|
};
|
|
await manager.handleEvent(ROOMS[i % ROOMS.length], event, joinDate(i));
|
|
}
|
|
|
|
{
|
|
const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000);
|
|
const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000);
|
|
|
|
const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp]));
|
|
const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp]));
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
const user = userId(i);
|
|
let map = i % 2 === 0 ? joins0ByUserId : joins1ByUserId;
|
|
const ts = map.get(user);
|
|
assert.ok(ts, `User ${user} should have been seen joining room ${i % 2}`);
|
|
assert.equal(ts, joinDate(i).getTime(), `User ${user} should have been seen joining the room at the right timestamp`);
|
|
map.delete(user);
|
|
}
|
|
|
|
assert.equal(joins0ByUserId.size, 0, "We should have found all the users in room 0");
|
|
assert.equal(joins1ByUserId.size, 0, "We should have found all the users in room 1");
|
|
}
|
|
|
|
// Now, let's add a few leave events.
|
|
let leaveDate = (i: number) => new Date(start.getTime() + (SAMPLE_SIZE + i) * 100_000);
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE / 3; ++i) {
|
|
const user = userId(i * 3);
|
|
const event = {
|
|
type: 'm.room.member',
|
|
state_key: user,
|
|
sender: user,
|
|
content: {
|
|
membership: "leave"
|
|
},
|
|
unsigned: {
|
|
prev_content: {
|
|
membership: "join"
|
|
}
|
|
}
|
|
};
|
|
await manager.handleEvent(ROOMS[0], event, leaveDate(i));
|
|
await manager.handleEvent(ROOMS[1], event, leaveDate(i));
|
|
}
|
|
|
|
// Let's see if we have properly updated the joins/leaves
|
|
{
|
|
const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000);
|
|
const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000);
|
|
|
|
const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp]));
|
|
const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp]));
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
const user = userId(i);
|
|
let map = i % 2 === 0 ? joins0ByUserId : joins1ByUserId;
|
|
let isStillJoined = i % 3 !== 0;
|
|
const ts = map.get(user);
|
|
if (isStillJoined) {
|
|
assert.ok(ts, `User ${user} should have been seen joining room ${i % 2}`);
|
|
assert.equal(ts, joinDate(i).getTime(), `User ${user} should have been seen joining the room at the right timestamp`);
|
|
map.delete(user);
|
|
} else {
|
|
assert.ok(!ts, `User ${user} should not be seen as a member of room ${i % 2} anymore`);
|
|
}
|
|
}
|
|
|
|
assert.equal(joins0ByUserId.size, 0, "We should have found all the users in room 0");
|
|
assert.equal(joins1ByUserId.size, 0, "We should have found all the users in room 1");
|
|
}
|
|
|
|
// Now let's make a few of these users rejoin.
|
|
let rejoinDate = (i: number) => new Date(start.getTime() + (SAMPLE_SIZE * 2 + i) * 100_000);
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE / 9; ++i) {
|
|
const user = userId(i * 9);
|
|
const event = {
|
|
type: 'm.room.member',
|
|
state_key: user,
|
|
sender: user,
|
|
content: {
|
|
membership: "join"
|
|
},
|
|
unsigned: {
|
|
prev_content: {
|
|
membership: "leave"
|
|
}
|
|
}
|
|
};
|
|
const room = ROOMS[i * 9 % 2];
|
|
await manager.handleEvent(room, event, rejoinDate(i * 9));
|
|
}
|
|
|
|
// Let's see if we have properly updated the joins/leaves
|
|
{
|
|
const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000);
|
|
const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000);
|
|
|
|
const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp]));
|
|
const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp]));
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
const user = userId(i);
|
|
let map = i % 2 === 0 ? joins0ByUserId : joins1ByUserId;
|
|
let hasLeft = i % 3 === 0;
|
|
let hasRejoined = i % 9 === 0;
|
|
const ts = map.get(user);
|
|
if (hasRejoined) {
|
|
assert.ok(ts, `User ${user} should have been seen rejoining room ${i % 2}`);
|
|
assert.equal(ts, rejoinDate(i).getTime(), `User ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`);
|
|
map.delete(user);
|
|
} else if (hasLeft) {
|
|
assert.ok(!ts, `User ${user} should not be seen as a member of room ${i % 2} anymore`);
|
|
} else {
|
|
assert.ok(ts, `User ${user} should have been seen joining room ${i % 2}`);
|
|
assert.equal(ts, joinDate(i).getTime(), `User ${user} should have been seen joining the room at the right timestamp`);
|
|
map.delete(user);
|
|
}
|
|
}
|
|
|
|
assert.equal(joins0ByUserId.size, 0, "We should have found all the users in room 0");
|
|
assert.equal(joins1ByUserId.size, 0, "We should have found all the users in room 1");
|
|
}
|
|
|
|
// Now let's check only the most recent joins.
|
|
{
|
|
const joins0 = manager.getUsersInRoom(ROOMS[0], rejoinDate(-1), 100_000);
|
|
const joins1 = manager.getUsersInRoom(ROOMS[1], rejoinDate(-1), 100_000);
|
|
|
|
const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp]));
|
|
const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp]));
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
const user = userId(i);
|
|
let map = i % 2 === 0 ? joins0ByUserId : joins1ByUserId;
|
|
let hasRejoined = i % 9 === 0;
|
|
const ts = map.get(user);
|
|
if (hasRejoined) {
|
|
assert.ok(ts, `User ${user} should have been seen rejoining room ${i % 2}`);
|
|
assert.equal(ts, rejoinDate(i).getTime(), `User ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`);
|
|
map.delete(user);
|
|
} else {
|
|
assert.ok(!ts, `When looking only at recent entries, user ${user} should not be seen as a member of room ${i % 2} anymore`);
|
|
}
|
|
}
|
|
|
|
assert.equal(joins0ByUserId.size, 0, "We should have found all the users who recently joined room 0");
|
|
assert.equal(joins1ByUserId.size, 0, "We should have found all the users who recently joined room 1");
|
|
}
|
|
|
|
// Perform a cleanup on both rooms, check that we have the same results.
|
|
for (let roomId of ROOMS) {
|
|
manager.cleanup(roomId);
|
|
}
|
|
|
|
// Let's see if we have properly updated the joins/leaves
|
|
{
|
|
const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000);
|
|
const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000);
|
|
|
|
const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp]));
|
|
const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp]));
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
const user = userId(i);
|
|
let map = i % 2 === 0 ? joins0ByUserId : joins1ByUserId;
|
|
let hasLeft = i % 3 === 0;
|
|
let hasRejoined = i % 9 === 0;
|
|
const ts = map.get(user);
|
|
if (hasRejoined) {
|
|
assert.ok(ts, `After cleanup, user ${user} should have been seen rejoining room ${i % 2}`);
|
|
assert.equal(ts, rejoinDate(i).getTime(), `After cleanup, user ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`);
|
|
map.delete(user);
|
|
} else if (hasLeft) {
|
|
assert.ok(!ts, `After cleanup, user ${user} should not be seen as a member of room ${i % 2} anymore`);
|
|
} else {
|
|
assert.ok(ts, `After cleanup, user ${user} should have been seen joining room ${i % 2}`);
|
|
assert.equal(ts, joinDate(i).getTime(), `After cleanup, user ${user} should have been seen joining the room at the right timestamp`);
|
|
map.delete(user);
|
|
}
|
|
}
|
|
|
|
assert.equal(joins0ByUserId.size, 0, "After cleanup, we should have found all the users in room 0");
|
|
assert.equal(joins1ByUserId.size, 0, "After cleanup, we should have found all the users in room 1");
|
|
}
|
|
|
|
// Now let's check only the most recent joins.
|
|
{
|
|
const joins0 = manager.getUsersInRoom(ROOMS[0], rejoinDate(-1), 100_000);
|
|
const joins1 = manager.getUsersInRoom(ROOMS[1], rejoinDate(-1), 100_000);
|
|
|
|
const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp]));
|
|
const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp]));
|
|
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
const user = userId(i);
|
|
let map = i % 2 === 0 ? joins0ByUserId : joins1ByUserId;
|
|
let hasRejoined = i % 9 === 0;
|
|
const ts = map.get(user);
|
|
if (hasRejoined) {
|
|
assert.ok(ts, `After cleanup, user ${user} should have been seen rejoining room ${i % 2}`);
|
|
assert.equal(ts, rejoinDate(i).getTime(), `After cleanup, user ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`);
|
|
map.delete(user);
|
|
} else {
|
|
assert.ok(!ts, `After cleanup, when looking only at recent entries, user ${user} should not be seen as a member of room ${i % 2} anymore`);
|
|
}
|
|
}
|
|
|
|
assert.equal(joins0ByUserId.size, 0, "After cleanup, we should have found all the users who recently joined room 0");
|
|
assert.equal(joins1ByUserId.size, 0, "After cleanup, we should have found all the users who recently joined room 1");
|
|
}
|
|
});
|
|
|
|
afterEach(async function() {
|
|
await this.moderator?.stop();
|
|
for (let array of [this.users, this.goodUsers, this.badUsers]) {
|
|
for (let client of array || []) {
|
|
await client.stop();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("RoomMemberManager counts correctly when we actually join/leave/get banned from the room", async function() {
|
|
this.timeout(60000);
|
|
const start = new Date(Date.now() - 10_000);
|
|
|
|
// Setup a moderator.
|
|
this.moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
|
|
await this.mjolnir.client.inviteUser(await this.moderator.getUserId(), this.mjolnir.managementRoomId)
|
|
await this.moderator.joinRoom(this.mjolnir.managementRoomId);
|
|
|
|
// Create a few users and two rooms.
|
|
this.users = [];
|
|
const SAMPLE_SIZE = 10;
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
this.users.push(await newTestUser(this.config.homeserverUrl, { name: { contains: `user_${i}_room_member_test` } }));
|
|
}
|
|
const userIds = [];
|
|
for (let client of this.users) {
|
|
userIds.push(await client.getUserId());
|
|
}
|
|
const roomId1 = await this.moderator.createRoom({
|
|
invite: userIds,
|
|
preset: "public_chat",
|
|
});
|
|
const roomId2 = await this.moderator.createRoom({
|
|
invite: userIds,
|
|
preset: "public_chat",
|
|
});
|
|
const roomIds = [roomId1, roomId2];
|
|
|
|
for (let roomId of roomIds) {
|
|
await this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir rooms add ${roomId}` });
|
|
}
|
|
|
|
let protectedRoomsUpdated = false;
|
|
do {
|
|
let protectedRooms = this.mjolnir.protectedRooms;
|
|
protectedRoomsUpdated = true;
|
|
for (let roomId of roomIds) {
|
|
if (!(roomId in protectedRooms)) {
|
|
protectedRoomsUpdated = false;
|
|
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
}
|
|
}
|
|
} while (!protectedRoomsUpdated);
|
|
|
|
|
|
// Initially, we shouldn't know about any user in these rooms... except Mjölnir itself.
|
|
const manager: RoomMemberManager = this.mjolnir.roomJoins;
|
|
for (let roomId of roomIds) {
|
|
const joined = manager.getUsersInRoom(roomId, start, 100);
|
|
assert.equal(joined.length, 1, "Initially, we shouldn't know about any other user in these rooms");
|
|
assert.equal(joined[0].userId, await this.mjolnir.client.getUserId(), "Initially, Mjölnir should be the only known user in these rooms");
|
|
}
|
|
|
|
// Initially, the command should show that same result.
|
|
for (let roomId of roomIds) {
|
|
const reply = await getFirstReply(this.mjolnir.client, this.mjolnir.managementRoomId, () => {
|
|
const command = `!mjolnir status joins ${roomId}`;
|
|
return this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command });
|
|
});
|
|
const body = reply["content"]?.["body"] as string;
|
|
assert.ok(body.includes("\n1 recent joins"), "Initially the command should respond with 1 user");
|
|
}
|
|
|
|
// Now join a few rooms.
|
|
for (let i = 0; i < userIds.length; ++i) {
|
|
await this.users[i].joinRoom(roomIds[i % roomIds.length]);
|
|
}
|
|
|
|
// Lists should have been updated.
|
|
for (let i = 0; i < roomIds.length; ++i) {
|
|
const roomId = roomIds[i];
|
|
const joined = manager.getUsersInRoom(roomId, start, 100);
|
|
assert.equal(joined.length, SAMPLE_SIZE / 2 /* half of the users */ + 1 /* mjolnir */, "We should now see all joined users in the room");
|
|
const reply = await getFirstReply(this.mjolnir.client, this.mjolnir.managementRoomId, () => {
|
|
const command = `!mjolnir status joins ${roomId}`;
|
|
return this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command });
|
|
});
|
|
const body = reply["content"]?.["body"] as string;
|
|
assert.ok(body.includes(`\n${joined.length} recent joins`), `After joins, the command should respond with ${joined.length} users`);
|
|
for (let j = 0; j < userIds.length; ++j) {
|
|
if (j % roomIds.length === i) {
|
|
assert.ok(body.includes(userIds[j]), `After joins, the command should display user ${userIds[j]} in room ${roomId}`);
|
|
} else {
|
|
assert.ok(!body.includes(userIds[j]), `After joins, the command should NOT display user ${userIds[j]} in room ${roomId}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Let's kick/ban a few users and see if they still show up.
|
|
const removedUsers = new Set();
|
|
for (let i = 0; i < SAMPLE_SIZE / 2; ++i) {
|
|
const roomId = roomIds[i % roomIds.length];
|
|
const userId = userIds[i];
|
|
if (i % 3 === 0) {
|
|
await this.moderator.kickUser(userId, roomId);
|
|
removedUsers.add(userIds[i]);
|
|
} else if (i % 3 === 1) {
|
|
await this.moderator.banUser(userId, roomId);
|
|
removedUsers.add(userId);
|
|
}
|
|
}
|
|
|
|
// Lists should have been updated.
|
|
|
|
for (let i = 0; i < roomIds.length; ++i) {
|
|
const roomId = roomIds[i];
|
|
const reply = await getFirstReply(this.mjolnir.client, this.mjolnir.managementRoomId, () => {
|
|
const command = `!mjolnir status joins ${roomId}`;
|
|
return this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command });
|
|
});
|
|
const body = reply["content"]?.["body"] as string;
|
|
for (let j = 0; j < userIds.length; ++j) {
|
|
const userId = userIds[j];
|
|
if (j % roomIds.length === i && !removedUsers.has(userId)) {
|
|
assert.ok(body.includes(userId), `After kicks, the command should display user ${userId} in room ${roomId}`);
|
|
} else {
|
|
assert.ok(!body.includes(userId), `After kicks, the command should NOT display user ${userId} in room ${roomId}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("!mjolnir since kicks the correct users", async function() {
|
|
this.timeout(600_000);
|
|
const start = new Date(Date.now() - 10_000);
|
|
|
|
// Setup a moderator.
|
|
this.moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } });
|
|
await this.moderator.joinRoom(this.mjolnir.managementRoomId);
|
|
|
|
// Create a few users.
|
|
this.goodUsers = [];
|
|
this.badUsers = [];
|
|
const SAMPLE_SIZE = 10;
|
|
for (let i = 0; i < SAMPLE_SIZE; ++i) {
|
|
this.goodUsers.push(await newTestUser(this.config.homeserverUrl, { name: { contains: `good_user_${i}_room_member_test` } }));
|
|
this.badUsers.push(await newTestUser(this.config.homeserverUrl, { name: { contains: `bad_user_${i}_room_member_test` } }));
|
|
}
|
|
const goodUserIds: string[] = [];
|
|
const badUserIds: string[] = [];
|
|
for (let client of this.goodUsers) {
|
|
goodUserIds.push(await client.getUserId());
|
|
}
|
|
for (let client of this.badUsers) {
|
|
badUserIds.push(await client.getUserId());
|
|
}
|
|
|
|
// Create and protect rooms.
|
|
//
|
|
// We reserve two control rooms:
|
|
// - room 0, also known as the "control unprotected room" is unprotected
|
|
// (we're not calling `!mjolnir rooms add` for this room), so none
|
|
// of the operations of `!mjolnir since` shoud affect it. We are
|
|
// using it to control, at the end of each experiment, that none of
|
|
// the `!mjolnir since` operations affect it.
|
|
// - room 1, also known as the "control protected room" is protected
|
|
// (we are calling `!mjolnir rooms add` for this room), but we are
|
|
// never directly requesting any `!mjolnir since` action against
|
|
// this room. We are using it to control, at the end of each experiment,
|
|
// that none of the `!mjolnir since` operations that should target
|
|
// one single other room also affect that room. It is, however, affected
|
|
// by general operations that are designed to affect all protected rooms.
|
|
const NUMBER_OF_ROOMS = 18;
|
|
const allRoomIds: string[] = [];
|
|
const allRoomAliases: string[] = [];
|
|
const mjolnirUserId = await this.mjolnir.client.getUserId();
|
|
for (let i = 0; i < NUMBER_OF_ROOMS; ++i) {
|
|
const roomId = await this.moderator.createRoom({
|
|
invite: [mjolnirUserId, ...goodUserIds, ...badUserIds],
|
|
});
|
|
allRoomIds.push(roomId);
|
|
|
|
const alias = `#since-test-${randomUUID()}:localhost:9999`;
|
|
await this.moderator.createRoomAlias(alias, roomId);
|
|
allRoomAliases.push(alias);
|
|
}
|
|
for (let i = 1; i < allRoomIds.length; ++i) {
|
|
// Protect all rooms except allRoomIds[0], as control.
|
|
const roomId = allRoomIds[i];
|
|
await this.mjolnir.client.joinRoom(roomId);
|
|
await this.moderator.setUserPowerLevel(mjolnirUserId, roomId, 100);
|
|
await this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir rooms add ${roomId}` });
|
|
}
|
|
|
|
let protectedRoomsUpdated = false;
|
|
do {
|
|
let protectedRooms = this.mjolnir.protectedRooms;
|
|
protectedRoomsUpdated = true;
|
|
for (let i = 1; i < allRoomIds.length; ++i) {
|
|
const roomId = allRoomIds[i];
|
|
if (!(roomId in protectedRooms)) {
|
|
protectedRoomsUpdated = false;
|
|
await new Promise(resolve => setTimeout(resolve, 1_000));
|
|
}
|
|
}
|
|
} while (!protectedRoomsUpdated);
|
|
|
|
// Good users join before cut date.
|
|
for (let user of this.goodUsers) {
|
|
for (let roomId of allRoomIds) {
|
|
await user.joinRoom(roomId);
|
|
}
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 5_000));
|
|
|
|
const cutDate = new Date();
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 5_000));
|
|
|
|
// Bad users join after cut date.
|
|
for (let user of this.badUsers) {
|
|
for (let roomId of allRoomIds) {
|
|
await user.joinRoom(roomId);
|
|
}
|
|
}
|
|
|
|
// Finally, prepare our control rooms and separate them
|
|
// from the regular rooms.
|
|
const CONTROL_UNPROTECTED_ROOM_ID = allRoomIds[0];
|
|
const CONTROL_PROTECTED_ID = allRoomIds[1];
|
|
const roomIds = allRoomIds.slice(2);
|
|
const roomAliases = allRoomAliases.slice(2);
|
|
|
|
enum Method {
|
|
kick,
|
|
ban,
|
|
mute,
|
|
unmute,
|
|
}
|
|
class Experiment {
|
|
// A human-readable name for the command.
|
|
readonly name: string;
|
|
// If `true`, this command should affect room `CONTROL_PROTECTED_ID`.
|
|
// Defaults to `false`.
|
|
readonly shouldAffectControlProtected: boolean;
|
|
// The actual command-line.
|
|
readonly command: (roomId: string, roomAlias: string) => string;
|
|
// The number of responses we expect to this command.
|
|
// Defaults to `1`.
|
|
readonly n: number;
|
|
// How affected users should leave the room.
|
|
readonly method: Method;
|
|
|
|
// If `true`, should this experiment look at the same room as the previous one.
|
|
// Defaults to `false`.
|
|
readonly isSameRoomAsPrevious: boolean;
|
|
|
|
// The index of the room on which we're acting.
|
|
//
|
|
// Initialized by `addTo`.
|
|
roomIndex: number | undefined;
|
|
|
|
constructor({name, shouldAffectControlProtected, command, n, method, sameRoom}: {name: string, command: (roomId: string, roomAlias: string) => string, shouldAffectControlProtected?: boolean, n?: number, method: Method, sameRoom?: boolean}) {
|
|
this.name = name;
|
|
this.shouldAffectControlProtected = typeof shouldAffectControlProtected === "undefined" ? false : shouldAffectControlProtected;
|
|
this.command = command;
|
|
this.n = typeof n === "undefined" ? 1 : n;
|
|
this.method = method;
|
|
this.isSameRoomAsPrevious = typeof sameRoom === "undefined" ? false : sameRoom;
|
|
}
|
|
|
|
// Add an experiment to the list of experiments.
|
|
//
|
|
// This is how `roomIndex` gets initialized.
|
|
addTo(experiments: Experiment[]) {
|
|
if (this.isSameRoomAsPrevious) {
|
|
this.roomIndex = experiments[experiments.length - 1].roomIndex;
|
|
} else if (experiments.length === 0) {
|
|
this.roomIndex = 0;
|
|
} else {
|
|
this.roomIndex = experiments[experiments.length - 1].roomIndex! + 1;
|
|
}
|
|
experiments.push(this);
|
|
}
|
|
}
|
|
const EXPERIMENTS: Experiment[] = [];
|
|
for (let experiment of [
|
|
// Kick bad users in one room, using duration syntax, no reason.
|
|
new Experiment({
|
|
name: "kick with duration",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomId}`,
|
|
method: Method.kick,
|
|
}),
|
|
// Ban bad users in one room, using duration syntax, no reason.
|
|
new Experiment({
|
|
name: "ban with duration",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms ban 100 ${roomId}`,
|
|
method: Method.ban,
|
|
}),
|
|
// Mute bad users in one room, using duration syntax, no reason.
|
|
new Experiment({
|
|
name: "mute with duration",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms mute 100 ${roomId}`,
|
|
method: Method.mute,
|
|
}),
|
|
new Experiment({
|
|
name: "unmute with duration",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms unmute 100 ${roomId}`,
|
|
method: Method.unmute,
|
|
sameRoom: true,
|
|
}),
|
|
// Kick bad users in one room, using date syntax, no reason.
|
|
new Experiment({
|
|
name: "kick with date",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" kick 100 ${roomId}`,
|
|
method: Method.kick,
|
|
}),
|
|
// Ban bad users in one room, using date syntax, no reason.
|
|
new Experiment({
|
|
name: "ban with date",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" ban 100 ${roomId}`,
|
|
method: Method.ban,
|
|
}),
|
|
// Mute bad users in one room, using date syntax, no reason.
|
|
new Experiment({
|
|
name: "mute with date",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" mute 100 ${roomId}`,
|
|
method: Method.mute,
|
|
}),
|
|
new Experiment({
|
|
name: "unmute with date",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" unmute 100 ${roomId}`,
|
|
method: Method.unmute,
|
|
sameRoom: true,
|
|
}),
|
|
|
|
// Kick bad users in one room, using duration syntax, with reason.
|
|
new Experiment({
|
|
name: "kick with duration and reason",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomId} bad, bad user`,
|
|
method: Method.kick,
|
|
}),
|
|
// Ban bad users in one room, using duration syntax, with reason.
|
|
new Experiment({
|
|
name: "ban with duration and reason",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms ban 100 ${roomId} bad, bad user`,
|
|
method: Method.ban,
|
|
}),
|
|
// Mute bad users in one room, using duration syntax, with reason.
|
|
new Experiment({
|
|
name: "mute with duration and reason",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms mute 100 ${roomId} bad, bad user`,
|
|
method: Method.mute,
|
|
}),
|
|
new Experiment({
|
|
name: "unmute with duration and reason",
|
|
command: (roomId: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms unmute 100 ${roomId} bad, bad user`,
|
|
method: Method.unmute,
|
|
sameRoom: true,
|
|
}),
|
|
|
|
// Kick bad users in one room, using date syntax, with reason.
|
|
new Experiment({
|
|
name: "kick with date and reason",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" kick 100 ${roomId} bad, bad user`,
|
|
shouldAffectControlProtected: false,
|
|
n: 1,
|
|
method: Method.kick,
|
|
}),
|
|
// Ban bad users in one room, using date syntax, with reason.
|
|
new Experiment({
|
|
name: "ban with date and reason",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" ban 100 ${roomId} bad, bad user`,
|
|
method: Method.ban,
|
|
}),
|
|
// Mute bad users in one room, using date syntax, with reason.
|
|
new Experiment({
|
|
name: "mute with date and reason",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" mute 100 ${roomId} bad, bad user`,
|
|
method: Method.mute,
|
|
}),
|
|
new Experiment({
|
|
name: "unmute with date and reason",
|
|
command: (roomId: string) => `!mjolnir since "${cutDate}" unmute 100 ${roomId} bad, bad user`,
|
|
method: Method.unmute,
|
|
sameRoom: true,
|
|
}),
|
|
|
|
// Kick bad users in one room, using duration syntax, without reason, using alias.
|
|
new Experiment({
|
|
name: "kick with duration, no reason, alias",
|
|
command: (_: string, roomAlias: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomAlias}`,
|
|
method: Method.kick,
|
|
}),
|
|
// Kick bad users in one room, using duration syntax, with reason, using alias.
|
|
new Experiment({
|
|
name: "kick with duration, reason and alias",
|
|
command: (_: string, roomAlias: string) => `!mjolnir since ${Date.now() - cutDate.getTime()}ms kick 100 ${roomAlias} for some reason`,
|
|
method: Method.kick,
|
|
}),
|
|
|
|
// Kick bad users everywhere, no reason
|
|
new Experiment({
|
|
name: "kick with date everywhere",
|
|
command: () => `!mjolnir since "${cutDate}" kick 100 * bad, bad user`,
|
|
shouldAffectControlProtected: true,
|
|
n: NUMBER_OF_ROOMS - 1,
|
|
method: Method.kick,
|
|
}),
|
|
]) {
|
|
experiment.addTo(EXPERIMENTS);
|
|
}
|
|
|
|
// Just-in-case health check, before starting.
|
|
{
|
|
const usersInUnprotectedControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_UNPROTECTED_ROOM_ID);
|
|
const usersInControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_PROTECTED_ID);
|
|
for (let userId of goodUserIds) {
|
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `Initially, good user ${userId} should be in the unprotected control room`);
|
|
assert.ok(usersInControlProtected.includes(userId), `Initially, good user ${userId} should be in the control room`);
|
|
}
|
|
for (let userId of badUserIds) {
|
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `Initially, bad user ${userId} should be in the unprotected control room`);
|
|
assert.ok(usersInControlProtected.includes(userId), `Initially, bad user ${userId} should be in the control room`);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < EXPERIMENTS.length; ++i) {
|
|
const experiment = EXPERIMENTS[i];
|
|
const index = experiment.roomIndex!;
|
|
const roomId = roomIds[index];
|
|
const roomAlias = roomAliases[index];
|
|
const joined = this.mjolnir.roomJoins.getUsersInRoom(roomId, start, 100);
|
|
console.debug(`Running experiment ${i} "${experiment.name}" in room index ${index} (${roomId} / ${roomAlias}): \`${experiment.command(roomId, roomAlias)}\``);
|
|
assert.ok(joined.length >= 2 * SAMPLE_SIZE, `In experiment ${experiment.name}, we should have seen ${2 * SAMPLE_SIZE} users, saw ${joined.length}`);
|
|
|
|
// Run experiment.
|
|
await getNthReply(this.mjolnir.client, this.mjolnir.managementRoomId, experiment.n, async () => {
|
|
const command = experiment.command(roomId, roomAlias);
|
|
let result = await this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command });
|
|
return result;
|
|
});
|
|
|
|
// Check post-conditions.
|
|
const usersInRoom = await this.mjolnir.client.getJoinedRoomMembers(roomId);
|
|
const usersInUnprotectedControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_UNPROTECTED_ROOM_ID);
|
|
const usersInControlProtected = await this.mjolnir.client.getJoinedRoomMembers(CONTROL_PROTECTED_ID);
|
|
for (let userId of goodUserIds) {
|
|
assert.ok(usersInRoom.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in affected room`);
|
|
assert.ok(usersInControlProtected.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in control room (${CONTROL_PROTECTED_ID})`);
|
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `After a ${experiment.name}, good user ${userId} should still be in unprotected control room (${CONTROL_UNPROTECTED_ROOM_ID})`);
|
|
}
|
|
if (experiment.method === Method.mute) {
|
|
for (let userId of goodUserIds) {
|
|
let canSpeak = await this.mjolnir.client.userHasPowerLevelFor(userId, roomId, "m.message", false);
|
|
assert.ok(canSpeak, `After a ${experiment.name}, good user ${userId} should still be allowed to speak in the room`);
|
|
}
|
|
for (let userId of badUserIds) {
|
|
let canSpeak = await this.mjolnir.client.userHasPowerLevelFor(userId, roomId, "m.message", false);
|
|
assert.ok(!canSpeak, `After a ${experiment.name}, bad user ${userId} should NOT be allowed to speak in the room`);
|
|
}
|
|
} else if (experiment.method === Method.unmute) {
|
|
for (let userId of goodUserIds) {
|
|
let canSpeak = await this.mjolnir.client.userHasPowerLevelFor(userId, roomId, "m.message", false);
|
|
assert.ok(canSpeak, `After a ${experiment.name}, good user ${userId} should still be allowed to speak in the room`);
|
|
}
|
|
for (let userId of badUserIds) {
|
|
let canSpeak = await this.mjolnir.client.userHasPowerLevelFor(userId, roomId, "m.message", false);
|
|
assert.ok(canSpeak, `After a ${experiment.name}, bad user ${userId} should AGAIN be allowed to speak in the room`);
|
|
}
|
|
} else {
|
|
for (let userId of badUserIds) {
|
|
assert.ok(!usersInRoom.includes(userId), `After a ${experiment.name}, bad user ${userId} should NOT be in affected room`);
|
|
assert.equal(usersInControlProtected.includes(userId), !experiment.shouldAffectControlProtected, `After a ${experiment.name}, bad user ${userId} should ${experiment.shouldAffectControlProtected ? "NOT" : "still"} be in control room`);
|
|
assert.ok(usersInUnprotectedControlProtected.includes(userId), `After a ${experiment.name}, bad user ${userId} should still be in unprotected control room`);
|
|
const leaveEvent = await this.mjolnir.client.getRoomStateEvent(roomId, "m.room.member", userId);
|
|
switch (experiment.method) {
|
|
case Method.kick:
|
|
assert.equal(leaveEvent.membership, "leave");
|
|
break;
|
|
case Method.ban:
|
|
assert.equal(leaveEvent.membership, "ban");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|