diff --git a/src/commands/CreateBanListCommand.ts b/src/commands/CreateBanListCommand.ts index 3462886..a7b7b86 100644 --- a/src/commands/CreateBanListCommand.ts +++ b/src/commands/CreateBanListCommand.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { Mjolnir } from "../Mjolnir"; -import { SHORTCODE_EVENT_TYPE } from "../models/PolicyList"; +import PolicyList from "../models/PolicyList"; import { Permalinks, RichReply } from "matrix-bot-sdk"; // !mjolnir list create @@ -23,34 +23,12 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir: const shortcode = parts[3]; const aliasLocalpart = parts[4]; - 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 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 listRoomId = await PolicyList.createList( + mjolnir.client, + shortcode, + [event['sender']], + { room_alias_name: aliasLocalpart } + ); const roomRef = Permalinks.forRoom(listRoomId); await mjolnir.watchList(roomRef); diff --git a/src/models/PolicyList.ts b/src/models/PolicyList.ts index 9c3aba0..1256aca 100644 --- a/src/models/PolicyList.ts +++ b/src/models/PolicyList.ts @@ -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. private batchedEvents = new Set(); + /** 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. * 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); } + /** + * 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 { + 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. */ diff --git a/test/integration/banListTest.ts b/test/integration/banListTest.ts index 89660a4..658b722 100644 --- a/test/integration/banListTest.ts +++ b/test/integration/banListTest.ts @@ -8,6 +8,7 @@ import { getMessagesByUserIn } from "../../src/utils"; import { Mjolnir } from "../../src/Mjolnir"; 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 { randomUUID } from "crypto"; /** * 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"); }) }) + +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 + ); + }) +})