Refactor list creation with MSC3784 support.

https://github.com/matrix-org/matrix-spec-proposals/pull/3784

This was extracted from the appservice mjolnir work to reduce review burden.
This commit is contained in:
gnuxie 2022-10-17 19:05:02 +01:00
parent 5bd23ced9b
commit 18d2f5fceb
3 changed files with 99 additions and 29 deletions

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { SHORTCODE_EVENT_TYPE } from "../models/PolicyList"; import PolicyList from "../models/PolicyList";
import { Permalinks, RichReply } from "matrix-bot-sdk"; import { Permalinks, RichReply } from "matrix-bot-sdk";
// !mjolnir list create <shortcode> <alias localpart> // !mjolnir list create <shortcode> <alias localpart>
@ -23,34 +23,12 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir:
const shortcode = parts[3]; const shortcode = parts[3];
const aliasLocalpart = parts[4]; const aliasLocalpart = parts[4];
const powerLevels: { [key: string]: any } = { const listRoomId = await PolicyList.createList(
"ban": 50, mjolnir.client,
"events": { shortcode,
"m.room.name": 100, [event['sender']],
"m.room.power_levels": 100, { room_alias_name: aliasLocalpart }
}, );
"events_default": 50, // non-default
"invite": 0,
"kick": 50,
"notifications": {
"room": 20,
},
"redact": 50,
"state_default": 50,
"users": {
[await mjolnir.client.getUserId()]: 100,
[event["sender"]]: 50
},
"users_default": 0,
};
const listRoomId = await mjolnir.client.createRoom({
preset: "public_chat",
room_alias_name: aliasLocalpart,
invite: [event['sender']],
initial_state: [{ type: SHORTCODE_EVENT_TYPE, state_key: "", content: { shortcode: shortcode } }],
power_level_content_override: powerLevels,
});
const roomRef = Permalinks.forRoom(listRoomId); const roomRef = Permalinks.forRoom(listRoomId);
await mjolnir.watchList(roomRef); await mjolnir.watchList(roomRef);

View File

@ -86,6 +86,10 @@ class PolicyList extends EventEmitter {
// Events that we have already informed the batcher about, that we haven't loaded from the room state yet. // Events that we have already informed the batcher about, that we haven't loaded from the room state yet.
private batchedEvents = new Set<string /* event id */>(); private batchedEvents = new Set<string /* event id */>();
/** MSC3784 support. Please note that policy lists predate room types. So there will be lists in the wild without this type. */
public static readonly ROOM_TYPE = "support.feline.policy.lists.msc.v1";
public static readonly ROOM_TYPE_VARIANTS = [PolicyList.ROOM_TYPE]
/** /**
* This is used to annotate state events we store with the rule they are associated with. * This is used to annotate state events we store with the rule they are associated with.
* If we refactor this, it is important to also refactor any listeners to 'PolicyList.update' * If we refactor this, it is important to also refactor any listeners to 'PolicyList.update'
@ -105,6 +109,65 @@ class PolicyList extends EventEmitter {
this.batcher = new UpdateBatcher(this); this.batcher = new UpdateBatcher(this);
} }
/**
* Create a new policy list.
* @param client A MatrixClient that will be used to create the list.
* @param shortcode A shortcode to refer to the list with.
* @param invite A list of users to invite to the list and make moderator.
* @param createRoomOptions Additional room create options such as an alias.
* @returns The room id for the newly created policy list.
*/
public static async createList(
client: MatrixClient,
shortcode: string,
invite: string[],
createRoomOptions = {}
): Promise<string /* room id */> {
const powerLevels: { [key: string]: any } = {
"ban": 50,
"events": {
"m.room.name": 100,
"m.room.power_levels": 100,
},
"events_default": 50, // non-default
"invite": 0,
"kick": 50,
"notifications": {
"room": 20,
},
"redact": 50,
"state_default": 50,
"users": {
[await client.getUserId()]: 100,
...invite.reduce((users, mxid) => ({...users, [mxid]: 50 }), {}),
},
"users_default": 0,
};
const finalRoomCreateOptions = {
// Support for MSC3784.
creation_content: {
type: PolicyList.ROOM_TYPE
},
preset: "public_chat",
invite,
initial_state: [
{
type: SHORTCODE_EVENT_TYPE,
state_key: "",
content: {shortcode: shortcode}
}
],
power_level_content_override: powerLevels,
...createRoomOptions
};
// Guard room type in case someone overwrites it when declaring custom creation_content in future code.
if (!PolicyList.ROOM_TYPE_VARIANTS.includes(finalRoomCreateOptions.creation_content.type)) {
throw new TypeError(`Creating a policy room with a type other than the policy room type is not supported, you probably don't want to do this.`);
}
const listRoomId = await client.createRoom(finalRoomCreateOptions);
return listRoomId
}
/** /**
* The code that can be used to refer to this banlist in Mjolnir commands. * The code that can be used to refer to this banlist in Mjolnir commands.
*/ */

View File

@ -8,6 +8,7 @@ import { getMessagesByUserIn } from "../../src/utils";
import { Mjolnir } from "../../src/Mjolnir"; import { Mjolnir } from "../../src/Mjolnir";
import { ALL_RULE_TYPES, Recommendation, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/ListRule"; import { ALL_RULE_TYPES, Recommendation, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/ListRule";
import AccessControlUnit, { Access, EntityAccess } from "../../src/models/AccessControlUnit"; import AccessControlUnit, { Access, EntityAccess } from "../../src/models/AccessControlUnit";
import { randomUUID } from "crypto";
/** /**
* Create a policy rule in a policy room. * Create a policy rule in a policy room.
@ -564,3 +565,31 @@ describe('Test: AccessControlUnit interaction with policy lists.', function() {
assertAccess(Access.Allowed, aclUnit.getAccessForServer(banMeServer), "Should not longer be any rules"); assertAccess(Access.Allowed, aclUnit.getAccessForServer(banMeServer), "Should not longer be any rules");
}) })
}) })
describe('Test: Creating policy lists.', function() {
it('Will automatically invite and op users from invites', async function() {
const mjolnir: Mjolnir = this.mjolnir;
const testUsers = await Promise.all([...Array(2)].map(_ => newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } })))
const invite = await Promise.all(testUsers.map(client => client.getUserId()));
const policyListId = await PolicyList.createList(
mjolnir.client,
randomUUID(),
invite
);
// Check power levels are right.
const powerLevelEvent = await mjolnir.client.getRoomStateEvent(policyListId, "m.room.power_levels", "");
assert.equal(Object.keys(powerLevelEvent.users ?? {}).length, invite.length + 1);
// Check create event for MSC3784 support.
const createEvent = await mjolnir.client.getRoomStateEvent(policyListId, "m.room.create", "");
assert.equal(createEvent.type, PolicyList.ROOM_TYPE);
// We can't create rooms without forgetting the type.
await assert.rejects(
async () => {
await PolicyList.createList(mjolnir.client, randomUUID(), [], {
creation_content: {}
})
},
TypeError
);
})
})