diff --git a/package.json b/package.json index a2a36cfd..eba1fe3a 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "dependencies": { "@sentry/node": "^7.17.2", "@sentry/tracing": "^7.17.2", + "@sinclair/typebox": "~0.31.15", "await-lock": "^2.2.2", "body-parser": "^1.20.2", "config": "^3.3.9", @@ -59,14 +60,18 @@ "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "matrix-appservice-bridge": "^9.0.1", - "matrix-protection-suite": "link:/home/user/experiments/matrix-protection-suite", - "matrix-protection-suite-for-matrix-bot-sdk": "link:/home/user/experiments/matrix-protection-suite-for-matrix-bot-sdk", + "matrix-protection-suite": "git+https://github.com/Gnuxie/matrix-protection-suite.git#0.8.0", + "matrix-protection-suite-for-matrix-bot-sdk": "git+https://github.com/Gnuxie/matrix-protection-suite-for-matrix-bot-sdk.git#2df8b462442a42c975f7932d17a08c3aea23604b", "parse-duration": "^1.0.2", "pg": "^8.8.0", "shell-quote": "^1.7.3", "ulidx": "^2.2.1", "yaml": "^2.3.2" }, + "overrides": { + "matrix-bot-sdk": "$@vector-im/matrix-bot-sdk", + "@vector-im/matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.6.6-element.1" + }, "engines": { "node": ">=18.0.0" } diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts deleted file mode 100644 index 4ad94e4e..00000000 --- a/src/Mjolnir.ts +++ /dev/null @@ -1,501 +0,0 @@ -/** - * Copyright (C) 2022 Gnuxie - * All rights reserved. - * - * This file is modified and is NOT licensed under the Apache License. - * This modified file incorperates work from mjolnir - * https://github.com/matrix-org/mjolnir - * which included the following license notice: - -Copyright 2019-2021 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. - * - * However, this file is modified and the modifications in this file - * are NOT distributed, contributed, committed, or licensed under the Apache License. - */ - -import { - LogLevel, - LogService, - MembershipEvent, -} from "matrix-bot-sdk"; - -import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; -import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue"; -import { htmlEscape } from "./utils"; -import { ReportManager } from "./report/ReportManager"; -import { ReportPoller } from "./report/ReportPoller"; -import { WebAPIs } from "./webapis/WebAPIs"; -import { ThrottlingQueue } from "./queues/ThrottlingQueue"; -import { getDefaultConfig, IConfig } from "./config"; -import ManagementRoomOutput from "./ManagementRoomOutput"; -import { ProtectionManager } from "./protections/ProtectionManager"; -import { findCommandTable } from "./commands/interface-manager/InterfaceCommand"; -import { MatrixReactionHandler } from "./commands/interface-manager/MatrixReactionHandler"; -import { ProtectedRoomsSet } from "matrix-protection-suite"; - -export const STATE_NOT_STARTED = "not_started"; -export const STATE_CHECKING_PERMISSIONS = "checking_permissions"; -export const STATE_SYNCING = "syncing"; -export const STATE_RUNNING = "running"; - -export class Mjolnir { - private displayName: string; - private localpart: string; - private currentState: string = STATE_NOT_STARTED; - /** - * This is for users who are not listed on a watchlist, - * but have been flagged by the automatic spam detection as suispicous - */ - private unlistedUserRedactionQueue = new UnlistedUserRedactionQueue(); - - private webapis: WebAPIs; - public taskQueue: ThrottlingQueue; - /** - * Reporting back to the management room. - */ - public readonly managementRoomOutput: ManagementRoomOutput; - /* - * Config-enabled polling of reports in Synapse, so Mjolnir can react to reports - */ - private reportPoller?: ReportPoller; - /** - * Store the protections being used by Mjolnir. - */ - public readonly protectionManager: ProtectionManager; - /** - * Handle user reports from the homeserver. - */ - public readonly reportManager: ReportManager; - - private readonly commandTable = findCommandTable("mjolnir"); - - public readonly reactionHandler: MatrixReactionHandler; - - /** - * Adds a listener to the client that will automatically accept invitations. - * @param {MatrixSendClient} client - * @param options By default accepts invites from anyone. - * @param {string} options.managementRoom The room to report ignored invitations to if `recordIgnoredInvites` is true. - * @param {boolean} options.recordIgnoredInvites Whether to report invites that will be ignored to the `managementRoom`. - * @param {boolean} options.autojoinOnlyIfManager Whether to only accept an invitation by a user present in the `managementRoom`. - * @param {string} options.acceptInvitesFromSpace A space of users to accept invites from, ignores invites form users not in this space. - */ - private static addJoinOnInviteListener(mjolnir: Mjolnir, client: MatrixSendClient, options: { [key: string]: any }) { - mjolnir.matrixEmitter.on("room.invite", async (roomId: string, inviteEvent: any) => { - const membershipEvent = new MembershipEvent(inviteEvent); - - const reportInvite = async () => { - if (!options.recordIgnoredInvites) return; // Nothing to do - - await client.sendMessage(mjolnir.managementRoomId, { - msgtype: "m.text", - body: `${membershipEvent.sender} has invited me to ${roomId} but the config prevents me from accepting the invitation. ` - + `If you would like this room protected, use "!mjolnir rooms add ${roomId}" so I can accept the invite.`, - format: "org.matrix.custom.html", - formatted_body: `${htmlEscape(membershipEvent.sender)} has invited me to ${htmlEscape(roomId)} but the config prevents me from ` - + `accepting the invitation. If you would like this room protected, use !mjolnir rooms add ${htmlEscape(roomId)} ` - + `so I can accept the invite.`, - }); - }; - - if (options.autojoinOnlyIfManager) { - const managers = await client.getJoinedRoomMembers(mjolnir.managementRoomId); - if (!managers.includes(membershipEvent.sender)) return reportInvite(); // ignore invite - } else { - const spaceId = await client.resolveRoom(options.acceptInvitesFromSpace); - const spaceUserIds = await client.getJoinedRoomMembers(spaceId) - .catch(async e => { - if (e.body?.errcode === "M_FORBIDDEN") { - await mjolnir.managementRoomOutput.logMessage(LogLevel.ERROR, 'Mjolnir', `Mjolnir is not in the space configured for acceptInvitesFromSpace, did you invite it?`); - await client.joinRoom(spaceId); - return await client.getJoinedRoomMembers(spaceId); - } else { - return Promise.reject(e); - } - }); - if (!spaceUserIds.includes(membershipEvent.sender)) return reportInvite(); // ignore invite - } - return client.joinRoom(roomId); - }); - } - - /** - * Create a new Mjolnir instance from a client and the options in the configuration file, ready to be started. - * @param {MatrixSendClient} client The client for Mjolnir to use. - * @returns A new Mjolnir instance that can be started without further setup. - */ - static async setupMjolnirFromConfig(client: MatrixSendClient, matrixEmitter: MatrixEmitter, config: IConfig): Promise { - if (!config.autojoinOnlyIfManager && config.acceptInvitesFromSpace === getDefaultConfig().acceptInvitesFromSpace) { - throw new TypeError("`autojoinOnlyIfManager` has been disabled, yet no space has been provided for `acceptInvitesFromSpace`."); - } - const joinedRooms = await client.getJoinedRooms(); - - // Ensure we're also in the management room - LogService.info("index", "Resolving management room..."); - const managementRoomId = await client.resolveRoom(config.managementRoom); - if (!joinedRooms.includes(managementRoomId)) { - await client.joinRoom(config.managementRoom); - } - - const mjolnir = new Mjolnir(client, await client.getUserId(), matrixEmitter, managementRoomId, config); - await mjolnir.managementRoomOutput.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status."); - Mjolnir.addJoinOnInviteListener(mjolnir, client, config); - return mjolnir; - } - - constructor( - public readonly client: MatrixSendClient, - private readonly clientUserId: string, - public readonly matrixEmitter: MatrixEmitter, - public readonly managementRoomId: string, - public readonly config: IConfig, - private readonly protectedRoomsSet: ProtectedRoomsSet, - ) { - this.protectedRoomsConfig = new ProtectedRoomsConfig(client); - this.policyListManager = new PolicyListManager(this); - this.reactionHandler = new MatrixReactionHandler(this.managementRoomId, client, clientUserId); - - const mutedModules = (LogService as any).mutedModules; - if (!Array.isArray(mutedModules)) { - throw new TypeError("MatrixBotSdk has changed their hacky handling of muted modules, praise be"); - } - for (const module of config.logMutedModules) { - if (!mutedModules.includes(module)) { - LogService.muteModule(module); - } - } - // Setup bot. - - matrixEmitter.on("room.event", this.handleEvent.bind(this)); - - matrixEmitter.on("room.message", async (roomId, event) => { - if (roomId !== this.managementRoomId) return; - if (!event['content']) return; - - const content = event['content']; - if (content['msgtype'] === "m.text" && content['body']) { - const prefixes = [ - COMMAND_PREFIX, - this.localpart + ":", - this.displayName + ":", - await client.getUserId() + ":", - this.localpart + " ", - this.displayName + " ", - await client.getUserId() + " ", - ...config.commands.additionalPrefixes.map(p => `!${p}`), - ...config.commands.additionalPrefixes.map(p => `${p}:`), - ...config.commands.additionalPrefixes.map(p => `${p} `), - ...config.commands.additionalPrefixes, - ]; - if (config.commands.allowNoPrefix) prefixes.push("!"); - - const prefixUsed = prefixes.find(p => content['body'].toLowerCase().startsWith(p.toLowerCase())); - if (!prefixUsed) return; - - // rewrite the event body to make the prefix uniform (in case the bot has spaces in its display name) - let restOfBody = content['body'].substring(prefixUsed.length); - if (!restOfBody.startsWith(" ")) restOfBody = ` ${restOfBody}`; - event['content']['body'] = COMMAND_PREFIX + restOfBody; - LogService.info("Mjolnir", `Command being run by ${event['sender']}: ${event['content']['body']}`); - - await client.sendReadReceipt(roomId, event['event_id']); - - return handleCommand(roomId, event, this, this.commandTable); - } - }); - - matrixEmitter.on("room.join", (roomId: string, event: any) => { - LogService.info("Mjolnir", `Joined ${roomId}`); - return this.resyncJoinedRooms(); - }); - matrixEmitter.on("room.leave", (roomId: string, event: any) => { - LogService.info("Mjolnir", `Left ${roomId}`); - return this.resyncJoinedRooms(); - }); - - client.getUserId().then(userId => { - this.localpart = userId.split(':')[0].substring(1); - return client.getUserProfile(userId); - }).then(profile => { - if (profile['displayname']) { - this.displayName = profile['displayname']; - } - }); - - // Setup Web APIs - console.log("Creating Web APIs"); - this.reportManager = new ReportManager(this); - this.webapis = new WebAPIs(this.reportManager, this.config); - if (config.pollReports) { - this.reportPoller = new ReportPoller(this, this.reportManager); - } - // Setup join/leave listener - this.roomJoins = new RoomMemberManager(this.matrixEmitter); - this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS); - - this.protectionManager = new ProtectionManager(this); - - this.managementRoomOutput = new ManagementRoomOutput(managementRoomId, client, config); - const protections = new ProtectionManager(this); - this.protectedRoomsTracker = new ProtectedRoomsSet(client, clientUserId, managementRoomId, this.managementRoomOutput, protections, config); - } - - public get state(): string { - return this.currentState; - } - - /** - * Returns the handler to flag a user for redaction, removing any future messages that they send. - * Typically this is used by the flooding or image protection on users that have not been banned from a list yet. - * It cannot used to redact any previous messages the user has sent, in that cas you should use the `EventRedactionQueue`. - */ - public get unlistedUserRedactionHandler(): UnlistedUserRedactionQueue { - return this.unlistedUserRedactionQueue; - } - - /** - * Start Mjölnir. - */ - public async start() { - try { - // Start the web server. - console.log("Starting web server"); - await this.webapis.start(); - - if (this.reportPoller) { - let reportPollSetting: { from: number } = { from: 0 }; - try { - reportPollSetting = await this.client.getAccountData(REPORT_POLL_EVENT_TYPE); - } catch (err) { - if (err.body?.errcode !== "M_NOT_FOUND") { - throw err; - } else { - this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "report poll setting does not exist yet"); - } - } - this.reportPoller.start(reportPollSetting.from); - } - - // Load the state. - this.currentState = STATE_CHECKING_PERMISSIONS; - - await this.managementRoomOutput.logMessage(LogLevel.DEBUG, "Mjolnir@startup", "Loading protected rooms..."); - await this.protectedRoomsConfig.loadProtectedRoomsFromConfig(this.config); - await this.protectedRoomsConfig.loadProtectedRoomsFromAccountData(); - this.protectedRoomsConfig.getExplicitlyProtectedRooms().forEach(this.protectRoom, this); - // We have to build the policy lists before calling `resyncJoinedRooms` otherwise mjolnir will try to protect - // every policy list we are already joined to, as mjolnir will not be able to distinguish them from normal rooms. - await this.policyListManager.start(); - await this.resyncJoinedRooms(false); - await this.protectionManager.start(); - this.reactionHandler.start(this.matrixEmitter); - - if (this.config.verifyPermissionsOnStartup) { - await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions..."); - await this.protectedRoomsTracker.verifyPermissions(); - } - - // Start the bot. - await this.matrixEmitter.start(); - - this.currentState = STATE_SYNCING; - if (this.config.syncOnStartup) { - await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists..."); - await this.protectedRoomsTracker.syncLists(); - } - - this.currentState = STATE_RUNNING; - await this.managementRoomOutput.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms."); - if (this.config.verboseLogging) { - await this.managementRoomOutput.logMessage(LogLevel.WARN, "Mjolnir@startup", "The use of verbose logging is deprecated and will be removed in a future version, check your config."); - } - } catch (err) { - try { - LogService.error("Mjolnir", "Error during startup:", err); - this.stop(); - await this.managementRoomOutput.logMessage(LogLevel.ERROR, "Mjolnir@startup", "Startup failed due to error - see console"); - } catch (e) { - LogService.error("Mjolnir", `Failed to report startup error to the management room:`, e); - } - throw err; - } - } - - /** - * Stop Mjolnir from syncing and processing commands. - */ - public stop() { - LogService.info("Mjolnir", "Stopping Mjolnir..."); - this.matrixEmitter.stop(); - this.reactionHandler.stop(this.matrixEmitter); - this.webapis.stop(); - this.reportPoller?.stop(); - } - - /** - * Rooms that mjolnir is configured to explicitly protect. - * Do not use to access all of the rooms that mjolnir protects. - * FIXME: In future ProtectedRoomsSet on this mjolnir should not be public and should also be accessed via a delegator method. - */ - public get explicitlyProtectedRooms(): string[] { - return this.protectedRoomsConfig.getExplicitlyProtectedRooms() - } - - /** - * Explicitly protect this room, adding it to the account data. - * Should NOT be used to protect a room to implement e.g. `config.protectAllJoinedRooms`, - * use `protectRoom` instead. - * @param roomId The room to be explicitly protected by mjolnir and persisted in config. - */ - public async addProtectedRoom(roomId: string) { - await this.protectedRoomsConfig.addProtectedRoom(roomId); - this.protectRoom(roomId); - } - - /** - * Protect the room, but do not persist it to the account data. - * @param roomId The room to protect. - */ - private protectRoom(roomId: string): void { - this.protectedRoomsTracker.addProtectedRoom(roomId); - this.roomJoins.addRoom(roomId); - } - - /** - * Remove a room from the explicitly protect set of rooms that is persisted to account data. - * Should NOT be used to remove a room that we have left, e.g. when implementing `config.protectAllJoinedRooms`, - * use `unprotectRoom` instead. - * @param roomId The room to remove from account data and stop protecting. - */ - public async removeProtectedRoom(roomId: string) { - await this.protectedRoomsConfig.removeProtectedRoom(roomId); - this.unprotectRoom(roomId); - } - - /** - * Unprotect a room. - * @param roomId The room to stop protecting. - */ - private unprotectRoom(roomId: string): void { - this.roomJoins.removeRoom(roomId); - this.protectedRoomsTracker.removeProtectedRoom(roomId); - } - - /** - * Resynchronize the protected rooms with rooms that the mjolnir user is joined to. - * This is to implement `config.protectAllJoinedRooms` functionality. - * @param withSync Whether to synchronize all protected rooms with the watched policy lists afterwards. - */ - private async resyncJoinedRooms(withSync = true): Promise { - if (!this.config.protectAllJoinedRooms) return; - - // We filter out all policy rooms so that we only protect ones that are - // explicitly protected, so that we don't try to protect lists that we are just watching. - const filterOutManagementAndPolicyRooms = (roomId: string) => { - const policyListIds = this.policyListManager.lists.map(list => list.roomId); - return roomId !== this.managementRoomId && !policyListIds.includes(roomId); - }; - - const joinedRoomIdsToProtect = new Set([ - ...(await this.client.getJoinedRooms()).filter(filterOutManagementAndPolicyRooms), - // We do this specifically so policy lists that have been explicitly marked as protected - // will be protected. - ...this.protectedRoomsConfig.getExplicitlyProtectedRooms(), - ]); - const previousRoomIdsProtecting = new Set(this.protectedRoomsTracker.getProtectedRooms()); - // find every room that we have left (since last time) - for (const roomId of previousRoomIdsProtecting.keys()) { - if (!joinedRoomIdsToProtect.has(roomId)) { - // Then we have left this room. - this.unprotectRoom(roomId); - } - } - // find every room that we have joined (since last time). - for (const roomId of joinedRoomIdsToProtect.keys()) { - if (!previousRoomIdsProtecting.has(roomId)) { - // Then we have joined this room - this.protectRoom(roomId); - } - } - - if (withSync) { - await this.protectedRoomsTracker.syncLists(); - } - } - - private async handleEvent(roomId: string, event: any) { - // Check for UISI errors - if (roomId === this.managementRoomId) { - if (event['type'] === 'm.room.message' && event['content'] && event['content']['body']) { - if (event['content']['body'] === "** Unable to decrypt: The sender's device has not sent us the keys for this message. **") { - // UISI - await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '⚠'); - await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], 'UISI'); - await this.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '🚨'); - } - } - } - - // Check for updated ban lists before checking protected rooms - the ban lists might be protected - // themselves. - const policyList = this.policyListManager.lists.find(list => list.roomId === roomId); - if (policyList !== undefined) { - if (ALL_BAN_LIST_RULE_TYPES.includes(event['type']) || event['type'] === 'm.room.redaction') { - policyList.updateForEvent(event.event_id) - } - } - - if (event.sender !== this.clientUserId) { - this.protectedRoomsTracker.handleEvent(roomId, event); - } - } - - public async isSynapseAdmin(): Promise { - try { - const endpoint = `/_synapse/admin/v1/users/${await this.client.getUserId()}/admin`; - const response = await this.client.doRequest("GET", endpoint); - return response['admin']; - } catch (e) { - LogService.error("Mjolnir", "Error determining if Mjolnir is a server admin:", e); - return false; // assume not - } - } - - public async deactivateSynapseUser(userId: string): Promise { - const endpoint = `/_synapse/admin/v1/deactivate/${userId}`; - return await this.client.doRequest("POST", endpoint); - } - - public async shutdownSynapseRoom(roomId: string, message?: string): Promise { - const endpoint = `/_synapse/admin/v1/rooms/${roomId}`; - return await this.client.doRequest("DELETE", endpoint, null, { - new_room_user_id: await this.client.getUserId(), - block: true, - message: message /* If `undefined`, we'll use Synapse's default message. */ - }); - } - - /** - * Make a user administrator via the Synapse Admin API - * @param roomId the room where the user (or the bot) shall be made administrator. - * @param userId optionally specify the user mxID to be made administrator. - */ - public async makeUserRoomAdmin(roomId: string, userId: string): Promise { - const endpoint = `/_synapse/admin/v1/rooms/${roomId}/make_room_admin`; - return await this.client.doRequest("POST", endpoint, null, { - user_id: userId - }); - } -} diff --git a/src/index.ts b/src/index.ts index 607b8221..090c0c82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,8 +40,11 @@ import { } from "matrix-bot-sdk"; import { StoreType } from "@matrix-org/matrix-sdk-crypto-nodejs"; import { read as configRead } from "./config"; -import { Mjolnir } from "./Mjolnir"; import { initializeSentry, patchMatrixClient } from "./utils"; +import { makeDraupnirBotModeFromConfig } from "./DraupnirBotMode"; +import { Draupnir } from "./Draupnir"; +import { SafeMatrixEmitterWrapper } from "matrix-protection-suite-for-matrix-bot-sdk"; +import { DefaultEventDecoder } from "matrix-protection-suite"; (async function () { @@ -64,7 +67,7 @@ import { initializeSentry, patchMatrixClient } from "./utils"; healthz.listen(); } - let bot: Mjolnir | null = null; + let bot: Draupnir | null = null; try { const storagePath = path.isAbsolute(config.dataPath) ? config.dataPath : path.join(__dirname, '../', config.dataPath); const storage = new SimpleFsStorageProvider(path.join(storagePath, "bot.json")); @@ -86,7 +89,7 @@ import { initializeSentry, patchMatrixClient } from "./utils"; patchMatrixClient(); config.RUNTIME.client = client; - bot = await Mjolnir.setupMjolnirFromConfig(client, client, config); + bot = await makeDraupnirBotModeFromConfig(client, new SafeMatrixEmitterWrapper(client, DefaultEventDecoder), config); } catch (err) { console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`); throw err; diff --git a/yarn.lock b/yarn.lock index 221585e7..2eebca44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -211,10 +211,10 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@sinclair/typebox@^0.31.15": - version "0.31.21" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.31.21.tgz#d52d8e35f71e5651042aa0237e918e4b21fbbbf8" - integrity sha512-Wtq/K44EMkREaXytK+2c5DrygtYsH7ZxT0StQL8HMJz2BoOM7NZ/xfrUFBVuZxDrhJCoXf5Im282P2CCz5DHwQ== +"@sinclair/typebox@~0.31.15": + version "0.31.28" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.31.28.tgz#b68831e7bc7d09daac26968ea32f42bedc968ede" + integrity sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ== "@types/body-parser@*": version "1.19.2" @@ -2319,13 +2319,19 @@ matrix-appservice@^2.0.0: request-promise "^4.2.6" sanitize-html "^2.8.0" -"matrix-protection-suite-for-matrix-bot-sdk@link:/home/user/experiments/matrix-protection-suite-for-matrix-bot-sdk": - version "0.0.0" - uid "" +"matrix-protection-suite-for-matrix-bot-sdk@git+https://github.com/Gnuxie/matrix-protection-suite-for-matrix-bot-sdk.git#2df8b462442a42c975f7932d17a08c3aea23604b": + version "0.8.0" + resolved "git+https://github.com/Gnuxie/matrix-protection-suite-for-matrix-bot-sdk.git#2df8b462442a42c975f7932d17a08c3aea23604b" -"matrix-protection-suite@link:../../experiments/matrix-protection-suite": - version "0.0.0" - uid "" +"matrix-protection-suite@git+https://github.com/Gnuxie/matrix-protection-suite.git#0.8.0": + version "0.8.0" + resolved "git+https://github.com/Gnuxie/matrix-protection-suite.git#e67a5fcbba9565acad7da43fb84c4a4c0f321466" + dependencies: + await-lock "^2.2.2" + crypto-js "^4.1.1" + glob-to-regexp "^0.4.1" + immutable "^5.0.0-beta.4" + ulidx "^2.1.0" media-typer@0.3.0: version "0.3.0"