diff --git a/config/default.yaml b/config/default.yaml index 6b94fc1..3a2e560 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -34,9 +34,9 @@ dataPath: "/data/storage" # If true (the default), Mjolnir will only accept invites from users present in managementRoom. autojoinOnlyIfManager: true -# If `autojoinOnlyIfManager` is false, only the members in this group can invite +# If `autojoinOnlyIfManager` is false, only the members in this space can invite # the bot to new rooms. -acceptInvitesFromGroup: "+example:example.org" +acceptInvitesFromSpace: "!example:example.org" # Whether Mjolnir should report ignored invites to the management room (if autojoinOnlyIfManager is true). recordIgnoredInvites: false diff --git a/config/harness.yaml b/config/harness.yaml index e3c3a9d..18b992f 100644 --- a/config/harness.yaml +++ b/config/harness.yaml @@ -32,9 +32,9 @@ dataPath: "./test/harness/mjolnir-data/" # to new rooms. autojoinOnlyIfManager: true -# If `autojoinOnlyIfManager` is false, only the members in this group can invite +# If `autojoinOnlyIfManager` is false, only the members in this space can invite # the bot to new rooms. -acceptInvitesFromGroup: '+example:example.org' +acceptInvitesFromSpace: '!example:example.org' # If the bot is invited to a room and it won't accept the invite (due to the # conditions above), report it to the management room. Defaults to disabled (no diff --git a/package.json b/package.json index 659c403..90c347c 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ "version": "sed -i '/# version automated/s/[0-9][0-9]*\\.[0-9][0-9]*\\.[0-9][0-9]*/'$npm_package_version'/' synapse_antispam/setup.py && git add synapse_antispam/setup.py && cat synapse_antispam/setup.py" }, "devDependencies": { - "@types/config": "0.0.41", "@types/crypto-js": "^4.0.2", "@types/html-to-text": "^8.0.1", "@types/humanize-duration": "^3.27.1", + "@types/js-yaml": "^4.0.5", "@types/jsdom": "^16.2.11", "@types/mocha": "^9.0.0", "@types/node": "^16.7.10", @@ -36,7 +36,6 @@ "typescript-formatter": "^7.2" }, "dependencies": { - "config": "^3.3.6", "express": "^4.17", "html-to-text": "^8.0.0", "humanize-duration": "^3.27.1", @@ -45,7 +44,8 @@ "jsdom": "^16.6.0", "matrix-bot-sdk": "^0.5.19", "parse-duration": "^1.0.2", - "shell-quote": "^1.7.3" + "shell-quote": "^1.7.3", + "yaml": "^2.1.1" }, "engines": { "node": ">=16.0.0" diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index d1f2acd..ce6637c 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -27,19 +27,17 @@ import { TextualMessageEventContent } from "matrix-bot-sdk"; -import BanList, { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/BanList"; +import { ALL_RULE_TYPES as ALL_BAN_LIST_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER } from "./models/ListRule"; import { applyServerAcls } from "./actions/ApplyAcl"; import { RoomUpdateError } from "./models/RoomUpdateError"; import { COMMAND_PREFIX, handleCommand } from "./commands/CommandHandler"; import { applyUserBans } from "./actions/ApplyBan"; -import config from "./config"; import ErrorCache, { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "./ErrorCache"; import { Protection } from "./protections/IProtection"; import { PROTECTIONS } from "./protections/protections"; import { ConsequenceType, Consequence } from "./protections/consequence"; import { ProtectionSettingValidationError } from "./protections/ProtectionSettings"; import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue"; -import { Healthz } from "./health/healthz"; import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue"; import { htmlEscape } from "./utils"; import { ReportManager } from "./report/ReportManager"; @@ -50,6 +48,8 @@ import RuleServer from "./models/RuleServer"; import { RoomMemberManager } from "./RoomMembers"; import { ProtectedRoomActivityTracker } from "./queues/ProtectedRoomActivityTracker"; import { ThrottlingQueue } from "./queues/ThrottlingQueue"; +import { IConfig } from "./config"; +import PolicyList, { ListRuleChange } from "./models/PolicyList"; const levelToFn = { [LogLevel.DEBUG.toString()]: LogService.debug, @@ -103,6 +103,15 @@ export class Mjolnir { private webapis: WebAPIs; private protectedRoomActivityTracker: ProtectedRoomActivityTracker; public taskQueue: ThrottlingQueue; + /** + * Used to provide mutual exclusion when synchronizing rooms with the state of a policy list. + * This is because requests operating with rules from an older version of the list that are slow + * could race & give the room an inconsistent state. An example is if we add multiple m.policy.rule.server rules, + * which would cause several requests to a room to send a new m.room.server_acl event. + * These requests could finish in any order, which has left rooms with an inconsistent server_acl event + * until Mjolnir synchronises the room with its policy lists again, which can be in the region of hours. + */ + public aclChain: Promise = Promise.resolve(); /* * Config-enabled polling of reports in Synapse, so Mjolnir can react to reports */ @@ -114,7 +123,7 @@ export class Mjolnir { * @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.acceptInvitesFromGroup A group of users to accept invites from, ignores invites form users not in this group. + * @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: MatrixClient, options: { [key: string]: any }) { client.on("room.invite", async (roomId: string, inviteEvent: any) => { @@ -138,9 +147,18 @@ export class Mjolnir { const managers = await client.getJoinedRoomMembers(mjolnir.managementRoomId); if (!managers.includes(membershipEvent.sender)) return reportInvite(); // ignore invite } else { - const groupMembers = await client.unstableApis.getGroupUsers(options.acceptInvitesFromGroup); - const userIds = groupMembers.map(m => m.user_id); - if (!userIds.includes(membershipEvent.sender)) return reportInvite(); // ignore invite + const spaceId = await client.resolveRoom(options.acceptInvitesFromSpace); + const spaceUserIds = await client.getJoinedRoomMembers(spaceId) + .catch(async e => { + if (e.body?.errcode === "M_FORBIDDEN") { + await mjolnir.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); @@ -152,8 +170,8 @@ export class Mjolnir { * @param {MatrixClient} client The client for Mjolnir to use. * @returns A new Mjolnir instance that can be started without further setup. */ - static async setupMjolnirFromConfig(client: MatrixClient): Promise { - const banLists: BanList[] = []; + static async setupMjolnirFromConfig(client: MatrixClient, config: IConfig): Promise { + const policyLists: PolicyList[] = []; const protectedRooms: { [roomId: string]: string } = {}; const joinedRooms = await client.getJoinedRooms(); // Ensure we're also joined to the rooms we're protecting @@ -178,7 +196,7 @@ export class Mjolnir { } const ruleServer = config.web.ruleServer ? new RuleServer() : null; - const mjolnir = new Mjolnir(client, managementRoomId, protectedRooms, banLists, ruleServer); + const mjolnir = new Mjolnir(client, managementRoomId, config, protectedRooms, policyLists, ruleServer); await mjolnir.logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status."); Mjolnir.addJoinOnInviteListener(mjolnir, client, config); return mjolnir; @@ -187,18 +205,19 @@ export class Mjolnir { constructor( public readonly client: MatrixClient, public readonly managementRoomId: string, + public readonly config: IConfig, /* * All the rooms that Mjolnir is protecting and their permalinks. * If `config.protectAllJoinedRooms` is specified, then `protectedRooms` will be all joined rooms except watched banlists that we can't protect (because they aren't curated by us). */ public readonly protectedRooms: { [roomId: string]: string }, - private banLists: BanList[], + private policyLists: PolicyList[], // Combines the rules from ban lists so they can be served to a homeserver module or another consumer. - public readonly ruleServer: RuleServer|null, + public readonly ruleServer: RuleServer | null, ) { this.explicitlyProtectedRoomIds = Object.keys(this.protectedRooms); - for (const reason of config.automaticallyRedactForReasons) { + for (const reason of this.config.automaticallyRedactForReasons) { this.automaticRedactionReasons.push(new MatrixGlob(reason.toLowerCase())); } @@ -266,7 +285,7 @@ export class Mjolnir { console.log("Creating Web APIs"); const reportManager = new ReportManager(this); reportManager.on("report.new", this.handleReport.bind(this)); - this.webapis = new WebAPIs(reportManager, this.ruleServer); + this.webapis = new WebAPIs(reportManager, this.config, this.ruleServer); if (config.pollReports) { this.reportPoller = new ReportPoller(this, reportManager); } @@ -275,8 +294,8 @@ export class Mjolnir { this.taskQueue = new ThrottlingQueue(this, config.backgroundDelayMS); } - public get lists(): BanList[] { - return this.banLists; + public get lists(): PolicyList[] { + return this.policyLists; } public get state(): string { @@ -343,23 +362,22 @@ export class Mjolnir { } catch (e) { LogService.warn("Mjolnir", extractRequestError(e)); } - await this.buildWatchedBanLists(); + await this.buildWatchedPolicyLists(); this.applyUnprotectedRooms(); - if (config.verifyPermissionsOnStartup) { + if (this.config.verifyPermissionsOnStartup) { await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Checking permissions..."); - await this.verifyPermissions(config.verboseLogging); + await this.verifyPermissions(this.config.verboseLogging); } this.currentState = STATE_SYNCING; - if (config.syncOnStartup) { + if (this.config.syncOnStartup) { await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Syncing lists..."); - await this.syncLists(config.verboseLogging); + await this.syncLists(this.config.verboseLogging); await this.registerProtections(); } this.currentState = STATE_RUNNING; - Healthz.isHealthy = true; await this.logMessage(LogLevel.INFO, "Mjolnir@startup", "Startup complete. Now monitoring rooms."); } catch (err) { try { @@ -389,13 +407,13 @@ export class Mjolnir { if (!additionalRoomIds) additionalRoomIds = []; if (!Array.isArray(additionalRoomIds)) additionalRoomIds = [additionalRoomIds]; - if (config.verboseLogging || LogLevel.INFO.includes(level)) { + if (this.config.verboseLogging || LogLevel.INFO.includes(level)) { let clientMessage = message; if (level === LogLevel.WARN) clientMessage = `⚠ | ${message}`; if (level === LogLevel.ERROR) clientMessage = `‼ | ${message}`; - const managementRoomId = await this.client.resolveRoom(config.managementRoom); - const roomIds = [managementRoomId, ...additionalRoomIds]; + const client = this.client; + const roomIds = [this.managementRoomId, ...additionalRoomIds]; let evContent: TextualMessageEventContent = { body: message, @@ -407,7 +425,7 @@ export class Mjolnir { evContent = await replaceRoomIdsWithPills(this, clientMessage, new Set(roomIds), "m.notice"); } - await this.client.sendMessage(managementRoomId, evContent); + await client.sendMessage(this.managementRoomId, evContent); } levelToFn[level.toString()](module, message); @@ -432,7 +450,7 @@ export class Mjolnir { const rooms = (additionalProtectedRooms?.rooms ?? []); rooms.push(roomId); await this.client.setAccountData(PROTECTED_ROOMS_EVENT_TYPE, { rooms: rooms }); - await this.syncLists(config.verboseLogging); + await this.syncLists(this.config.verboseLogging); } public async removeProtectedRoom(roomId: string) { @@ -454,7 +472,7 @@ export class Mjolnir { } private async resyncJoinedRooms(withSync = true) { - if (!config.protectAllJoinedRooms) return; + if (!this.config.protectAllJoinedRooms) return; const joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== this.managementRoomId); const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds); @@ -480,7 +498,7 @@ export class Mjolnir { this.applyUnprotectedRooms(); if (withSync) { - await this.syncLists(config.verboseLogging); + await this.syncLists(this.config.verboseLogging); } } @@ -553,12 +571,12 @@ export class Mjolnir { const validatedSettings: { [setting: string]: any } = {} for (let [key, value] of Object.entries(savedSettings)) { if ( - // is this a setting name with a known parser? - key in settingDefinitions - // is the datatype of this setting's value what we expect? - && typeof(settingDefinitions[key].value) === typeof(value) - // is this setting's value valid for the setting? - && settingDefinitions[key].validate(value) + // is this a setting name with a known parser? + key in settingDefinitions + // is the datatype of this setting's value what we expect? + && typeof (settingDefinitions[key].value) === typeof (value) + // is this setting's value valid for the setting? + && settingDefinitions[key].validate(value) ) { validatedSettings[key] = value; } else { @@ -592,8 +610,8 @@ export class Mjolnir { if (!(key in protection.settings)) { throw new ProtectionSettingValidationError(`Failed to find protection setting by name: ${key}`); } - if (typeof(protection.settings[key].value) !== typeof(value)) { - throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof(value)})`); + if (typeof (protection.settings[key].value) !== typeof (value)) { + throw new ProtectionSettingValidationError(`Invalid type for protection setting: ${key} (${typeof (value)})`); } if (!protection.settings[key].validate(value)) { throw new ProtectionSettingValidationError(`Invalid value for protection setting: ${key} (${value})`); @@ -643,16 +661,16 @@ export class Mjolnir { } /** - * Helper for constructing `BanList`s and making sure they have the right listeners set up. - * @param roomId The room id for the `BanList`. - * @param roomRef A reference (matrix.to URL) for the `BanList`. + * Helper for constructing `PolicyList`s and making sure they have the right listeners set up. + * @param roomId The room id for the `PolicyList`. + * @param roomRef A reference (matrix.to URL) for the `PolicyList`. */ - private async addBanList(roomId: string, roomRef: string): Promise { - const list = new BanList(roomId, roomRef, this.client); + private async addPolicyList(roomId: string, roomRef: string): Promise { + const list = new PolicyList(roomId, roomRef, this.client); this.ruleServer?.watch(list); - list.on('BanList.batch', this.syncWithBanList.bind(this)); + list.on('PolicyList.batch', this.syncWithPolicyList.bind(this)); await list.updateList(); - this.banLists.push(list); + this.policyLists.push(list); return list; } @@ -666,7 +684,7 @@ export class Mjolnir { return this.protections.get(protectionName) ?? null; } - public async watchList(roomRef: string): Promise { + public async watchList(roomRef: string): Promise { const joinedRooms = await this.client.getJoinedRooms(); const permalink = Permalinks.parseUrl(roomRef); if (!permalink.roomIdOrAlias) return null; @@ -676,38 +694,38 @@ export class Mjolnir { await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers); } - if (this.banLists.find(b => b.roomId === roomId)) return null; + if (this.policyLists.find(b => b.roomId === roomId)) return null; - const list = await this.addBanList(roomId, roomRef); + const list = await this.addPolicyList(roomId, roomRef); await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, { - references: this.banLists.map(b => b.roomRef), + references: this.policyLists.map(b => b.roomRef), }); - await this.warnAboutUnprotectedBanListRoom(roomId); + await this.warnAboutUnprotectedPolicyListRoom(roomId); return list; } - public async unwatchList(roomRef: string): Promise { + public async unwatchList(roomRef: string): Promise { const permalink = Permalinks.parseUrl(roomRef); if (!permalink.roomIdOrAlias) return null; const roomId = await this.client.resolveRoom(permalink.roomIdOrAlias); - const list = this.banLists.find(b => b.roomId === roomId) || null; + const list = this.policyLists.find(b => b.roomId === roomId) || null; if (list) { - this.banLists.splice(this.banLists.indexOf(list), 1); + this.policyLists.splice(this.policyLists.indexOf(list), 1); this.ruleServer?.unwatch(list); } await this.client.setAccountData(WATCHED_LISTS_EVENT_TYPE, { - references: this.banLists.map(b => b.roomRef), + references: this.policyLists.map(b => b.roomRef), }); return list; } - public async warnAboutUnprotectedBanListRoom(roomId: string) { - if (!config.protectAllJoinedRooms) return; // doesn't matter + public async warnAboutUnprotectedPolicyListRoom(roomId: string) { + if (!this.config.protectAllJoinedRooms) return; // doesn't matter if (this.explicitlyProtectedRoomIds.includes(roomId)) return; // explicitly protected const createEvent = new CreateEvent(await this.client.getRoomStateEvent(roomId, "m.room.create", "")); @@ -734,8 +752,8 @@ export class Mjolnir { } } - private async buildWatchedBanLists() { - this.banLists = []; + private async buildWatchedPolicyLists() { + this.policyLists = []; const joinedRooms = await this.client.getJoinedRooms(); let watchedListsEvent: { references?: string[] } | null = null; @@ -754,8 +772,8 @@ export class Mjolnir { await this.client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers); } - await this.warnAboutUnprotectedBanListRoom(roomId); - await this.addBanList(roomId, roomRef); + await this.warnAboutUnprotectedPolicyListRoom(roomId); + await this.addPolicyList(roomId, roomRef); } } @@ -881,15 +899,15 @@ export class Mjolnir { * @param verbose Whether to report any errors to the management room. */ public async syncLists(verbose = true) { - for (const list of this.banLists) { + for (const list of this.policyLists) { const changes = await list.updateList(); await this.printBanlistChanges(changes, list, true); } let hadErrors = false; const [aclErrors, banErrors] = await Promise.all([ - applyServerAcls(this.banLists, this.protectedRoomsByActivity(), this), - applyUserBans(this.banLists, this.protectedRoomsByActivity(), this) + applyServerAcls(this.policyLists, this.protectedRoomsByActivity(), this), + applyUserBans(this.policyLists, this.protectedRoomsByActivity(), this) ]); const redactionErrors = await this.processRedactionQueue(); hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:"); @@ -911,16 +929,16 @@ export class Mjolnir { /** * Pulls any changes to the rules that are in a policy room and updates all protected rooms * with those changes. Does not fail if there are errors updating the room, these are reported to the management room. - * @param banList The `BanList` which we will check for changes and apply them to all protected rooms. + * @param policyList The `PolicyList` which we will check for changes and apply them to all protected rooms. * @returns When all of the protected rooms have been updated. */ - private async syncWithBanList(banList: BanList): Promise { - const changes = await banList.updateList(); + private async syncWithPolicyList(policyList: PolicyList): Promise { + const changes = await policyList.updateList(); let hadErrors = false; const [aclErrors, banErrors] = await Promise.all([ - applyServerAcls(this.banLists, this.protectedRoomsByActivity(), this), - applyUserBans(this.banLists, this.protectedRoomsByActivity(), this) + applyServerAcls(this.policyLists, this.protectedRoomsByActivity(), this), + applyUserBans(this.policyLists, this.protectedRoomsByActivity(), this) ]); const redactionErrors = await this.processRedactionQueue(); hadErrors = hadErrors || await this.printActionResult(aclErrors, "Errors updating server ACLs:"); @@ -938,7 +956,7 @@ export class Mjolnir { }); } // This can fail if the change is very large and it is much less important than applying bans, so do it last. - await this.printBanlistChanges(changes, banList, true); + await this.printBanlistChanges(changes, policyList, true); } private async handleConsequence(protection: Protection, roomId: string, eventId: string, sender: string, consequence: Consequence) { @@ -988,10 +1006,10 @@ export class Mjolnir { // Check for updated ban lists before checking protected rooms - the ban lists might be protected // themselves. - const banList = this.banLists.find(list => list.roomId === roomId); - if (banList !== undefined) { + const policyList = this.policyLists.find(list => list.roomId === roomId); + if (policyList !== undefined) { if (ALL_BAN_LIST_RULE_TYPES.includes(event['type']) || event['type'] === 'm.room.redaction') { - banList.updateForEvent(event) + policyList.updateForEvent(event) } } @@ -1036,7 +1054,7 @@ export class Mjolnir { // we cannot eagerly ban users (that is to ban them when they have never been a member) // as they can be force joined to a room they might not have known existed. // Only apply bans and then redactions in the room we are currently looking at. - const banErrors = await applyUserBans(this.banLists, [roomId], this); + const banErrors = await applyUserBans(this.policyLists, [roomId], this); const redactionErrors = await this.processRedactionQueue(roomId); await this.printActionResult(banErrors); await this.printActionResult(redactionErrors); @@ -1050,7 +1068,7 @@ export class Mjolnir { * @param ignoreSelf Whether to exclude changes that have been made by Mjolnir. * @returns true if the message was sent, false if it wasn't (because there there were no changes to report). */ - private async printBanlistChanges(changes: ListRuleChange[], list: BanList, ignoreSelf = false): Promise { + private async printBanlistChanges(changes: ListRuleChange[], list: PolicyList, ignoreSelf = false): Promise { if (ignoreSelf) { const sender = await this.client.getUserId(); changes = changes.filter(change => change.sender !== sender); diff --git a/src/actions/ApplyAcl.ts b/src/actions/ApplyAcl.ts index 41f2ee9..917be55 100644 --- a/src/actions/ApplyAcl.ts +++ b/src/actions/ApplyAcl.ts @@ -14,11 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import BanList from "../models/BanList"; +import PolicyList from "../models/PolicyList"; import { ServerAcl } from "../models/ServerAcl"; import { RoomUpdateError } from "../models/RoomUpdateError"; import { Mjolnir } from "../Mjolnir"; -import config from "../config"; import { LogLevel, UserID } from "matrix-bot-sdk"; import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; @@ -26,11 +25,21 @@ import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; * Applies the server ACLs represented by the ban lists to the provided rooms, returning the * room IDs that could not be updated and their error. * Does not update the banLists before taking their rules to build the server ACL. - * @param {BanList[]} lists The lists to construct ACLs from. + * @param {PolicyList[]} lists The lists to construct ACLs from. * @param {string[]} roomIds The room IDs to apply the ACLs in. * @param {Mjolnir} mjolnir The Mjolnir client to apply the ACLs with. */ -export async function applyServerAcls(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise { +export async function applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise { + // we need to provide mutual exclusion so that we do not have requests updating the m.room.server_acl event + // finish out of order and therefore leave the room out of sync with the policy lists. + return new Promise((resolve, reject) => { + mjolnir.aclChain = mjolnir.aclChain + .then(() => _applyServerAcls(lists, roomIds, mjolnir)) + .then(resolve, reject); + }); +} + +async function _applyServerAcls(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise { const serverName: string = new UserID(await mjolnir.client.getUserId()).domain; // Construct a server ACL first @@ -47,7 +56,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Mjölnir has detected and removed an ACL that would exclude itself. Please check the ACL lists.`); } - if (config.verboseLogging) { + if (mjolnir.config.verboseLogging) { // We specifically use sendNotice to avoid having to escape HTML await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Constructed server ACL:\n${JSON.stringify(finalAcl, null, 2)}`); } @@ -70,7 +79,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln // We specifically use sendNotice to avoid having to escape HTML await mjolnir.logMessage(LogLevel.DEBUG, "ApplyAcl", `Applying ACL in ${roomId}`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.sendStateEvent(roomId, "m.room.server_acl", "", finalAcl); } else { await mjolnir.logMessage(LogLevel.WARN, "ApplyAcl", `Tried to apply ACL in ${roomId} but Mjolnir is running in no-op mode`, roomId); @@ -78,7 +87,7 @@ export async function applyServerAcls(lists: BanList[], roomIds: string[], mjoln } catch (e) { const message = e.message || (e.body ? e.body.error : ''); const kind = message && message.includes("You don't have permission to post that to the room") ? ERROR_KIND_PERMISSION : ERROR_KIND_FATAL; - errors.push({roomId, errorMessage: message, errorKind: kind}); + errors.push({ roomId, errorMessage: message, errorKind: kind }); } } diff --git a/src/actions/ApplyBan.ts b/src/actions/ApplyBan.ts index b735a1b..e842cc6 100644 --- a/src/actions/ApplyBan.ts +++ b/src/actions/ApplyBan.ts @@ -14,21 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import BanList from "../models/BanList"; +import PolicyList from "../models/PolicyList"; import { RoomUpdateError } from "../models/RoomUpdateError"; import { Mjolnir } from "../Mjolnir"; -import config from "../config"; import { LogLevel } from "matrix-bot-sdk"; import { ERROR_KIND_FATAL, ERROR_KIND_PERMISSION } from "../ErrorCache"; /** * Applies the member bans represented by the ban lists to the provided rooms, returning the * room IDs that could not be updated and their error. - * @param {BanList[]} lists The lists to determine bans from. + * @param {PolicyList[]} lists The lists to determine bans from. * @param {string[]} roomIds The room IDs to apply the bans in. * @param {Mjolnir} mjolnir The Mjolnir client to apply the bans with. */ -export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir: Mjolnir): Promise { +export async function applyUserBans(lists: PolicyList[], roomIds: string[], mjolnir: Mjolnir): Promise { // We can only ban people who are not already banned, and who match the rules. const errors: RoomUpdateError[] = []; for (const roomId of roomIds) { @@ -38,15 +37,15 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir let members: { userId: string, membership: string }[]; - if (config.fasterMembershipChecks) { + if (mjolnir.config.fasterMembershipChecks) { const memberIds = await mjolnir.client.getJoinedRoomMembers(roomId); members = memberIds.map(u => { - return {userId: u, membership: "join"}; + return { userId: u, membership: "join" }; }); } else { const state = await mjolnir.client.getRoomState(roomId); members = state.filter(s => s['type'] === 'm.room.member' && !!s['state_key']).map(s => { - return {userId: s['state_key'], membership: s['content'] ? s['content']['membership'] : 'leave'}; + return { userId: s['state_key'], membership: s['content'] ? s['content']['membership'] : 'leave' }; }); } @@ -64,7 +63,7 @@ export async function applyUserBans(lists: BanList[], roomIds: string[], mjolnir // We specifically use sendNotice to avoid having to escape HTML await mjolnir.logMessage(LogLevel.INFO, "ApplyBan", `Banning ${member.userId} in ${roomId} for: ${userRule.reason}`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.banUser(member.userId, roomId, userRule.reason); if (mjolnir.automaticRedactGlobs.find(g => g.test(userRule.reason.toLowerCase()))) { mjolnir.queueRedactUserMessagesIn(member.userId, roomId); diff --git a/src/commands/CommandHandler.ts b/src/commands/CommandHandler.ts index 4d96780..3fad2cc 100644 --- a/src/commands/CommandHandler.ts +++ b/src/commands/CommandHandler.ts @@ -28,8 +28,10 @@ import { execRedactCommand } from "./RedactCommand"; import { execImportCommand } from "./ImportCommand"; import { execSetDefaultListCommand } from "./SetDefaultBanListCommand"; import { execDeactivateCommand } from "./DeactivateCommand"; -import { execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection, - execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection } from "./ProtectionsCommands"; +import { + execDisableProtection, execEnableProtection, execListProtections, execConfigGetProtection, + execConfigSetProtection, execConfigAddProtection, execConfigRemoveProtection +} from "./ProtectionsCommands"; import { execListProtectedRooms } from "./ListProtectedRoomsCommand"; import { execAddProtectedRoom, execRemoveProtectedRoom } from "./AddRemoveProtectedRoomsCommand"; import { execAddRoomToDirectoryCommand, execRemoveRoomFromDirectoryCommand } from "./AddRemoveRoomFromDirectoryCommand"; diff --git a/src/commands/CreateBanListCommand.ts b/src/commands/CreateBanListCommand.ts index 68f0786..3462886 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/BanList"; +import { SHORTCODE_EVENT_TYPE } from "../models/PolicyList"; import { Permalinks, RichReply } from "matrix-bot-sdk"; // !mjolnir list create @@ -48,7 +48,7 @@ export async function execCreateListCommand(roomId: string, event: any, mjolnir: preset: "public_chat", room_alias_name: aliasLocalpart, invite: [event['sender']], - initial_state: [{type: SHORTCODE_EVENT_TYPE, state_key: "", content: {shortcode: shortcode}}], + initial_state: [{ type: SHORTCODE_EVENT_TYPE, state_key: "", content: { shortcode: shortcode } }], power_level_content_override: powerLevels, }); diff --git a/src/commands/DumpRulesCommand.ts b/src/commands/DumpRulesCommand.ts index 2c6c3d7..ca92e79 100644 --- a/src/commands/DumpRulesCommand.ts +++ b/src/commands/DumpRulesCommand.ts @@ -16,7 +16,7 @@ limitations under the License. import { RichReply } from "matrix-bot-sdk"; import { Mjolnir } from "../Mjolnir"; -import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../models/BanList"; +import { EntityType } from "../models/ListRule"; import { htmlEscape } from "../utils"; /** @@ -33,7 +33,7 @@ export async function execRulesMatchingCommand(roomId: string, event: any, mjoln let html = ""; let text = ""; for (const list of mjolnir.lists) { - const matches = list.rulesMatchingEntity(entity) + const matches = list.rulesMatchingEntity(entity) if (matches.length === 0) { continue; @@ -48,12 +48,16 @@ export async function execRulesMatchingCommand(roomId: string, event: any, mjoln for (const rule of matches) { // If we know the rule kind, we will give it a readable name, otherwise just use its name. let ruleKind: string = rule.kind; - if (ruleKind === RULE_USER) { - ruleKind = 'user'; - } else if (ruleKind === RULE_SERVER) { - ruleKind = 'server'; - } else if (ruleKind === RULE_ROOM) { - ruleKind = 'room'; + switch (ruleKind) { + case EntityType.RULE_USER: + ruleKind = 'user'; + break; + case EntityType.RULE_SERVER: + ruleKind = 'server'; + break; + case EntityType.RULE_ROOM: + ruleKind = 'room'; + break; } html += `
  • ${htmlEscape(ruleKind)} (${htmlEscape(rule.recommendation ?? "")}): ${htmlEscape(rule.entity)} (${htmlEscape(rule.reason)})
  • `; text += `* ${ruleKind} (${rule.recommendation}): ${rule.entity} (${rule.reason})\n`; diff --git a/src/commands/ImportCommand.ts b/src/commands/ImportCommand.ts index 077eb3f..8082efe 100644 --- a/src/commands/ImportCommand.ts +++ b/src/commands/ImportCommand.ts @@ -16,8 +16,7 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; -import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule"; -import { RULE_SERVER, RULE_USER, ruleTypeToStable } from "../models/BanList"; +import { EntityType, Recommendation } from "../models/ListRule"; // !mjolnir import export async function execImportCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { @@ -45,14 +44,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding user ${stateEvent['state_key']} to ban list`); - const recommendation = recommendationToStable(RECOMMENDATION_BAN); const ruleContent = { entity: stateEvent['state_key'], - recommendation, + recommendation: Recommendation.Ban, reason: reason, }; const stateKey = `rule:${ruleContent.entity}`; - let stableRule = ruleTypeToStable(RULE_USER); + let stableRule = EntityType.RULE_USER; if (stableRule) { await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent); } @@ -66,14 +64,13 @@ export async function execImportCommand(roomId: string, event: any, mjolnir: Mjo await mjolnir.client.sendNotice(mjolnir.managementRoomId, `Adding server ${server} to ban list`); - const recommendation = recommendationToStable(RECOMMENDATION_BAN); const ruleContent = { entity: server, - recommendation, + recommendation: Recommendation.Ban, reason: reason, }; const stateKey = `rule:${ruleContent.entity}`; - let stableRule = ruleTypeToStable(RULE_SERVER); + let stableRule = EntityType.RULE_SERVER; if (stableRule) { await mjolnir.client.sendStateEvent(list.roomId, stableRule, stateKey, ruleContent); } diff --git a/src/commands/KickCommand.ts b/src/commands/KickCommand.ts index f841ef4..dde837d 100644 --- a/src/commands/KickCommand.ts +++ b/src/commands/KickCommand.ts @@ -16,7 +16,6 @@ limitations under the License. import { Mjolnir } from "../Mjolnir"; import { LogLevel, MatrixGlob, RichReply } from "matrix-bot-sdk"; -import config from "../config"; // !mjolnir kick [room] [reason] export async function execKickCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { @@ -30,7 +29,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln parts.pop(); } - if (config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) { + if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(glob) && !force) { let replyMessage = "Wildcard bans require an addition `--force` argument to confirm"; const reply = RichReply.createFor(roomId, event, replyMessage, replyMessage); reply["msgtype"] = "m.notice"; @@ -60,7 +59,7 @@ export async function execKickCommand(roomId: string, event: any, mjolnir: Mjoln if (kickRule.test(victim)) { await mjolnir.logMessage(LogLevel.DEBUG, "KickCommand", `Removing ${victim} in ${protectedRoomId}`, protectedRoomId); - if (!config.noop) { + if (!mjolnir.config.noop) { try { await mjolnir.taskQueue.push(async () => { return mjolnir.client.kickUser(victim, protectedRoomId, reason); diff --git a/src/commands/MakeRoomAdminCommand.ts b/src/commands/MakeRoomAdminCommand.ts index 5165dd3..ea748cf 100644 --- a/src/commands/MakeRoomAdminCommand.ts +++ b/src/commands/MakeRoomAdminCommand.ts @@ -14,14 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "../config"; import { Mjolnir } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; // !mjolnir make admin [] export async function execMakeRoomAdminCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { const isAdmin = await mjolnir.isSynapseAdmin(); - if (!config.admin?.enableMakeRoomAdminCommand || !isAdmin) { + if (!mjolnir.config.admin?.enableMakeRoomAdminCommand || !isAdmin) { const message = "Either the command is disabled or I am not running as homeserver administrator."; const reply = RichReply.createFor(roomId, event, message, message); reply['msgtype'] = "m.notice"; diff --git a/src/commands/SetDefaultBanListCommand.ts b/src/commands/SetDefaultBanListCommand.ts index f4927f1..2967c91 100644 --- a/src/commands/SetDefaultBanListCommand.ts +++ b/src/commands/SetDefaultBanListCommand.ts @@ -31,6 +31,6 @@ export async function execSetDefaultListCommand(roomId: string, event: any, mjol return; } - await mjolnir.client.setAccountData(DEFAULT_LIST_EVENT_TYPE, {shortcode}); + await mjolnir.client.setAccountData(DEFAULT_LIST_EVENT_TYPE, { shortcode }); await mjolnir.client.unstableApis.addReactionToEvent(roomId, event['event_id'], '✅'); } diff --git a/src/commands/UnbanBanCommand.ts b/src/commands/UnbanBanCommand.ts index cfac205..3ea6ee0 100644 --- a/src/commands/UnbanBanCommand.ts +++ b/src/commands/UnbanBanCommand.ts @@ -15,21 +15,20 @@ limitations under the License. */ import { Mjolnir } from "../Mjolnir"; -import BanList, { RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/BanList"; +import PolicyList from "../models/PolicyList"; import { extractRequestError, LogLevel, LogService, MatrixGlob, RichReply } from "matrix-bot-sdk"; -import { RECOMMENDATION_BAN, recommendationToStable } from "../models/ListRule"; -import config from "../config"; +import { Recommendation, RULE_ROOM, RULE_SERVER, RULE_USER, USER_RULE_TYPES } from "../models/ListRule"; import { DEFAULT_LIST_EVENT_TYPE } from "./SetDefaultBanListCommand"; interface Arguments { - list: BanList | null; + list: PolicyList | null; entity: string; ruleType: string | null; reason: string; } // Exported for tests -export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise { +export async function parseArguments(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]): Promise { let defaultShortcode: string | null = null; try { const data: { shortcode: string } = await mjolnir.client.getAccountData(DEFAULT_LIST_EVENT_TYPE); @@ -44,7 +43,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni let argumentIndex = 2; let ruleType: string | null = null; let entity: string | null = null; - let list: BanList | null = null; + let list: PolicyList | null = null; let force = false; while (argumentIndex < 7 && argumentIndex < parts.length) { const arg = parts[argumentIndex++]; @@ -95,7 +94,7 @@ export async function parseArguments(roomId: string, event: any, mjolnir: Mjolni else if (!ruleType) replyMessage = "Please specify the type as either 'user', 'room', or 'server'"; else if (!entity) replyMessage = "No entity found"; - if (config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) { + if (mjolnir.config.commands.confirmWildcardBan && /[*?]/.test(entity) && !force) { replyMessage = "Wildcard bans require an additional `--force` argument to confirm"; } @@ -119,10 +118,9 @@ export async function execBanCommand(roomId: string, event: any, mjolnir: Mjolni const bits = await parseArguments(roomId, event, mjolnir, parts); if (!bits) return; // error already handled - const recommendation = recommendationToStable(RECOMMENDATION_BAN); const ruleContent = { entity: bits.entity, - recommendation, + recommendation: Recommendation.Ban, reason: bits.reason || '', }; const stateKey = `rule:${bits.entity}`; @@ -151,7 +149,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol if (rule.test(victim)) { await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Unbanning ${victim} in ${protectedRoomId}`, protectedRoomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.unbanUser(victim, protectedRoomId); } else { await mjolnir.logMessage(LogLevel.WARN, "UnbanBanCommand", `Attempted to unban ${victim} in ${protectedRoomId} but Mjolnir is running in no-op mode`, protectedRoomId); @@ -164,7 +162,7 @@ export async function execUnbanCommand(roomId: string, event: any, mjolnir: Mjol if (unbannedSomeone) { await mjolnir.logMessage(LogLevel.DEBUG, "UnbanBanCommand", `Syncing lists to ensure no users were accidentally unbanned`); - await mjolnir.syncLists(config.verboseLogging); + await mjolnir.syncLists(mjolnir.config.verboseLogging); } } diff --git a/src/config.ts b/src/config.ts index c286f51..3d6cd3b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as config from "config"; +import * as fs from "fs"; +import { load } from "js-yaml"; import { MatrixClient } from "matrix-bot-sdk"; /** @@ -25,7 +26,7 @@ import { MatrixClient } from "matrix-bot-sdk"; // The object is magically generated by external lib `config` // from the file specified by `NODE_ENV`, e.g. production.yaml // or harness.yaml. -interface IConfig { +export interface IConfig { homeserverUrl: string; rawHomeserverUrl: string; accessToken: string; @@ -35,7 +36,7 @@ interface IConfig { password: string; }; dataPath: string; - acceptInvitesFromGroup: string; + acceptInvitesFromSpace: string; autojoinOnlyIfManager: boolean; recordIgnoredInvites: boolean; managementRoom: string; @@ -114,7 +115,7 @@ const defaultConfig: IConfig = { password: "", }, dataPath: "/data/storage", - acceptInvitesFromGroup: '+example:example.org', + acceptInvitesFromSpace: '!noop:example.org', autojoinOnlyIfManager: false, recordIgnoredInvites: false, managementRoom: "!noop:example.org", @@ -168,5 +169,9 @@ const defaultConfig: IConfig = { }, }; -const finalConfig = Object.assign({}, defaultConfig, config); -export default finalConfig; +export function read(): IConfig { + const content = fs.readFileSync(`./config/${process.env.NODE_ENV || 'default'}.yaml`, "utf8"); + const parsed = load(content); + const config = {...defaultConfig, ...(parsed as object)} as IConfig; + return config; +} diff --git a/src/health/healthz.ts b/src/health/healthz.ts index 7f00309..45fea26 100644 --- a/src/health/healthz.ts +++ b/src/health/healthz.ts @@ -14,28 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "../config"; import * as http from "http"; import { LogService } from "matrix-bot-sdk"; +import { IConfig } from "../config"; +// allowed to use the global configuration since this is only intended to be used by `src/index.ts`. export class Healthz { - private static healthCode: number; + private healthCode: number; - public static set isHealthy(val: boolean) { - Healthz.healthCode = val ? config.health.healthz.healthyStatus : config.health.healthz.unhealthyStatus; + constructor(private config: IConfig) { } + + public set isHealthy(val: boolean) { + this.healthCode = val ? this.config.health.healthz.healthyStatus : this.config.health.healthz.unhealthyStatus; } - public static get isHealthy(): boolean { - return Healthz.healthCode === config.health.healthz.healthyStatus; + public get isHealthy(): boolean { + return this.healthCode === this.config.health.healthz.healthyStatus; } - public static listen() { + public listen() { const server = http.createServer((req, res) => { - res.writeHead(Healthz.healthCode); - res.end(`health code: ${Healthz.healthCode}`); + res.writeHead(this.healthCode); + res.end(`health code: ${this.healthCode}`); }); - server.listen(config.health.healthz.port, config.health.healthz.address, () => { - LogService.info("Healthz", `Listening for health requests on ${config.health.healthz.address}:${config.health.healthz.port}`); + server.listen(this.config.health.healthz.port, this.config.health.healthz.address, () => { + LogService.info("Healthz", `Listening for health requests on ${this.config.health.healthz.address}:${this.config.health.healthz.port}`); }); } } diff --git a/src/index.ts b/src/index.ts index 0ed071f..38e72bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,24 +23,28 @@ import { RichConsoleLogger, SimpleFsStorageProvider } from "matrix-bot-sdk"; -import config from "./config"; +import { read as configRead } from "./config"; import { Healthz } from "./health/healthz"; import { Mjolnir } from "./Mjolnir"; import { patchMatrixClient } from "./utils"; -config.RUNTIME = {}; - -LogService.setLogger(new RichConsoleLogger()); -LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); - -LogService.info("index", "Starting bot..."); - -Healthz.isHealthy = false; // start off unhealthy -if (config.health.healthz.enabled) { - Healthz.listen(); -} (async function () { + const config = configRead(); + + config.RUNTIME = {}; + + LogService.setLogger(new RichConsoleLogger()); + LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); + + LogService.info("index", "Starting bot..."); + + const healthz = new Healthz(config); + healthz.isHealthy = false; // start off unhealthy + if (config.health.healthz.enabled) { + healthz.listen(); + } + let bot: Mjolnir | null = null; try { const storagePath = path.isAbsolute(config.dataPath) ? config.dataPath : path.join(__dirname, '../', config.dataPath); @@ -56,13 +60,14 @@ if (config.health.healthz.enabled) { patchMatrixClient(); config.RUNTIME.client = client; - bot = await Mjolnir.setupMjolnirFromConfig(client); + bot = await Mjolnir.setupMjolnirFromConfig(client, config); } catch (err) { console.error(`Failed to setup mjolnir from the config ${config.dataPath}: ${err}`); throw err; } try { await bot.start(); + healthz.isHealthy = true; } catch (err) { console.error(`Mjolnir failed to start: ${err}`); throw err; diff --git a/src/models/ListRule.ts b/src/models/ListRule.ts index 1653abd..f992722 100644 --- a/src/models/ListRule.ts +++ b/src/models/ListRule.ts @@ -16,32 +16,238 @@ limitations under the License. import { MatrixGlob } from "matrix-bot-sdk"; -export const RECOMMENDATION_BAN = "m.ban"; -export const RECOMMENDATION_BAN_TYPES = [RECOMMENDATION_BAN, "org.matrix.mjolnir.ban"]; +export enum EntityType { + /// `entity` is to be parsed as a glob of users IDs + RULE_USER = "m.policy.rule.user", -export function recommendationToStable(recommendation: string, unstable = false): string|null { - if (RECOMMENDATION_BAN_TYPES.includes(recommendation)) return unstable ? RECOMMENDATION_BAN_TYPES[RECOMMENDATION_BAN_TYPES.length - 1] : RECOMMENDATION_BAN; - return null; + /// `entity` is to be parsed as a glob of room IDs/aliases + RULE_ROOM = "m.policy.rule.room", + + /// `entity` is to be parsed as a glob of server names + RULE_SERVER = "m.policy.rule.server", } -export class ListRule { +export const RULE_USER = EntityType.RULE_USER; +export const RULE_ROOM = EntityType.RULE_ROOM; +export const RULE_SERVER = EntityType.RULE_SERVER; +// README! The order here matters for determining whether a type is obsolete, most recent should be first. +// These are the current and historical types for each type of rule which were used while MSC2313 was being developed +// and were left as an artifact for some time afterwards. +// Most rules (as of writing) will have the prefix `m.room.rule.*` as this has been in use for roughly 2 years. +export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"]; +export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjolnir.rule.room"]; +export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"]; +export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; + +export enum Recommendation { + /// The rule recommends a "ban". + /// + /// The actual semantics for this "ban" may vary, e.g. room ban, + /// server ban, ignore user, etc. To determine the semantics for + /// this "ban", clients need to take into account the context for + /// the list, e.g. how the rule was imported. + Ban = "m.ban", + + /// The rule specifies an "opinion", as a number in [-100, +100], + /// where -100 represents a user who is considered absolutely toxic + /// by whoever issued this ListRule and +100 represents a user who + /// is considered absolutely absolutely perfect by whoever issued + /// this ListRule. + Opinion = "org.matrix.msc3845.opinion", +} + +/** + * All variants of recommendation `m.ban` + */ +const RECOMMENDATION_BAN_VARIANTS = [ + // Stable + Recommendation.Ban, + // Unstable prefix, for compatibility. + "org.matrix.mjolnir.ban" +]; + +/** + * All variants of recommendation `m.opinion` + */ +const RECOMMENDATION_OPINION_VARIANTS: string[] = [ + // Unstable + Recommendation.Opinion +]; + +export const OPINION_MIN = -100; +export const OPINION_MAX = +100; + +/** + * Representation of a rule within a Policy List. + */ +export abstract class ListRule { + /** + * A glob for `entity`. + */ private glob: MatrixGlob; - - constructor(public readonly entity: string, private action: string, public readonly reason: string, public readonly kind: string) { + constructor( + /** + * The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain. + */ + public readonly entity: string, + /** + * A human-readable reason for this rule, for audit purposes. + */ + public readonly reason: string, + /** + * The type of entity for this rule, e.g. user, server domain, etc. + */ + public readonly kind: EntityType, + /** + * The recommendation for this rule, e.g. "ban" or "opinion", or `null` + * if the recommendation is one that Mjölnir doesn't understand. + */ + public readonly recommendation: Recommendation | null) { this.glob = new MatrixGlob(entity); } /** - * The recommendation for this rule, or `null` if there is no recommendation or the recommendation is invalid. - * Recommendations are normalised to their stable types. + * Determine whether this rule should apply to a given entity. */ - public get recommendation(): string|null { - if (RECOMMENDATION_BAN_TYPES.includes(this.action)) return RECOMMENDATION_BAN; - return null; - } - public isMatch(entity: string): boolean { return this.glob.test(entity); } + + /** + * Validate and parse an event into a ListRule. + * + * @param event An *untrusted* event. + * @returns null if the ListRule is invalid or not recognized by Mjölnir. + */ + public static parse(event: {type: string, content: any}): ListRule | null { + // Parse common fields. + // If a field is ill-formed, discard the rule. + const content = event['content']; + if (!content || typeof content !== "object") { + return null; + } + const entity = content['entity']; + if (!entity || typeof entity !== "string") { + return null; + } + const recommendation = content['recommendation']; + if (!recommendation || typeof recommendation !== "string") { + return null; + } + + const reason = content['reason'] || ''; + if (typeof reason !== "string") { + return null; + } + + let type = event['type']; + let kind; + if (USER_RULE_TYPES.includes(type)) { + kind = EntityType.RULE_USER; + } else if (ROOM_RULE_TYPES.includes(type)) { + kind = EntityType.RULE_ROOM; + } else if (SERVER_RULE_TYPES.includes(type)) { + kind = EntityType.RULE_SERVER; + } else { + return null; + } + + // From this point, we may need specific fields. + if (RECOMMENDATION_BAN_VARIANTS.includes(recommendation)) { + return new ListRuleBan(entity, reason, kind); + } else if (RECOMMENDATION_OPINION_VARIANTS.includes(recommendation)) { + let opinion = content['opinion']; + if (!Number.isInteger(opinion)) { + return null; + } + return new ListRuleOpinion(entity, reason, kind, opinion); + } else { + // As long as the `recommendation` is defined, we assume + // that the rule is correct, just unknown. + return new ListRuleUnknown(entity, reason, kind, content); + } + } +} + +/** + * A rule representing a "ban". + */ +export class ListRuleBan extends ListRule { + constructor( + /** + * The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain. + */ + entity: string, + /** + * A human-readable reason for this rule, for audit purposes. + */ + reason: string, + /** + * The type of entity for this rule, e.g. user, server domain, etc. + */ + kind: EntityType, + ) { + super(entity, reason, kind, Recommendation.Ban) + } +} + +/** + * A rule representing an "opinion" + */ +export class ListRuleOpinion extends ListRule { + constructor( + /** + * The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain. + */ + entity: string, + /** + * A human-readable reason for this rule, for audit purposes. + */ + reason: string, + /** + * The type of entity for this rule, e.g. user, server domain, etc. + */ + kind: EntityType, + /** + * A number in [-100, +100] where -100 represents the worst possible opinion + * on the entity (e.g. toxic user or community) and +100 represents the best + * possible opinion on the entity (e.g. pillar of the community). + */ + public readonly opinion: number + ) { + super(entity, reason, kind, Recommendation.Opinion); + if (!Number.isInteger(opinion)) { + throw new TypeError(`The opinion must be an integer, got ${opinion}`); + } + if (opinion < OPINION_MIN || opinion > OPINION_MAX) { + throw new TypeError(`The opinion must be within [-100, +100], got ${opinion}`); + } + } +} + +/** + * Any list rule that we do not understand. + */ +export class ListRuleUnknown extends ListRule { + constructor( + /** + * The entity covered by this rule, e.g. a glob user ID, a room ID, a server domain. + */ + entity: string, + /** + * A human-readable reason for this rule, for audit purposes. + */ + reason: string, + /** + * The type of entity for this rule, e.g. user, server domain, etc. + */ + kind: EntityType, + /** + * The event used to create the rule. + */ + public readonly content: any, + ) { + super(entity, reason, kind, null); + } } diff --git a/src/models/BanList.ts b/src/models/PolicyList.ts similarity index 79% rename from src/models/BanList.ts rename to src/models/PolicyList.ts index 3b56f7f..b462893 100644 --- a/src/models/BanList.ts +++ b/src/models/PolicyList.ts @@ -16,33 +16,13 @@ limitations under the License. import { extractRequestError, LogService, MatrixClient, UserID } from "matrix-bot-sdk"; import { EventEmitter } from "events"; -import { ListRule, RECOMMENDATION_BAN } from "./ListRule"; - -export const RULE_USER = "m.policy.rule.user"; -export const RULE_ROOM = "m.policy.rule.room"; -export const RULE_SERVER = "m.policy.rule.server"; - -// README! The order here matters for determining whether a type is obsolete, most recent should be first. -// These are the current and historical types for each type of rule which were used while MSC2313 was being developed -// and were left as an artifact for some time afterwards. -// Most rules (as of writing) will have the prefix `m.room.rule.*` as this has been in use for roughly 2 years. -export const USER_RULE_TYPES = [RULE_USER, "m.room.rule.user", "org.matrix.mjolnir.rule.user"]; -export const ROOM_RULE_TYPES = [RULE_ROOM, "m.room.rule.room", "org.matrix.mjolnir.rule.room"]; -export const SERVER_RULE_TYPES = [RULE_SERVER, "m.room.rule.server", "org.matrix.mjolnir.rule.server"]; -export const ALL_RULE_TYPES = [...USER_RULE_TYPES, ...ROOM_RULE_TYPES, ...SERVER_RULE_TYPES]; +import { ALL_RULE_TYPES, EntityType, ListRule, Recommendation, ROOM_RULE_TYPES, RULE_ROOM, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./ListRule"; export const SHORTCODE_EVENT_TYPE = "org.matrix.mjolnir.shortcode"; -export function ruleTypeToStable(rule: string, unstable = true): string|null { - if (USER_RULE_TYPES.includes(rule)) return unstable ? USER_RULE_TYPES[USER_RULE_TYPES.length - 1] : RULE_USER; - if (ROOM_RULE_TYPES.includes(rule)) return unstable ? ROOM_RULE_TYPES[ROOM_RULE_TYPES.length - 1] : RULE_ROOM; - if (SERVER_RULE_TYPES.includes(rule)) return unstable ? SERVER_RULE_TYPES[SERVER_RULE_TYPES.length - 1] : RULE_SERVER; - return null; -} - export enum ChangeType { - Added = "ADDED", - Removed = "REMOVED", + Added = "ADDED", + Removed = "REMOVED", Modified = "MODIFIED" } @@ -71,28 +51,28 @@ export interface ListRuleChange { readonly previousState?: any, } -declare interface BanList { - // BanList.update is emitted when the BanList has pulled new rules from Matrix and informs listeners of any changes. - on(event: 'BanList.update', listener: (list: BanList, changes: ListRuleChange[]) => void): this - emit(event: 'BanList.update', list: BanList, changes: ListRuleChange[]): boolean - // BanList.batch is emitted when the BanList has created a batch from the events provided by `updateForEvent`. - on(event: 'BanList.batch', listener: (list: BanList) => void): this - emit(event: 'BanList.batch', list: BanList): boolean +declare interface PolicyList { + // PolicyList.update is emitted when the PolicyList has pulled new rules from Matrix and informs listeners of any changes. + on(event: 'PolicyList.update', listener: (list: PolicyList, changes: ListRuleChange[]) => void): this + emit(event: 'PolicyList.update', list: PolicyList, changes: ListRuleChange[]): boolean + // PolicyList.batch is emitted when the PolicyList has created a batch from the events provided by `updateForEvent`. + on(event: 'PolicyList.batch', listener: (list: PolicyList) => void): this + emit(event: 'PolicyList.batch', list: PolicyList): boolean } /** - * The BanList caches all of the rules that are active in a policy room so Mjolnir can refer to when applying bans etc. + * The PolicyList caches all of the rules that are active in a policy room so Mjolnir can refer to when applying bans etc. * This cannot be used to update events in the modeled room, it is a readonly model of the policy room. */ -class BanList extends EventEmitter { - private shortcode: string|null = null; +class PolicyList extends EventEmitter { + private shortcode: string | null = null; // A map of state events indexed first by state type and then state keys. private state: Map> = new Map(); // Batches new events from sync together before starting the process to update the list. private readonly batcher: UpdateBatcher; /** - * Construct a BanList, does not synchronize with the room. + * Construct a PolicyList, does not synchronize with the room. * @param roomId The id of the policy room, i.e. a room containing MSC2313 policies. * @param roomRef A sharable/clickable matrix URL that refers to the room. * @param client A matrix client that is used to read the state of the room when `updateList` is called. @@ -120,7 +100,7 @@ class BanList extends EventEmitter { } /** - * Store this state event as part of the active room state for this BanList (used to cache rules). + * Store this state event as part of the active room state for this PolicyList (used to cache rules). * The state type should be normalised if it is obsolete e.g. m.room.rule.user should be stored as m.policy.rule.user. * @param stateType The event type e.g. m.room.policy.user. * @param stateKey The state key e.g. rule:@bad:matrix.org @@ -137,7 +117,7 @@ class BanList extends EventEmitter { /** * Return all the active rules of a given kind. - * @param kind e.g. RULE_SERVER (m.policy.rule.server). Rule types are always normalised when they are interned into the BanList. + * @param kind e.g. RULE_SERVER (m.policy.rule.server). Rule types are always normalised when they are interned into the PolicyList. * @returns The active ListRules for the ban list of that kind. */ private rulesOfKind(kind: string): ListRule[] { @@ -149,7 +129,7 @@ class BanList extends EventEmitter { // README! If you are refactoring this and/or introducing a mechanism to return the list of rules, // please make sure that you *only* return rules with `m.ban` or create a different method // (we don't want to accidentally ban entities). - if (rule && rule.kind === kind && rule.recommendation === RECOMMENDATION_BAN) { + if (rule && rule.kind === kind && rule.recommendation === Recommendation.Ban) { rules.push(rule); } } @@ -160,8 +140,8 @@ class BanList extends EventEmitter { public set listShortcode(newShortcode: string) { const currentShortcode = this.shortcode; this.shortcode = newShortcode; - this.client.sendStateEvent(this.roomId, SHORTCODE_EVENT_TYPE, '', {shortcode: this.shortcode}).catch(err => { - LogService.error("BanList", extractRequestError(err)); + this.client.sendStateEvent(this.roomId, SHORTCODE_EVENT_TYPE, '', { shortcode: this.shortcode }).catch(err => { + LogService.error("PolicyList", extractRequestError(err)); if (this.shortcode === newShortcode) this.shortcode = currentShortcode; }); } @@ -267,7 +247,7 @@ class BanList extends EventEmitter { continue; } - let kind: string|null = null; + let kind: EntityType | null = null; if (USER_RULE_TYPES.includes(event['type'])) { kind = RULE_USER; } else if (ROOM_RULE_TYPES.includes(event['type'])) { @@ -286,7 +266,7 @@ class BanList extends EventEmitter { // as it may be someone deleting the older versions of the rules. if (previousState) { const logObsoleteRule = () => { - LogService.info('BanList', `In BanList ${this.roomRef}, conflict between rules ${event['event_id']} (with obsolete type ${event['type']}) ` + + LogService.info('PolicyList', `In PolicyList ${this.roomRef}, conflict between rules ${event['event_id']} (with obsolete type ${event['type']}) ` + `and ${previousState['event_id']} (with standard type ${previousState['type']}). Ignoring rule with obsolete type.`); } if (kind === RULE_USER && USER_RULE_TYPES.indexOf(event['type']) > USER_RULE_TYPES.indexOf(previousState['type'])) { @@ -305,7 +285,7 @@ class BanList extends EventEmitter { // in order to mark a rule as deleted. // We always set state with the normalised state type via `kind` to de-duplicate rules. this.setState(kind, event['state_key'], event); - const changeType: null|ChangeType = (() => { + const changeType: null | ChangeType = (() => { if (!previousState) { return ChangeType.Added; } else if (previousState['event_id'] === event['event_id']) { @@ -329,56 +309,52 @@ class BanList extends EventEmitter { // and so will not have been used. Removing a rule like this therefore results in no change. if (changeType === ChangeType.Removed && previousState?.unsigned?.rule) { const sender = event.unsigned['redacted_because'] ? event.unsigned['redacted_because']['sender'] : event.sender; - changes.push({changeType, event, sender, rule: previousState.unsigned.rule, - ... previousState ? {previousState} : {} }); + changes.push({ + changeType, event, sender, rule: previousState.unsigned.rule, + ...previousState ? { previousState } : {} + }); // Event has no content and cannot be parsed as a ListRule. continue; } // It's a rule - parse it - const content = event['content']; - if (!content) continue; - - const entity = content['entity']; - const recommendation = content['recommendation']; - const reason = content['reason'] || ''; - - if (!entity || !recommendation) { + const rule = ListRule.parse(event); + if (!rule) { + // Invalid/unknown rule, just skip it. continue; } - const rule = new ListRule(entity, recommendation, reason, kind); event.unsigned.rule = rule; if (changeType) { - changes.push({rule, changeType, event, sender: event.sender, ... previousState ? {previousState} : {} }); + changes.push({ rule, changeType, event, sender: event.sender, ...previousState ? { previousState } : {} }); } } - this.emit('BanList.update', this, changes); + this.emit('PolicyList.update', this, changes); return changes; } /** - * Inform the `BanList` about a new event from the room it is modelling. - * @param event An event from the room the `BanList` models to inform an instance about. + * Inform the `PolicyList` about a new event from the room it is modelling. + * @param event An event from the room the `PolicyList` models to inform an instance about. */ public updateForEvent(event: { event_id: string }): void { this.batcher.addToBatch(event.event_id) } } -export default BanList; +export default PolicyList; /** - * Helper class that emits a batch event on a `BanList` when it has made a batch + * Helper class that emits a batch event on a `PolicyList` when it has made a batch * out of the events given to `addToBatch`. */ class UpdateBatcher { // Whether we are waiting for more events to form a batch. private isWaiting = false; // The latest (or most recent) event we have received. - private latestEventId: string|null = null; + private latestEventId: string | null = null; private readonly waitPeriodMS = 200; // 200ms seems good enough. private readonly maxWaitMS = 3000; // 3s is long enough to wait while batching. - constructor(private readonly banList: BanList) { + constructor(private readonly banList: PolicyList) { } @@ -402,7 +378,7 @@ class UpdateBatcher { await new Promise(resolve => setTimeout(resolve, this.waitPeriodMS)); } while ((Date.now() - start) < this.maxWaitMS && this.latestEventId !== eventId) this.reset(); - this.banList.emit('BanList.batch', this.banList); + this.banList.emit('PolicyList.batch', this.banList); } /** diff --git a/src/models/RuleServer.ts b/src/models/RuleServer.ts index ce41718..515c131 100644 --- a/src/models/RuleServer.ts +++ b/src/models/RuleServer.ts @@ -13,10 +13,11 @@ 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. */ -import BanList, { ChangeType, ListRuleChange, RULE_ROOM, RULE_SERVER, RULE_USER } from "./BanList" +import BanList, { ChangeType, ListRuleChange } from "./PolicyList" import * as crypto from "crypto"; import { LogService } from "matrix-bot-sdk"; -import { ListRule } from "./ListRule"; +import { EntityType, ListRule } from "./ListRule"; +import PolicyList from "./PolicyList"; export const USER_MAY_INVITE = 'user_may_invite'; export const CHECK_EVENT_FOR_SPAM = 'check_event_for_spam'; @@ -25,7 +26,7 @@ export const CHECK_EVENT_FOR_SPAM = 'check_event_for_spam'; * Rules in the RuleServer format that have been produced from a single event. */ class EventRules { - constructor ( + constructor( readonly eventId: string, readonly roomId: string, readonly ruleServerRules: RuleServerRule[], @@ -108,7 +109,7 @@ export default class RuleServer { * @returns The `EventRules` object describing which rules have been created based on the policy the event represents * or `undefined` if there are no `EventRules` associated with the event. */ - private getEventRules(roomId: string, eventId: string): EventRules|undefined { + private getEventRules(roomId: string, eventId: string): EventRules | undefined { return this.rulesByEvent.get(roomId)?.get(eventId); } @@ -118,7 +119,7 @@ export default class RuleServer { * @throws If there are already rules associated with the event specified in `eventRules.eventId`. */ private addEventRules(eventRules: EventRules): void { - const {roomId, eventId, token} = eventRules; + const { roomId, eventId, token } = eventRules; if (this.rulesByEvent.get(roomId)?.has(eventId)) { throw new TypeError(`There is already an entry in the RuleServer for rules created from the event ${eventId}.`); } @@ -136,7 +137,7 @@ export default class RuleServer { * @param eventRules The EventRules to stop serving from the rule server. */ private stopEventRules(eventRules: EventRules): void { - const {eventId, roomId, token} = eventRules; + const { eventId, roomId, token } = eventRules; this.rulesByEvent.get(roomId)?.delete(eventId); // We expect that each row of `rulesByEvent` list of eventRules (represented by 1 row in `rulesByEvent`) to be relatively small (1-5) // as it can only contain eventRules added during the instant of time represented by one token. @@ -156,7 +157,7 @@ export default class RuleServer { const eventRules = new EventRules(change.event.event_id, change.event.room_id, toRuleServerFormat(change.rule), this.currentToken); this.addEventRules(eventRules); } else if (change.changeType === ChangeType.Modified) { - const entry: EventRules|undefined = this.getEventRules(change.event.roomId, change.previousState.event_id); + const entry: EventRules | undefined = this.getEventRules(change.event.roomId, change.previousState.event_id); if (entry === undefined) { LogService.error('RuleServer', `Could not find the rules for the previous modified state ${change.event['state_type']} ${change.event['state_key']} ${change.previousState?.event_id}`); return; @@ -169,7 +170,7 @@ export default class RuleServer { // 2) When an event has been "soft redacted" (ie we have a new event with the same state type and state_key with no content), // the events in the `previousState` and `event` slots of `change` will be distinct events. // In either case (of redaction or "soft redaction") we can use `previousState` to get the right event id to stop. - const entry: EventRules|undefined = this.getEventRules(change.event.room_id, change.previousState.event_id); + const entry: EventRules | undefined = this.getEventRules(change.event.room_id, change.previousState.event_id); if (entry === undefined) { LogService.error('RuleServer', `Could not find the rules for the previous modified state ${change.event['state_type']} ${change.event['state_key']} ${change.previousState?.event_id}`); return; @@ -184,16 +185,16 @@ export default class RuleServer { * as we won't be able to serve rules that have already been interned in the BanList. * @param banList a BanList to watch for rule changes with. */ - public watch(banList: BanList): void { - banList.on('BanList.update', this.banListUpdateListener); + public watch(banList: PolicyList): void { + banList.on('PolicyList.update', this.banListUpdateListener); } /** * Remove all of the rules that have been created from the policies in this banList. * @param banList The BanList to unwatch. */ - public unwatch(banList: BanList): void { - banList.removeListener('BanList.update', this.banListUpdateListener); + public unwatch(banList: PolicyList): void { + banList.removeListener('PolicyList.update', this.banListUpdateListener); const listRules = this.rulesByEvent.get(banList.roomId); this.nextToken(); if (listRules) { @@ -221,8 +222,8 @@ export default class RuleServer { * @param sinceToken A token that has previously been issued by this server. * @returns An object with the rules that have been started and stopped since the token and a new token to poll for more rules with. */ - public getUpdates(sinceToken: string | null): {start: RuleServerRule[], stop: string[], reset?: boolean, since: string} { - const updatesSince = (token: number | null, policyStore: T[][]): T[] => { + public getUpdates(sinceToken: string | null): { start: RuleServerRule[], stop: string[], reset?: boolean, since: string } { + const updatesSince = (token: number | null, policyStore: T[][]): T[] => { if (token === null) { // The client is requesting for the first time, we will give them everything. return policyStore.flat(); @@ -234,7 +235,7 @@ export default class RuleServer { } } const [serverId, since] = sinceToken ? sinceToken.split('::') : [null, null]; - const parsedSince: number | null = since ? parseInt(since, 10) : null; + const parsedSince: number | null = since ? parseInt(since, 10) : null; if (serverId && serverId !== this.serverId) { // The server has restarted, but the client has not and still has rules we can no longer account for. // So we have to resend them everything. @@ -261,59 +262,59 @@ export default class RuleServer { * @returns An array of rules that can be served from the rule server. */ function toRuleServerFormat(policyRule: ListRule): RuleServerRule[] { - function makeLiteral(literal: string) { - return {literal} - } + function makeLiteral(literal: string) { + return { literal } + } - function makeGlob(glob: string) { - return {glob} - } + function makeGlob(glob: string) { + return { glob } + } - function makeServerGlob(server: string) { - return {glob: `:${server}`} - } + function makeServerGlob(server: string) { + return { glob: `:${server}` } + } - function makeRule(checks: Checks) { - return { - id: crypto.randomUUID(), - checks: checks - } - } + function makeRule(checks: Checks) { + return { + id: crypto.randomUUID(), + checks: checks + } + } - if (policyRule.kind === RULE_USER) { - // Block any messages or invites from being sent by a matching local user - // Block any messages or invitations from being received that were sent by a matching remote user. - return [{ - property: USER_MAY_INVITE, - user_id: [makeGlob(policyRule.entity)] - }, - { - property: CHECK_EVENT_FOR_SPAM, - sender: [makeGlob(policyRule.entity)] - }].map(makeRule) - } else if (policyRule.kind === RULE_ROOM) { - // Block any messages being sent or received in the room, stop invitations being sent to the room and - // stop anyone receiving invitations from the room. - return [{ - property: USER_MAY_INVITE, - 'room_id': [makeLiteral(policyRule.entity)] - }, - { - property: CHECK_EVENT_FOR_SPAM, - 'room_id': [makeLiteral(policyRule.entity)] - }].map(makeRule) - } else if (policyRule.kind === RULE_SERVER) { - // Block any invitations from the server or any new messages from the server. - return [{ - property: USER_MAY_INVITE, - user_id: [makeServerGlob(policyRule.entity)] - }, - { - property: CHECK_EVENT_FOR_SPAM, - sender: [makeServerGlob(policyRule.entity)] - }].map(makeRule) - } else { - LogService.info('RuleServer', `Ignoring unsupported policy rule type ${policyRule.kind}`); - return [] - } + if (policyRule.kind === EntityType.RULE_USER) { + // Block any messages or invites from being sent by a matching local user + // Block any messages or invitations from being received that were sent by a matching remote user. + return [{ + property: USER_MAY_INVITE, + user_id: [makeGlob(policyRule.entity)] + }, + { + property: CHECK_EVENT_FOR_SPAM, + sender: [makeGlob(policyRule.entity)] + }].map(makeRule) + } else if (policyRule.kind === EntityType.RULE_ROOM) { + // Block any messages being sent or received in the room, stop invitations being sent to the room and + // stop anyone receiving invitations from the room. + return [{ + property: USER_MAY_INVITE, + 'room_id': [makeLiteral(policyRule.entity)] + }, + { + property: CHECK_EVENT_FOR_SPAM, + 'room_id': [makeLiteral(policyRule.entity)] + }].map(makeRule) + } else if (policyRule.kind === EntityType.RULE_SERVER) { + // Block any invitations from the server or any new messages from the server. + return [{ + property: USER_MAY_INVITE, + user_id: [makeServerGlob(policyRule.entity)] + }, + { + property: CHECK_EVENT_FOR_SPAM, + sender: [makeServerGlob(policyRule.entity)] + }].map(makeRule) + } else { + LogService.info('RuleServer', `Ignoring unsupported policy rule type ${policyRule.kind}`); + return [] + } } diff --git a/src/protections/BasicFlooding.ts b/src/protections/BasicFlooding.ts index 323c739..66c82fa 100644 --- a/src/protections/BasicFlooding.ts +++ b/src/protections/BasicFlooding.ts @@ -18,7 +18,6 @@ import { Protection } from "./IProtection"; import { NumberProtectionSetting } from "./ProtectionSettings"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, LogService } from "matrix-bot-sdk"; -import config from "../config"; // if this is exceeded, we'll ban the user for spam and redact their messages export const DEFAULT_MAX_PER_MINUTE = 10; @@ -64,7 +63,7 @@ export class BasicFlooding extends Protection { if (messageCount >= this.settings.maxPerMinute.value) { await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Banning ${event['sender']} in ${roomId} for flooding (${messageCount} messages in the last minute)`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.banUser(event['sender'], roomId, "spam"); } else { await mjolnir.logMessage(LogLevel.WARN, "BasicFlooding", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); @@ -75,7 +74,7 @@ export class BasicFlooding extends Protection { this.recentlyBanned.push(event['sender']); // flag to reduce spam // Redact all the things the user said too - if (!config.noop) { + if (!mjolnir.config.noop) { for (const eventId of forUser.map(e => e.eventId)) { await mjolnir.client.redactEvent(roomId, eventId, "spam"); } diff --git a/src/protections/FirstMessageIsImage.ts b/src/protections/FirstMessageIsImage.ts index 4f3c168..a1cc588 100644 --- a/src/protections/FirstMessageIsImage.ts +++ b/src/protections/FirstMessageIsImage.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, LogService } from "matrix-bot-sdk"; -import config from "../config"; import { isTrueJoinEvent } from "../utils"; export class FirstMessageIsImage extends Protection { @@ -58,7 +57,7 @@ export class FirstMessageIsImage extends Protection { const isMedia = msgtype === 'm.image' || msgtype === 'm.video' || formattedBody.toLowerCase().includes('= this.settings.maxPer.value) { await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Setting ${roomId} to invite-only as more than ${this.settings.maxPer.value} users have joined over the last ${this.settings.timescaleMinutes.value} minutes (since ${this.joinBuckets[roomId].lastBucketStart})`, roomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.sendStateEvent(roomId, "m.room.join_rules", "", {"join_rule": "invite"}) } else { await mjolnir.logMessage(LogLevel.WARN, "JoinWaveShortCircuit", `Tried to set ${roomId} to invite-only, but Mjolnir is running in no-op mode`, roomId); diff --git a/src/protections/MessageIsMedia.ts b/src/protections/MessageIsMedia.ts index a33cfbb..6d4c759 100644 --- a/src/protections/MessageIsMedia.ts +++ b/src/protections/MessageIsMedia.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; -import config from "../config"; export class MessageIsMedia extends Protection { @@ -43,7 +42,7 @@ export class MessageIsMedia extends Protection { if (isMedia) { await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Redacting event from ${event['sender']} for posting an image/video. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`); // Redact the event - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id'], "Images/videos are not permitted here"); } else { await mjolnir.logMessage(LogLevel.WARN, "MessageIsMedia", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); diff --git a/src/protections/MessageIsVoice.ts b/src/protections/MessageIsVoice.ts index 490e1bc..3b9382f 100644 --- a/src/protections/MessageIsVoice.ts +++ b/src/protections/MessageIsVoice.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, Permalinks, UserID } from "matrix-bot-sdk"; -import config from "../config"; export class MessageIsVoice extends Protection { @@ -40,7 +39,7 @@ export class MessageIsVoice extends Protection { if (event['content']['org.matrix.msc3245.voice'] === undefined) return; await mjolnir.logMessage(LogLevel.INFO, "MessageIsVoice", `Redacting event from ${event['sender']} for posting a voice message. ${Permalinks.forEvent(roomId, event['event_id'], [new UserID(await mjolnir.client.getUserId()).domain])}`); // Redact the event - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id'], "Voice messages are not permitted here"); } else { await mjolnir.logMessage(LogLevel.WARN, "MessageIsVoice", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); diff --git a/src/protections/TrustedReporters.ts b/src/protections/TrustedReporters.ts index 7f831f7..99c1a18 100644 --- a/src/protections/TrustedReporters.ts +++ b/src/protections/TrustedReporters.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import config from "../config"; import { Protection } from "./IProtection"; import { MXIDListProtectionSetting, NumberProtectionSetting } from "./ProtectionSettings"; import { Mjolnir } from "../Mjolnir"; @@ -83,7 +82,7 @@ export class TrustedReporters extends Protection { if (met.length > 0) { - await mjolnir.client.sendMessage(config.managementRoom, { + await mjolnir.client.sendMessage(mjolnir.config.managementRoom, { msgtype: "m.notice", body: `message ${event.id} reported by ${[...reporters].join(', ')}. ` + `actions: ${met.join(', ')}` diff --git a/src/protections/WordList.ts b/src/protections/WordList.ts index b089213..9decad1 100644 --- a/src/protections/WordList.ts +++ b/src/protections/WordList.ts @@ -17,7 +17,6 @@ limitations under the License. import { Protection } from "./IProtection"; import { Mjolnir } from "../Mjolnir"; import { LogLevel, LogService } from "matrix-bot-sdk"; -import config from "../config"; import { isTrueJoinEvent } from "../utils"; export class WordList extends Protection { @@ -25,15 +24,10 @@ export class WordList extends Protection { settings = {}; private justJoined: { [roomId: string]: { [username: string]: Date} } = {}; - private badWords: RegExp; + private badWords?: RegExp; constructor() { super(); - // Create a mega-regex from all the tiny baby regexs - this.badWords = new RegExp( - "(" + config.protections.wordlist.words.join(")|(") + ")", - "i" - ) } public get name(): string { @@ -47,7 +41,7 @@ export class WordList extends Protection { public async handleEvent(mjolnir: Mjolnir, roomId: string, event: any): Promise { const content = event['content'] || {}; - const minsBeforeTrusting = config.protections.wordlist.minutesBeforeTrusting; + const minsBeforeTrusting = mjolnir.config.protections.wordlist.minutesBeforeTrusting; if (minsBeforeTrusting > 0) { if (!this.justJoined[roomId]) this.justJoined[roomId] = {}; @@ -89,19 +83,29 @@ export class WordList extends Protection { return } } - + if (this.badWords === null) { + // Create a mega-regex from all the tiny baby regexs + try { + this.badWords = new RegExp( + "(" + mjolnir.config.protections.wordlist.words.join(")|(") + ")", + "i" + ); + } catch (ex) { + await mjolnir.logMessage(LogLevel.ERROR, "WordList", `Could not produce a regex from the word list:\n${ex}.`) + } + } // Perform the test - if (message && this.badWords.test(message)) { + if (message && this.badWords!.test(message)) { await mjolnir.logMessage(LogLevel.WARN, "WordList", `Banning ${event['sender']} for word list violation in ${roomId}.`); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.banUser(event['sender'], roomId, "Word list violation"); } else { await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to ban ${event['sender']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); } // Redact the event - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id'], "spam"); } else { await mjolnir.logMessage(LogLevel.WARN, "WordList", `Tried to redact ${event['event_id']} in ${roomId} but Mjolnir is running in no-op mode`, roomId); diff --git a/src/queues/ProtectedRoomActivityTracker.ts b/src/queues/ProtectedRoomActivityTracker.ts index 33692c1..643980e 100644 --- a/src/queues/ProtectedRoomActivityTracker.ts +++ b/src/queues/ProtectedRoomActivityTracker.ts @@ -39,6 +39,7 @@ export class ProtectedRoomActivityTracker { */ public addProtectedRoom(roomId: string): void { this.protectedRoomActivities.set(roomId, /* epoch */ 0); + this.activeRoomsCache = null; } /** @@ -47,6 +48,7 @@ export class ProtectedRoomActivityTracker { */ public removeProtectedRoom(roomId: string): void { this.protectedRoomActivities.delete(roomId); + this.activeRoomsCache = null; } /** diff --git a/src/queues/UnlistedUserRedactionQueue.ts b/src/queues/UnlistedUserRedactionQueue.ts index f774459..91ac878 100644 --- a/src/queues/UnlistedUserRedactionQueue.ts +++ b/src/queues/UnlistedUserRedactionQueue.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import { extractRequestError, LogLevel, LogService, Permalinks } from "matrix-bot-sdk"; -import config from "../config"; import { Mjolnir } from "../Mjolnir"; /** @@ -43,7 +42,7 @@ export class UnlistedUserRedactionQueue { const permalink = Permalinks.forEvent(roomId, event['event_id']); try { LogService.info("AutomaticRedactionQueue", `Redacting event because the user is listed as bad: ${permalink}`) - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(roomId, event['event_id']); } else { await mjolnir.logMessage(LogLevel.WARN, "AutomaticRedactionQueue", `Tried to redact ${permalink} but Mjolnir is running in no-op mode`); diff --git a/src/report/ReportManager.ts b/src/report/ReportManager.ts index 5c96d98..c75e636 100644 --- a/src/report/ReportManager.ts +++ b/src/report/ReportManager.ts @@ -20,8 +20,6 @@ import { htmlToText } from "html-to-text"; import { htmlEscape } from "../utils"; import { JSDOM } from 'jsdom'; import { EventEmitter } from 'events'; - -import config from "../config"; import { Mjolnir } from "../Mjolnir"; /// Regexp, used to extract the action label from an action reaction @@ -115,7 +113,7 @@ export class ReportManager extends EventEmitter { */ public async handleServerAbuseReport({ roomId, reporterId, event, reason }: { roomId: string, reporterId: string, event: any, reason?: string }) { this.emit("report.new", { roomId: roomId, reporterId: reporterId, event: event, reason: reason }); - if (config.displayReports) { + if (this.mjolnir.config.displayReports) { return this.displayManager.displayReportAndUI({ kind: Kind.SERVER_ABUSE_REPORT, event, reporterId, reason, moderationRoomId: this.mjolnir.managementRoomId }); } } diff --git a/src/report/ReportPoller.ts b/src/report/ReportPoller.ts index 2229b50..2e98833 100644 --- a/src/report/ReportPoller.ts +++ b/src/report/ReportPoller.ts @@ -18,7 +18,7 @@ import { Mjolnir, REPORT_POLL_EVENT_TYPE } from "../Mjolnir"; import { ReportManager } from './ReportManager'; import { LogLevel } from "matrix-bot-sdk"; -class InvalidStateError extends Error {} +class InvalidStateError extends Error { } /** * A class to poll synapse's report endpoint, so we can act on new reports @@ -68,7 +68,11 @@ export class ReportPoller { response_ = await this.mjolnir.client.doRequest( "GET", "/_synapse/admin/v1/event_reports", - { from: this.from.toString() } + { + // short for direction: forward; i.e. show newest last + dir: "f", + from: this.from.toString() + } ); } catch (ex) { await this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to poll events: ${ex}`); @@ -108,7 +112,7 @@ export class ReportPoller { if (response.next_token !== undefined) { this.from = response.next_token; try { - await this.mjolnir.client.setAccountData(REPORT_POLL_EVENT_TYPE, { from: response.next_token }); + await this.mjolnir.client.setAccountData(REPORT_POLL_EVENT_TYPE, { from: response.next_token }); } catch (ex) { await this.mjolnir.logMessage(LogLevel.ERROR, "getAbuseReports", `failed to update progress: ${ex}`); } diff --git a/src/utils.ts b/src/utils.ts index d30609f..7d31538 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -28,7 +28,6 @@ import { setRequestFn, } from "matrix-bot-sdk"; import { Mjolnir } from "./Mjolnir"; -import config from "./config"; import { ClientRequest, IncomingMessage } from "http"; import { default as parseDuration } from "parse-duration"; @@ -78,7 +77,7 @@ export async function redactUserMessagesIn(mjolnir: Mjolnir, userIdOrGlob: strin await getMessagesByUserIn(mjolnir.client, userIdOrGlob, targetRoomId, limit, async (eventsToRedact) => { for (const victimEvent of eventsToRedact) { await mjolnir.logMessage(LogLevel.DEBUG, "utils#redactUserMessagesIn", `Redacting ${victimEvent['event_id']} in ${targetRoomId}`, targetRoomId); - if (!config.noop) { + if (!mjolnir.config.noop) { await mjolnir.client.redactEvent(targetRoomId, victimEvent['event_id']); } else { await mjolnir.logMessage(LogLevel.WARN, "utils#redactUserMessagesIn", `Tried to redact ${victimEvent['event_id']} in ${targetRoomId} but Mjolnir is running in no-op mode`, targetRoomId); diff --git a/src/webapis/WebAPIs.ts b/src/webapis/WebAPIs.ts index 863c6ae..e8fae70 100644 --- a/src/webapis/WebAPIs.ts +++ b/src/webapis/WebAPIs.ts @@ -15,13 +15,11 @@ limitations under the License. */ import { Server } from "http"; - import * as express from "express"; import { LogService, MatrixClient } from "matrix-bot-sdk"; - -import config from "../config"; import RuleServer from "../models/RuleServer"; import { ReportManager } from "../report/ReportManager"; +import { IConfig } from "../config"; /** @@ -35,7 +33,7 @@ export class WebAPIs { private webController: express.Express = express(); private httpServer?: Server; - constructor(private reportManager: ReportManager, private readonly ruleServer: RuleServer|null) { + constructor(private reportManager: ReportManager, private readonly config: IConfig, private readonly ruleServer: RuleServer|null) { // Setup JSON parsing. this.webController.use(express.json()); } @@ -44,14 +42,14 @@ export class WebAPIs { * Start accepting requests to the Web API. */ public async start() { - if (!config.web.enabled) { + if (!this.config.web.enabled) { return; } - this.httpServer = this.webController.listen(config.web.port, config.web.address); + this.httpServer = this.webController.listen(this.config.web.port, this.config.web.address); - // Configure /report API. - if (config.web.abuseReporting.enabled) { - console.log(`Configuring ${API_PREFIX}/report/:room_id/:event_id...`); + // configure /report API. + if (this.config.web.abuseReporting.enabled) { + console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id...`); this.webController.options(`${API_PREFIX}/report/:room_id/:event_id`, async (request, response) => { // reply with CORS options response.header("Access-Control-Allow-Origin", "*"); @@ -68,15 +66,15 @@ export class WebAPIs { response.header("Access-Control-Allow-Methods", "POST, OPTIONS"); await this.handleReport({ request, response, roomId: request.params.room_id, eventId: request.params.event_id }) }); - console.log(`Configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`); + console.log(`configuring ${API_PREFIX}/report/:room_id/:event_id... DONE`); } - // Configure ruleServer API. + // configure ruleServer API. // FIXME: Doesn't this need some kind of access control? // See https://github.com/matrix-org/mjolnir/issues/139#issuecomment-1012221479. - if (config.web.ruleServer?.enabled) { + if (this.config.web.ruleServer?.enabled) { const updatesUrl = `${API_PREFIX}/ruleserver/updates`; - LogService.info("WebAPIs", `Configuring ${updatesUrl}...`); + LogService.info("WebAPIs", `configuring ${updatesUrl}...`); if (!this.ruleServer) { throw new Error("The rule server to use has not been configured for the WebAPIs."); } @@ -84,7 +82,7 @@ export class WebAPIs { this.webController.get(updatesUrl, async (request, response) => { await this.handleRuleServerUpdate(ruleServer, { request, response, since: request.query.since as string}); }); - LogService.info("WebAPIs", `Configuring ${updatesUrl}... DONE`); + LogService.info("WebAPIs", `configuring ${updatesUrl}... DONE`); } } @@ -163,7 +161,7 @@ export class WebAPIs { // so we are not extending the abilities of Mjölnir // 3. We are avoiding the use of the Synapse Admin API to ensure that // this feature can work with all homeservers, not just Synapse. - let reporterClient = new MatrixClient(config.rawHomeserverUrl, accessToken); + let reporterClient = new MatrixClient(this.config.rawHomeserverUrl, accessToken); reporterClient.start = () => { throw new Error("We MUST NEVER call start on the reporter client"); }; diff --git a/test/commands/UnbanBanCommandTest.ts b/test/commands/UnbanBanCommandTest.ts index ce94097..f19c897 100644 --- a/test/commands/UnbanBanCommandTest.ts +++ b/test/commands/UnbanBanCommandTest.ts @@ -18,18 +18,24 @@ import * as expect from "expect"; import { Mjolnir } from "../../src/Mjolnir"; import { DEFAULT_LIST_EVENT_TYPE } from "../../src/commands/SetDefaultBanListCommand"; import { parseArguments } from "../../src/commands/UnbanBanCommand"; -import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/BanList"; +import { read as configRead } from "../../src/config"; +import { RULE_ROOM, RULE_SERVER, RULE_USER } from "../../src/models/ListRule"; -function createTestMjolnir(defaultShortcode: string = null): Mjolnir { +function createTestMjolnir(defaultShortcode: string|null = null): Mjolnir { + const config = configRead(); const client = { + // Mock `MatrixClient.getAccountData` . getAccountData: (eventType: string): Promise => { - if (eventType === DEFAULT_LIST_EVENT_TYPE && defaultShortcode) { + if (eventType === DEFAULT_LIST_EVENT_TYPE || defaultShortcode) { return Promise.resolve({shortcode: defaultShortcode}); } - throw new Error("Unknown event type"); + throw new Error(`Unknown event type ${eventType}, expected ${DEFAULT_LIST_EVENT_TYPE}`); }, }; - return {client}; + return { + client, + config, + }; } function createFakeEvent(command: string): any { @@ -55,11 +61,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_SERVER); - expect(bits.entity).toBe("example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_SERVER); + expect(bits!.entity).toBe("example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect servers with ban reasons", async () => { @@ -72,11 +78,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test example.org reason here"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBe("reason here"); - expect(bits.ruleType).toBe(RULE_SERVER); - expect(bits.entity).toBe("example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBe("reason here"); + expect(bits!.ruleType).toBe(RULE_SERVER); + expect(bits!.entity).toBe("example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect servers with globs", async () => { @@ -89,11 +95,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test *.example.org --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_SERVER); - expect(bits.entity).toBe("*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_SERVER); + expect(bits!.entity).toBe("*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect servers with the type specified", async () => { @@ -106,11 +112,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test server @*.example.org --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_SERVER); - expect(bits.entity).toBe("@*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_SERVER); + expect(bits!.entity).toBe("@*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect room IDs", async () => { @@ -123,11 +129,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test !example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_ROOM); - expect(bits.entity).toBe("!example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_ROOM); + expect(bits!.entity).toBe("!example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect room IDs with ban reasons", async () => { @@ -140,11 +146,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test !example.org reason here"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBe("reason here"); - expect(bits.ruleType).toBe(RULE_ROOM); - expect(bits.entity).toBe("!example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBe("reason here"); + expect(bits!.ruleType).toBe(RULE_ROOM); + expect(bits!.entity).toBe("!example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect room IDs with globs", async () => { @@ -157,11 +163,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test !*.example.org --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_ROOM); - expect(bits.entity).toBe("!*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_ROOM); + expect(bits!.entity).toBe("!*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect room aliases", async () => { @@ -174,11 +180,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test #example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_ROOM); - expect(bits.entity).toBe("#example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_ROOM); + expect(bits!.entity).toBe("#example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect room aliases with ban reasons", async () => { @@ -191,11 +197,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test #example.org reason here"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBe("reason here"); - expect(bits.ruleType).toBe(RULE_ROOM); - expect(bits.entity).toBe("#example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBe("reason here"); + expect(bits!.ruleType).toBe(RULE_ROOM); + expect(bits!.entity).toBe("#example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect room aliases with globs", async () => { @@ -208,11 +214,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test #*.example.org --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_ROOM); - expect(bits.entity).toBe("#*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_ROOM); + expect(bits!.entity).toBe("#*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect rooms with the type specified", async () => { @@ -225,11 +231,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test room @*.example.org --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_ROOM); - expect(bits.entity).toBe("@*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_ROOM); + expect(bits!.entity).toBe("@*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect user IDs", async () => { @@ -242,11 +248,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test @example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect user IDs with ban reasons", async () => { @@ -259,11 +265,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test @example.org reason here"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBe("reason here"); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBe("reason here"); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect user IDs with globs", async () => { @@ -276,11 +282,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test @*.example.org --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should be able to detect user IDs with the type specified", async () => { @@ -293,11 +299,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test user #*.example.org --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("#*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("#*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should error if wildcards used without --force", async () => { @@ -324,11 +330,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test user #*.example.org reason here --force"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBe("reason here"); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("#*.example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBe("reason here"); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("#*.example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); describe("[without default list]", () => { @@ -370,11 +376,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban user test @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should not error if a list (without type) is specified", async () => { @@ -387,11 +393,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should not error if a list (with type reversed) is specified", async () => { @@ -404,11 +410,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban test user @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); }); @@ -423,11 +429,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban user @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should use the default list if no list (without type) is specified", async () => { @@ -440,11 +446,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("test"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("test"); }); it("should use the specified list if a list (with type) is specified", async () => { @@ -457,11 +463,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban user other @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("other"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("other"); }); it("should use the specified list if a list (without type) is specified", async () => { @@ -474,11 +480,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban other @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("other"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("other"); }); it("should not error if a list (with type reversed) is specified", async () => { @@ -491,11 +497,11 @@ describe("UnbanBanCommand", () => { const command = "!mjolnir ban other user @example:example.org"; const bits = await parseArguments("!a", createFakeEvent(command), mjolnir, command.split(' ')); expect(bits).toBeTruthy(); - expect(bits.reason).toBeFalsy(); - expect(bits.ruleType).toBe(RULE_USER); - expect(bits.entity).toBe("@example:example.org"); - expect(bits.list).toBeDefined(); - expect(bits.list.listShortcode).toBe("other"); + expect(bits!.reason).toBeFalsy(); + expect(bits!.ruleType).toBe(RULE_USER); + expect(bits!.entity).toBe("@example:example.org"); + expect(bits!.list).toBeDefined(); + expect(bits!.list!.listShortcode).toBe("other"); }); }); }); diff --git a/test/integration/abuseReportTest.ts b/test/integration/abuseReportTest.ts index e8abbc4..e3c58b7 100644 --- a/test/integration/abuseReportTest.ts +++ b/test/integration/abuseReportTest.ts @@ -32,8 +32,8 @@ describe("Test: Reporting abuse", async () => { }); // Create a few users and a room. - let goodUser = await newTestUser({ name: { contains: "reporting-abuse-good-user" }}); - let badUser = await newTestUser({ name: { contains: "reporting-abuse-bad-user" }}); + let goodUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-good-user" }}); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-bad-user" }}); let goodUserId = await goodUser.getUserId(); let badUserId = await badUser.getUserId(); @@ -227,13 +227,13 @@ describe("Test: Reporting abuse", async () => { }); // Create a moderator. - let moderatorUser = await newTestUser({ name: { contains: "reporting-abuse-moderator-user" }}); + let moderatorUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reporting-abuse-moderator-user" }}); matrixClient().inviteUser(await moderatorUser.getUserId(), this.mjolnir.managementRoomId); await moderatorUser.joinRoom(this.mjolnir.managementRoomId); // Create a few users and a room. - let goodUser = await newTestUser({ name: { contains: "reacting-abuse-good-user" }}); - let badUser = await newTestUser({ name: { contains: "reacting-abuse-bad-user" }}); + let goodUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reacting-abuse-good-user" }}); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "reacting-abuse-bad-user" }}); let goodUserId = await goodUser.getUserId(); let badUserId = await badUser.getUserId(); diff --git a/test/integration/acceptInvitesFromSpaceTest.ts b/test/integration/acceptInvitesFromSpaceTest.ts new file mode 100644 index 0000000..3f581a5 --- /dev/null +++ b/test/integration/acceptInvitesFromSpaceTest.ts @@ -0,0 +1,48 @@ +import { MatrixClient } from "matrix-bot-sdk"; +import { Mjolnir } from "../../src/Mjolnir" +import { newTestUser } from "./clientHelper"; + +describe("Test: Accept Invites From Space", function() { + let client: MatrixClient|undefined; + this.beforeEach(async function () { + client = await newTestUser(this.config.homeserverUrl, { name: { contains: "spacee" }}); + await client.start(); + }) + this.afterEach(async function () { + await client.stop(); + }) + it("Mjolnir should accept an invite from a user in a nominated Space", async function() { + this.timeout(20000); + + const mjolnir: Mjolnir = this.mjolnir!; + const mjolnirUserId = await mjolnir.client.getUserId(); + + const space = await client.createSpace({ + name: "mjolnir space invite test", + invites: [mjolnirUserId], + isPublic: false + }); + + await this.mjolnir.client.joinRoom(space.roomId); + + // we're mutating a static object, which may affect other tests :( + mjolnir.config.autojoinOnlyIfManager = false; + mjolnir.config.acceptInvitesFromSpace = space.roomId; + + const promise = new Promise(async resolve => { + const newRoomId = await client.createRoom({ invite: [mjolnirUserId] }); + client.on("room.event", (roomId, event) => { + if ( + roomId === newRoomId + && event.type === "m.room.member" + && event.sender === mjolnirUserId + && event.content?.membership === "join" + ) { + resolve(null); + } + }); + }); + await promise; + }); +}); + diff --git a/test/integration/banListTest.ts b/test/integration/banListTest.ts index 66e52ef..1c8610c 100644 --- a/test/integration/banListTest.ts +++ b/test/integration/banListTest.ts @@ -1,12 +1,12 @@ import { strict as assert } from "assert"; - -import config from "../../src/config"; import { newTestUser } from "./clientHelper"; import { LogService, MatrixClient, Permalinks, UserID } from "matrix-bot-sdk"; -import BanList, { ALL_RULE_TYPES, ChangeType, ListRuleChange, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/BanList"; +import PolicyList, { ChangeType, ListRuleChange } from "../../src/models/PolicyList"; import { ServerAcl } from "../../src/models/ServerAcl"; import { getFirstReaction } from "./commands/commandUtils"; import { getMessagesByUserIn } from "../../src/utils"; +import { Mjolnir } from "../../src/Mjolnir"; +import { ALL_RULE_TYPES, RULE_SERVER, RULE_USER, SERVER_RULE_TYPES } from "../../src/models/ListRule"; /** * Create a policy rule in a policy room. @@ -18,7 +18,7 @@ import { getMessagesByUserIn } from "../../src/utils"; * @param template The template to use for the policy rule event. * @returns The event id of the newly created policy rule. */ -async function createPolicyRule(client: MatrixClient, policyRoomId: string, policyType: string, entity: string, reason: string, template = {recommendation: 'm.ban'}) { +async function createPolicyRule(client: MatrixClient, policyRoomId: string, policyType: string, entity: string, reason: string, template = { recommendation: 'm.ban' }) { return await client.sendStateEvent(policyRoomId, policyType, `rule:${entity}`, { entity, reason, @@ -26,36 +26,36 @@ async function createPolicyRule(client: MatrixClient, policyRoomId: string, poli }); } -describe("Test: Updating the BanList", function () { - it("Calculates what has changed correctly.", async function () { +describe("Test: Updating the PolicyList", function() { + it("Calculates what has changed correctly.", async function() { this.timeout(10000); - const mjolnir = config.RUNTIME.client! - const moderator = await newTestUser({ name: { contains: "moderator" }}); - const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]}); - const banList = new BanList(banListId, banListId, mjolnir); - mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); + const mjolnir: Mjolnir = this.mjolnir! + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] }); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100); assert.equal(banList.allRules.length, 0); // Test adding a new rule - await createPolicyRule(mjolnir, banListId, RULE_USER, '@added:localhost:9999', ''); + await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@added:localhost:9999', ''); let changes: ListRuleChange[] = await banList.updateList(); assert.equal(changes.length, 1, 'There should only be one change'); assert.equal(changes[0].changeType, ChangeType.Added); - assert.equal(changes[0].sender, await mjolnir.getUserId()); + assert.equal(changes[0].sender, await mjolnir.client.getUserId()); assert.equal(banList.userRules.length, 1); assert.equal(banList.allRules.length, 1); // Test modifiying a rule - let originalEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', ''); + let originalEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', ''); await banList.updateList(); - let modifyingEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason'); + let modifyingEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified reason'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule'); assert.equal(changes[0].event['event_id'], modifyingEventId); - let modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@modified:localhost:9999', 'modified again'); + let modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@modified:localhost:9999', 'modified again'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); @@ -64,10 +64,10 @@ describe("Test: Updating the BanList", function () { assert.equal(banList.userRules.length, 2, 'There should be two rules, one for @modified:localhost:9999 and one for @added:localhost:9999'); // Test redacting a rule - const redactThis = await createPolicyRule(mjolnir, banListId, RULE_USER, '@redacted:localhost:9999', ''); + const redactThis = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@redacted:localhost:9999', ''); await banList.updateList(); assert.equal(banList.userRules.filter(r => r.entity === '@redacted:localhost:9999').length, 1); - await mjolnir.redactEvent(banListId, redactThis); + await mjolnir.client.redactEvent(banListId, redactThis); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -79,10 +79,10 @@ describe("Test: Updating the BanList", function () { // Test soft redaction of a rule const softRedactedEntity = '@softredacted:localhost:9999' - await createPolicyRule(mjolnir, banListId, RULE_USER, softRedactedEntity, ''); + await createPolicyRule(mjolnir.client, banListId, RULE_USER, softRedactedEntity, ''); await banList.updateList(); assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 1); - await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); + await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -92,25 +92,25 @@ describe("Test: Updating the BanList", function () { assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed'); // Now test a double soft redaction just to make sure stuff doesn't explode - await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); + await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${softRedactedEntity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 0, "It shouldn't detect a double soft redaction as a change, it should be seen as adding an invalid rule."); assert.equal(banList.userRules.filter(r => r.entity === softRedactedEntity).length, 0, 'The rule should have been removed'); // Test that different (old) rule types will be modelled as the latest event type. - originalEventId = await createPolicyRule(mjolnir, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', ''); + originalEventId = await createPolicyRule(mjolnir.client, banListId, 'org.matrix.mjolnir.rule.user', '@old:localhost:9999', ''); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Added); assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1); - modifyingEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason'); + modifyingEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', '@old:localhost:9999', 'modified reason'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); assert.equal(changes[0].event['event_id'], modifyingEventId); assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule'); assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1); - modifyingAgainEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, '@old:localhost:9999', 'changes again'); + modifyingAgainEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, '@old:localhost:9999', 'changes again'); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Modified); @@ -118,21 +118,21 @@ describe("Test: Updating the BanList", function () { assert.equal(changes[0].previousState['event_id'], modifyingEventId, 'There should be a previous state event for a modified rule'); assert.equal(banList.userRules.filter(r => r.entity === '@old:localhost:9999').length, 1); }) - it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function () { + it("Will remove rules with old types when they are 'soft redacted' with a different but more recent event type.", async function() { this.timeout(3000); - const mjolnir = config.RUNTIME.client! - const moderator = await newTestUser({ name: { contains: "moderator" }}); - const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]}); - const banList = new BanList(banListId, banListId, mjolnir); - mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); + const mjolnir: Mjolnir = this.mjolnir! + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }} ); + const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] }); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100); const entity = '@old:localhost:9999'; - let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, ''); + let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, ''); let changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Added); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...') - let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); + let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -140,21 +140,20 @@ describe("Test: Updating the BanList", function () { assert.equal(changes[0].previousState['event_id'], originalEventId, 'There should be a previous state event for a modified rule'); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.'); }) - it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function () { - this.timeout(3000); - const mjolnir = config.RUNTIME.client! - const moderator = await newTestUser({ name: { contains: "moderator" }}); - const banListId = await mjolnir.createRoom({ invite: [await moderator.getUserId()]}); - const banList = new BanList(banListId, banListId, mjolnir); - mjolnir.setUserPowerLevel(await moderator.getUserId(), banListId, 100); + it("A rule of the most recent type won't be deleted when an old rule is deleted for the same entity.", async function() { + const mjolnir: Mjolnir = this.mjolnir! + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + const banListId = await mjolnir.client.createRoom({ invite: [await moderator.getUserId()] }); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + mjolnir.client.setUserPowerLevel(await moderator.getUserId(), banListId, 100); const entity = '@old:localhost:9999'; - let originalEventId = await createPolicyRule(mjolnir, banListId, 'm.room.rule.user', entity, ''); + let originalEventId = await createPolicyRule(mjolnir.client, banListId, 'm.room.rule.user', entity, ''); let changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Added); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'There should be a rule stored that we just added...') - let updatedEventId = await createPolicyRule(mjolnir, banListId, RULE_USER, entity, ''); + let updatedEventId = await createPolicyRule(mjolnir.client, banListId, RULE_USER, entity, ''); changes = await banList.updateList(); // If in the future you change this and it fails, it's really subjective whether this constitutes a modification, since the only thing that has changed // is the rule type. The actual content is identical. @@ -165,13 +164,13 @@ describe("Test: Updating the BanList", function () { assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'Only the latest version of the rule gets returned.'); // Now we delete the old version of the rule without consequence. - await mjolnir.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {}); + await mjolnir.client.sendStateEvent(banListId, 'm.room.rule.user', `rule:${entity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 0); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 1, 'The rule should still be active.'); // And we can still delete the new version of the rule. - let softRedactingEventId = await mjolnir.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); + let softRedactingEventId = await mjolnir.client.sendStateEvent(banListId, RULE_USER, `rule:${entity}`, {}); changes = await banList.updateList(); assert.equal(changes.length, 1); assert.equal(changes[0].changeType, ChangeType.Removed); @@ -179,12 +178,12 @@ describe("Test: Updating the BanList", function () { assert.equal(changes[0].previousState['event_id'], updatedEventId, 'There should be a previous state event for a modified rule'); assert.equal(banList.userRules.filter(rule => rule.entity === entity).length, 0, 'The rule should no longer be stored.'); }) - it('Test: BanList Supports all entity types.', async function () { - const mjolnir = config.RUNTIME.client! - const banListId = await mjolnir.createRoom(); - const banList = new BanList(banListId, banListId, mjolnir); + it('Test: PolicyList Supports all entity types.', async function () { + const mjolnir: Mjolnir = this.mjolnir! + const banListId = await mjolnir.client.createRoom(); + const banList = new PolicyList(banListId, banListId, mjolnir.client); for (let i = 0; i < ALL_RULE_TYPES.length; i++) { - await createPolicyRule(mjolnir, banListId, ALL_RULE_TYPES[i], `*${i}*`, ''); + await createPolicyRule(mjolnir.client, banListId, ALL_RULE_TYPES[i], `*${i}*`, ''); } let changes: ListRuleChange[] = await banList.updateList(); assert.equal(changes.length, ALL_RULE_TYPES.length); @@ -192,31 +191,31 @@ describe("Test: Updating the BanList", function () { }) }); -describe('Test: We do not respond to recommendations other than m.ban in the banlist', function () { - it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function () { - const mjolnir = config.RUNTIME.client! - const banListId = await mjolnir.createRoom(); - const banList = new BanList(banListId, banListId, mjolnir); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'exmaple.org', '', {recommendation: 'something that is not m.ban'}); +describe('Test: We do not respond to recommendations other than m.ban in the PolicyList', function() { + it('Will not respond to a rule that has a different recommendation to m.ban (or the unstable equivalent).', async function() { + const mjolnir: Mjolnir = this.mjolnir! + const banListId = await mjolnir.client.createRoom(); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'exmaple.org', '', { recommendation: 'something that is not m.ban' }); let changes: ListRuleChange[] = await banList.updateList(); assert.equal(changes.length, 1, 'There should only be one change'); assert.equal(changes[0].changeType, ChangeType.Added); - assert.equal(changes[0].sender, await mjolnir.getUserId()); + assert.equal(changes[0].sender, await mjolnir.client.getUserId()); // We really don't want things that aren't m.ban to end up being accessible in these APIs. - assert.equal(banList.serverRules.length, 0); - assert.equal(banList.allRules.length, 0); + assert.equal(banList.serverRules.length, 0, `We should have an empty serverRules, got ${JSON.stringify(banList.serverRules)}`); + assert.equal(banList.allRules.length, 0, `We should have an empty allRules, got ${JSON.stringify(banList.allRules)}`); }) }) -describe('Test: We will not be able to ban ourselves via ACL.', function () { - it('We do not ban ourselves when we put ourselves into the policy list.', async function () { - const mjolnir = config.RUNTIME.client! - const serverName = new UserID(await mjolnir.getUserId()).domain; - const banListId = await mjolnir.createRoom(); - const banList = new BanList(banListId, banListId, mjolnir); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, serverName, ''); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, 'evil.com', ''); - await createPolicyRule(mjolnir, banListId, RULE_SERVER, '*', ''); +describe('Test: We will not be able to ban ourselves via ACL.', function() { + it('We do not ban ourselves when we put ourselves into the policy list.', async function() { + const mjolnir: Mjolnir = this.mjolnir + const serverName = new UserID(await mjolnir.client.getUserId()).domain; + const banListId = await mjolnir.client.createRoom(); + const banList = new PolicyList(banListId, banListId, mjolnir.client); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, serverName, ''); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, 'evil.com', ''); + await createPolicyRule(mjolnir.client, banListId, RULE_SERVER, '*', ''); // We should still intern the matching rules rule. let changes: ListRuleChange[] = await banList.updateList(); assert.equal(banList.serverRules.length, 3); @@ -229,62 +228,65 @@ describe('Test: We will not be able to ban ourselves via ACL.', function () { }) -describe('Test: ACL updates will batch when rules are added in succession.', function () { - it('Will batch ACL updates if we spam rules into a BanList', async function () { - const mjolnir = config.RUNTIME.client! - const serverName: string = new UserID(await mjolnir.getUserId()).domain - const moderator = await newTestUser({ name: { contains: "moderator" }}); - moderator.joinRoom(this.mjolnir.managementRoomId); - const mjolnirId = await mjolnir.getUserId(); +describe('Test: ACL updates will batch when rules are added in succession.', function() { + it('Will batch ACL updates if we spam rules into a PolicyList', async function() { + const mjolnir: Mjolnir = this.mjolnir! + const serverName: string = new UserID(await mjolnir.client.getUserId()).domain + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + moderator.joinRoom(this.mjolnir.client.managementRoomId); + const mjolnirId = await mjolnir.client.getUserId(); // Setup some protected rooms so we can check their ACL state later. const protectedRooms: string[] = []; - for (let i = 0; i < 10; i++) { - const room = await moderator.createRoom({ invite: [mjolnirId]}); - await mjolnir.joinRoom(room); + for (let i = 0; i < 5; i++) { + const room = await moderator.createRoom({ invite: [mjolnirId] }); + await mjolnir.client.joinRoom(room); await moderator.setUserPowerLevel(mjolnirId, room, 100); - await this.mjolnir!.addProtectedRoom(room); + await mjolnir.addProtectedRoom(room); protectedRooms.push(room); } // If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); await Promise.all(protectedRooms.map(async room => { // We're going to need timeline pagination I'm afraid. - const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", ""); + const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", ""); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.'); })); // Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms. const banListId = await moderator.createRoom({ invite: [mjolnirId] }); - mjolnir.joinRoom(banListId); - this.mjolnir!.watchList(Permalinks.forRoom(banListId)); + mjolnir.client.joinRoom(banListId); + mjolnir.watchList(Permalinks.forRoom(banListId)); const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*"); - for (let i = 0; i < 200; i++) { + const evilServerCount = 200; + for (let i = 0; i < evilServerCount; i++) { const badServer = `${i}.evil.com`; acl.denyServer(badServer); await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule #${i}`); // Give them a bit of a spread over time. await new Promise(resolve => setTimeout(resolve, 5)); } - // give the events a chance to appear in the response to `/state`, since this is a problem. - await new Promise(resolve => setTimeout(resolve, 2000)); - // We do this because it should force us to wait until all the ACL events have been applied. // Even if that does mean the last few events will not go through batching... - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); + + // At this point we check that the state within Mjolnir is internally consistent, this is just because debugging the following + // is a pita. + const list: PolicyList = this.mjolnir.policyLists[0]!; + assert.equal(list.serverRules.length, evilServerCount, `There should be ${evilServerCount} rules in here`); // Check each of the protected rooms for ACL events and make sure they were batched and are correct. await Promise.all(protectedRooms.map(async room => { - const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", ""); + const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", ""); if (!acl.matches(roomAcl)) { assert.fail(`Room ${room} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`) } let aclEventCount = 0; - await getMessagesByUserIn(mjolnir, mjolnirId, room, 100, events => { + await getMessagesByUserIn(mjolnir.client, mjolnirId, room, 100, events => { events.forEach(event => event.type === 'm.room.server_acl' ? aclEventCount += 1 : null); }); - LogService.debug('BanListTest', `aclEventCount: ${aclEventCount}`); + LogService.debug('PolicyListTest', `aclEventCount: ${aclEventCount}`); // If there's less than two then it means the ACL was updated by this test calling `this.mjolnir!.syncLists()` // and not the listener that detects changes to ban lists (that we want to test!). // It used to be 10, but it was too low, 30 seems better for CI. @@ -293,36 +295,36 @@ describe('Test: ACL updates will batch when rules are added in succession.', fun }) }) -describe('Test: unbaning entities via the BanList.', function () { +describe('Test: unbaning entities via the PolicyList.', function() { afterEach(function() { this.moderator?.stop(); }); - it('Will remove rules that have legacy types', async function () { - this.timeout(20000) - const mjolnir = config.RUNTIME.client! - const serverName: string = new UserID(await mjolnir.getUserId()).domain - const moderator = await newTestUser({ name: { contains: "moderator" }}); + it('Will remove rules that have legacy types', async function() { + const mjolnir: Mjolnir = this.mjolnir! + const serverName: string = new UserID(await mjolnir.client.getUserId()).domain + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - moderator.joinRoom(this.mjolnir.managementRoomId); - const mjolnirId = await mjolnir.getUserId(); + await moderator.joinRoom(mjolnir.managementRoomId); + const mjolnirId = await mjolnir.client.getUserId(); // We'll make 1 protected room to test ACLs in. - const protectedRoom = await moderator.createRoom({ invite: [mjolnirId]}); - await mjolnir.joinRoom(protectedRoom); + const protectedRoom = await moderator.createRoom({ invite: [mjolnirId] }); + await mjolnir.client.joinRoom(protectedRoom); await moderator.setUserPowerLevel(mjolnirId, protectedRoom, 100); - await this.mjolnir!.addProtectedRoom(protectedRoom); + await mjolnir.addProtectedRoom(protectedRoom); // If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. - await this.mjolnir!.syncLists(); - const roomAcl = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); + await mjolnir.syncLists(); + // If this is not present, then it means the room isn't being protected, which is really bad. + const roomAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.'); - // Create some legacy rules on a BanList. + // Create some legacy rules on a PolicyList. const banListId = await moderator.createRoom({ invite: [mjolnirId] }); - await moderator.setUserPowerLevel(await mjolnir.getUserId(), banListId, 100); - await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test"}); - await mjolnir.joinRoom(banListId); - this.mjolnir!.watchList(Permalinks.forRoom(banListId)); + await moderator.setUserPowerLevel(await mjolnir.client.getUserId(), banListId, 100); + await moderator.sendStateEvent(banListId, 'org.matrix.mjolnir.shortcode', '', { shortcode: "unban-test" }); + await mjolnir.client.joinRoom(banListId); + await mjolnir.watchList(Permalinks.forRoom(banListId)); // we use this to compare changes. - const banList = new BanList(banListId, banListId, moderator); + const banList = new PolicyList(banListId, banListId, moderator); // we need two because we need to test the case where an entity has all rule types in the list // and another one that only has one (so that we would hit 404 while looking up state) const olderBadServer = "old.evil.com" @@ -338,7 +340,7 @@ describe('Test: unbaning entities via the BanList.', function () { // Check that we have setup our test properly and therefore evil.com is banned. const acl = new ServerAcl(serverName).denyIpAddresses().allowServer("*").denyServer(olderBadServer).denyServer(newerBadServer); - const protectedAcl = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); + const protectedAcl = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); if (!acl.matches(protectedAcl)) { assert.fail(`Room ${protectedRoom} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`); } @@ -347,8 +349,8 @@ describe('Test: unbaning entities via the BanList.', function () { try { await moderator.start(); for (const server of [olderBadServer, newerBadServer]) { - await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => { - return await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: `!mjolnir unban unban-test server ${server}`}); + await getFirstReaction(moderator, mjolnir.managementRoomId, '✅', async () => { + return await moderator.sendMessage(mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unban unban-test server ${server}` }); }); } } finally { @@ -356,47 +358,47 @@ describe('Test: unbaning entities via the BanList.', function () { } // Wait for mjolnir to sync protected rooms to update ACL. - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); // Confirm that the server is unbanned. await banList.updateList(); assert.equal(banList.allRules.length, 0); - const aclAfter = await mjolnir.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); + const aclAfter = await mjolnir.client.getRoomStateEvent(protectedRoom, "m.room.server_acl", ""); assert.equal(aclAfter.deny.length, 0, 'Should be no servers denied anymore'); }) }) -describe('Test: should apply bans to the most recently active rooms first', function () { - it('Applies bans to the most recently active rooms first', async function () { +describe('Test: should apply bans to the most recently active rooms first', function() { + it('Applies bans to the most recently active rooms first', async function() { this.timeout(180000) - const mjolnir = config.RUNTIME.client! - const serverName: string = new UserID(await mjolnir.getUserId()).domain - const moderator = await newTestUser({ name: { contains: "moderator" }}); - moderator.joinRoom(this.mjolnir.managementRoomId); - const mjolnirId = await mjolnir.getUserId(); + const mjolnir: Mjolnir = this.mjolnir! + const serverName: string = new UserID(await mjolnir.client.getUserId()).domain + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + moderator.joinRoom(mjolnir.managementRoomId); + const mjolnirId = await mjolnir.client.getUserId(); // Setup some protected rooms so we can check their ACL state later. const protectedRooms: string[] = []; for (let i = 0; i < 10; i++) { - const room = await moderator.createRoom({ invite: [mjolnirId]}); - await mjolnir.joinRoom(room); + const room = await moderator.createRoom({ invite: [mjolnirId] }); + await mjolnir.client.joinRoom(room); await moderator.setUserPowerLevel(mjolnirId, room, 100); - await this.mjolnir!.addProtectedRoom(room); + await mjolnir.addProtectedRoom(room); protectedRooms.push(room); } // If a previous test hasn't cleaned up properly, these rooms will be populated by bogus ACLs at this point. - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); await Promise.all(protectedRooms.map(async room => { - const roomAcl = await mjolnir.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? {deny: []} : Promise.reject(e)); + const roomAcl = await mjolnir.client.getRoomStateEvent(room, "m.room.server_acl", "").catch(e => e.statusCode === 404 ? { deny: [] } : Promise.reject(e)); assert.equal(roomAcl?.deny?.length ?? 0, 0, 'There should be no entries in the deny ACL.'); })); // Flood the watched list with banned servers, which should prompt Mjolnir to update server ACL in protected rooms. const banListId = await moderator.createRoom({ invite: [mjolnirId] }); - mjolnir.joinRoom(banListId); - this.mjolnir!.watchList(Permalinks.forRoom(banListId)); + mjolnir.client.joinRoom(banListId); + mjolnir.watchList(Permalinks.forRoom(banListId)); - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); // shuffle protected rooms https://stackoverflow.com/a/12646864, we do this so we can create activity "randomly" in them. for (let i = protectedRooms.length - 1; i > 0; i--) { @@ -405,13 +407,13 @@ describe('Test: should apply bans to the most recently active rooms first', func } // create some activity in the same order. for (const roomId of protectedRooms.slice().reverse()) { - await mjolnir.sendMessage(roomId, {body: `activity`, msgtype: 'm.text'}); + await mjolnir.client.sendMessage(roomId, { body: `activity`, msgtype: 'm.text' }); await new Promise(resolve => setTimeout(resolve, 100)); } // check the rooms are in the expected order for (let i = 0; i < protectedRooms.length; i++) { - assert.equal(this.mjolnir!.protectedRoomsByActivity()[i], protectedRooms[i]); + assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms[i]); } const badServer = `evil.com`; @@ -420,18 +422,18 @@ describe('Test: should apply bans to the most recently active rooms first', func await createPolicyRule(moderator, banListId, RULE_SERVER, badServer, `Rule ${badServer}`); // Wait until all the ACL events have been applied. - await this.mjolnir!.syncLists(); + await mjolnir.syncLists(); for (let i = 0; i < protectedRooms.length; i++) { - assert.equal(this.mjolnir!.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1)); + assert.equal(mjolnir.protectedRoomsByActivity()[i], protectedRooms.at(-i - 1)); } // Check that the most recently active rooms got the ACL update first. let last_event_ts = 0; for (const roomId of protectedRooms) { - let roomAclEvent: null|any; + let roomAclEvent: null | any; // Can't be the best way to get the whole event, but ok. - await getMessagesByUserIn(mjolnir, mjolnirId, roomId, 1, events => roomAclEvent = events[0]); + await getMessagesByUserIn(mjolnir.client, mjolnirId, roomId, 1, events => roomAclEvent = events[0]); const roomAcl = roomAclEvent!.content; if (!acl.matches(roomAcl)) { assert.fail(`Room ${roomId} doesn't have the correct ACL: ${JSON.stringify(roomAcl, null, 2)}`) diff --git a/test/integration/clientHelper.ts b/test/integration/clientHelper.ts index 7b38900..9a8de47 100644 --- a/test/integration/clientHelper.ts +++ b/test/integration/clientHelper.ts @@ -1,6 +1,5 @@ import { HmacSHA1 } from "crypto-js"; import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk"; -import config from "../../src/config"; const REGISTRATION_ATTEMPTS = 10; const REGISTRATION_RETRY_BASE_DELAY_MS = 100; @@ -16,8 +15,8 @@ const REGISTRATION_RETRY_BASE_DELAY_MS = 100; * @param admin True to make the user an admin, false otherwise. * @returns The response from synapse. */ -export async function registerUser(username: string, displayname: string, password: string, admin: boolean): Promise { - let registerUrl = `${config.homeserverUrl}/_synapse/admin/v1/register` +export async function registerUser(homeserver: string, username: string, displayname: string, password: string, admin: boolean): Promise { + let registerUrl = `${homeserver}/_synapse/admin/v1/register` const data: {nonce: string} = await new Promise((resolve, reject) => { getRequestFn()({uri: registerUrl, method: "GET", timeout: 60000}, (error: any, response: any, resBody: any) => { error ? reject(error) : resolve(JSON.parse(resBody)) @@ -81,7 +80,7 @@ export type RegistrationOptions = { * * @returns A string that is both the username and password of a new user. */ -async function registerNewTestUser(options: RegistrationOptions) { +async function registerNewTestUser(homeserver: string, options: RegistrationOptions) { do { let username; if ("exact" in options.name) { @@ -90,7 +89,7 @@ async function registerNewTestUser(options: RegistrationOptions) { username = `mjolnir-test-user-${options.name.contains}${Math.floor(Math.random() * 100000)}` } try { - await registerUser(username, username, username, Boolean(options.isAdmin)); + await registerUser(homeserver, username, username, username, Boolean(options.isAdmin)); return username; } catch (e) { if (e?.body?.errcode === 'M_USER_IN_USE') { @@ -113,13 +112,13 @@ async function registerNewTestUser(options: RegistrationOptions) { * * @returns A new `MatrixClient` session for a unique test user. */ -export async function newTestUser(options: RegistrationOptions): Promise { - const username = await registerNewTestUser(options); - const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()); +export async function newTestUser(homeserver: string, options: RegistrationOptions): Promise { + const username = await registerNewTestUser(homeserver, options); + const pantalaimon = new PantalaimonClient(homeserver, new MemoryStorageProvider()); const client = await pantalaimon.createClientWithCredentials(username, username); if (!options.isThrottled) { let userId = await client.getUserId(); - await overrideRatelimitForUser(userId); + await overrideRatelimitForUser(homeserver, userId); } return client; } @@ -130,12 +129,12 @@ let _globalAdminUser: MatrixClient; * Get a client that can perform synapse admin API actions. * @returns A client logged in with an admin user. */ -async function getGlobalAdminUser(): Promise { +async function getGlobalAdminUser(homeserver: string): Promise { // Initialize global admin user if needed. if (!_globalAdminUser) { const USERNAME = "mjolnir-test-internal-admin-user"; try { - await registerUser(USERNAME, USERNAME, USERNAME, true); + await registerUser(homeserver, USERNAME, USERNAME, USERNAME, true); } catch (e) { if (e.isAxiosError && e?.response?.data?.errcode === 'M_USER_IN_USE') { // Then we've already registered the user in a previous run and that is ok. @@ -143,7 +142,7 @@ async function getGlobalAdminUser(): Promise { throw e; } } - _globalAdminUser = await new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()).createClientWithCredentials(USERNAME, USERNAME); + _globalAdminUser = await new PantalaimonClient(homeserver, new MemoryStorageProvider()).createClientWithCredentials(USERNAME, USERNAME); } return _globalAdminUser; } @@ -152,8 +151,8 @@ async function getGlobalAdminUser(): Promise { * Disable ratelimiting for this user in Synapse. * @param userId The user to disable ratelimiting for, has to include both the server part and local part. */ -export async function overrideRatelimitForUser(userId: string) { - await (await getGlobalAdminUser()).doRequest("POST", `/_synapse/admin/v1/users/${userId}/override_ratelimit`, null, { +export async function overrideRatelimitForUser(homeserver: string, userId: string) { + await (await getGlobalAdminUser(homeserver)).doRequest("POST", `/_synapse/admin/v1/users/${userId}/override_ratelimit`, null, { "messages_per_second": 0, "burst_count": 0 }); @@ -163,8 +162,8 @@ export async function overrideRatelimitForUser(userId: string) { * Put back the default ratelimiting for this user in Synapse. * @param userId The user to use default ratelimiting for, has to include both the server part and local part. */ -export async function resetRatelimitForUser(userId: string) { - await (await getGlobalAdminUser()).doRequest("DELETE", `/_synapse/admin/v1/users/${userId}/override_ratelimit`, null); +export async function resetRatelimitForUser(homeserver: string, userId: string) { + await (await getGlobalAdminUser(homeserver)).doRequest("DELETE", `/_synapse/admin/v1/users/${userId}/override_ratelimit`, null); } diff --git a/test/integration/commands/makedminCommandTest.ts b/test/integration/commands/makedminCommandTest.ts index 115de56..51be7ad 100644 --- a/test/integration/commands/makedminCommandTest.ts +++ b/test/integration/commands/makedminCommandTest.ts @@ -1,6 +1,5 @@ import { strict as assert } from "assert"; -import config from "../../../src/config"; import { newTestUser } from "../clientHelper"; import { PowerLevelAction } from "matrix-bot-sdk/lib/models/PowerLevelAction"; import { LogService } from "matrix-bot-sdk"; @@ -16,20 +15,20 @@ describe("Test: The make admin command", function () { it('Mjölnir make the bot self room administrator', async function () { this.timeout(90000); - if (!config.admin?.enableMakeRoomAdminCommand) { + if (!this.config.admin?.enableMakeRoomAdminCommand) { done(); } - const mjolnir = config.RUNTIME.client!; + const mjolnir = this.config.RUNTIME.client!; const mjolnirUserId = await mjolnir.getUserId(); - const moderator = await newTestUser({ name: { contains: "moderator" } }); - const userA = await newTestUser({ name: { contains: "a" } }); + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + const userA = await newTestUser(this.config.homeserverUrl, { name: { contains: "a" } }); const userAId = await userA.getUserId(); this.moderator = moderator; this.userA = userA; let powerLevels: any; - await moderator.joinRoom(config.managementRoom); - LogService.debug("makeadminTest", `Joining managementRoom: ${config.managementRoom}`); + await moderator.joinRoom(this.config.managementRoom); + LogService.debug("makeadminTest", `Joining managementRoom: ${this.config.managementRoom}`); let targetRoom = await moderator.createRoom({ invite: [mjolnirUserId], preset: "public_chat" }); LogService.debug("makeadminTest", `moderator creating targetRoom: ${targetRoom}; and inviting ${mjolnirUserId}`); await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}` }); @@ -57,14 +56,14 @@ describe("Test: The make admin command", function () { it('Mjölnir make the tester room administrator', async function () { this.timeout(90000); - if (!config.admin?.enableMakeRoomAdminCommand) { + if (!this.config.admin?.enableMakeRoomAdminCommand) { done(); } - const mjolnir = config.RUNTIME.client!; - const moderator = await newTestUser({ name: { contains: "moderator" } }); - const userA = await newTestUser({ name: { contains: "a" } }); - const userB = await newTestUser({ name: { contains: "b" } }); - const userC = await newTestUser({ name: { contains: "c" } }); + const mjolnir = this.config.RUNTIME.client!; + const moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); + const userA = await newTestUser(this.config.homeserverUrl, { name: { contains: "a" } }); + const userB = await newTestUser(this.config.homeserverUrl, { name: { contains: "b" } }); + const userC = await newTestUser(this.config.homeserverUrl, { name: { contains: "c" } }); const userBId = await userB.getUserId(); const userCId = await userC.getUserId(); this.moderator = moderator; diff --git a/test/integration/commands/redactCommandTest.ts b/test/integration/commands/redactCommandTest.ts index 1c02cb9..8ddc913 100644 --- a/test/integration/commands/redactCommandTest.ts +++ b/test/integration/commands/redactCommandTest.ts @@ -1,6 +1,5 @@ import { strict as assert } from "assert"; -import config from "../../../src/config"; import { newTestUser } from "../clientHelper"; import { getMessagesByUserIn } from "../../../src/utils"; import { LogService } from "matrix-bot-sdk"; @@ -13,19 +12,19 @@ import { getFirstReaction } from "./commandUtils"; it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id and a room id.', async function() { this.timeout(60000); // Create a few users and a room. - let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } }); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer-needs-redacting" } }); let badUserId = await badUser.getUserId(); - const mjolnir = config.RUNTIME.client! + const mjolnir = this.config.RUNTIME.client! let mjolnirUserId = await mjolnir.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" } }); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - await moderator.joinRoom(config.managementRoom); + await moderator.joinRoom(this.config.managementRoom); let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100); await badUser.joinRoom(targetRoom); moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}`}); - LogService.debug("redactionTest", `targetRoom: ${targetRoom}, managementRoom: ${config.managementRoom}`); + LogService.debug("redactionTest", `targetRoom: ${targetRoom}, managementRoom: ${this.config.managementRoom}`); // Sandwich irrelevant messages in bad messages. await badUser.sendMessage(targetRoom, {msgtype: 'm.text', body: "Very Bad Stuff"}); await Promise.all([...Array(50).keys()].map((i) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`}))); @@ -58,13 +57,13 @@ import { getFirstReaction } from "./commandUtils"; it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id in multiple rooms.', async function() { this.timeout(60000); // Create a few users and a room. - let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } }); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer-needs-redacting" } }); let badUserId = await badUser.getUserId(); - const mjolnir = config.RUNTIME.client! + const mjolnir = this.config.RUNTIME.client! let mjolnirUserId = await mjolnir.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" } }); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - await moderator.joinRoom(config.managementRoom); + await moderator.joinRoom(this.config.managementRoom); let targetRooms: string[] = []; for (let i = 0; i < 5; i++) { let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); @@ -107,12 +106,12 @@ import { getFirstReaction } from "./commandUtils"; it("Redacts a single event when instructed to.", async function () { this.timeout(60000); // Create a few users and a room. - let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } }); - const mjolnir = config.RUNTIME.client! + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer-needs-redacting" } }); + const mjolnir = this.config.RUNTIME.client! let mjolnirUserId = await mjolnir.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" } }); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); this.moderator = moderator; - await moderator.joinRoom(config.managementRoom); + await moderator.joinRoom(this.config.managementRoom); let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100); await badUser.joinRoom(targetRoom); diff --git a/test/integration/commands/shutdownCommandTest.ts b/test/integration/commands/shutdownCommandTest.ts index df24376..0ff5ea8 100644 --- a/test/integration/commands/shutdownCommandTest.ts +++ b/test/integration/commands/shutdownCommandTest.ts @@ -5,7 +5,7 @@ import { newTestUser } from "../clientHelper"; describe("Test: shutdown command", function() { let client; this.beforeEach(async function () { - client = await newTestUser({ name: { contains: "shutdown-command" }}); + client = await newTestUser(this.config.homeserverUrl, { name: { contains: "shutdown-command" }}); await client.start(); }) this.afterEach(async function () { diff --git a/test/integration/detectFederationLagTest.ts b/test/integration/detectFederationLagTest.ts index e679f23..c8711d6 100644 --- a/test/integration/detectFederationLagTest.ts +++ b/test/integration/detectFederationLagTest.ts @@ -28,7 +28,7 @@ describe("Test: DetectFederationLag protection", function() { await this.mjolnir.enableProtection("DetectFederationLag"); // Setup a moderator. - this.moderator = await newTestUser({ name: { contains: "moderator" } }); + this.moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); await this.moderator.joinRoom(this.mjolnir.managementRoomId); const SETTINGS = { diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts index a81d6f6..d1975af 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -1,4 +1,4 @@ -import config from "../../src/config"; +import { read as configRead } from "../../src/config"; import { makeMjolnir, teardownManagementRoom } from "./mjolnirSetupUtils"; // When Mjolnir starts (src/index.ts) it clobbers the config by resolving the management room @@ -12,9 +12,10 @@ export const mochaHooks = { console.error("---- entering test", JSON.stringify(this.currentTest.title)); // Makes MatrixClient error logs a bit easier to parse. console.log("mochaHooks.beforeEach"); // Sometimes it takes a little longer to register users. - this.timeout(10000) + this.timeout(10000); + const config = this.config = configRead(); this.managementRoomAlias = config.managementRoom; - this.mjolnir = await makeMjolnir(); + this.mjolnir = await makeMjolnir(config); config.RUNTIME.client = this.mjolnir.client; await Promise.all([ this.mjolnir.client.setAccountData('org.matrix.mjolnir.protected_rooms', { rooms: [] }), @@ -33,7 +34,7 @@ export const mochaHooks = { this.mjolnir.client.setAccountData('org.matrix.mjolnir.watched_lists', { references: [] }), ]); // remove alias from management room and leave it. - await teardownManagementRoom(this.mjolnir.client, this.mjolnir.managementRoomId, config.managementRoom); + await teardownManagementRoom(this.mjolnir.client, this.mjolnir.managementRoomId, this.managementRoomAlias); console.error("---- completed test", JSON.stringify(this.currentTest.title), "\n\n"); // Makes MatrixClient error logs a bit easier to parse. } ] diff --git a/test/integration/helloTest.ts b/test/integration/helloTest.ts index 4e3c055..3de2bb2 100644 --- a/test/integration/helloTest.ts +++ b/test/integration/helloTest.ts @@ -4,7 +4,7 @@ import { newTestUser, noticeListener } from "./clientHelper" describe("Test: !help command", function() { let client; this.beforeEach(async function () { - client = await newTestUser({ name: { contains: "-" }});; + client = await newTestUser(this.config.homeserverUrl, { name: { contains: "-" }});; await client.start(); }) this.afterEach(async function () { @@ -13,7 +13,7 @@ describe("Test: !help command", function() { it('Mjolnir responded to !mjolnir help', async function() { this.timeout(30000); // send a messgage - await client.joinRoom(config.managementRoom); + await client.joinRoom(this.config.managementRoom); // listener for getting the event reply let reply = new Promise((resolve, reject) => { client.on('room.message', noticeListener(this.mjolnir.managementRoomId, (event) => { diff --git a/test/integration/manualLaunchScript.ts b/test/integration/manualLaunchScript.ts index aff4440..eee1f09 100644 --- a/test/integration/manualLaunchScript.ts +++ b/test/integration/manualLaunchScript.ts @@ -3,8 +3,10 @@ */ import { makeMjolnir } from "./mjolnirSetupUtils"; +import { read as configRead } from '../../src/config'; (async () => { - let mjolnir = await makeMjolnir(); + const config = configRead(); + let mjolnir = await makeMjolnir(config); await mjolnir.start(); })(); diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index 2a4939b..93ce6b8 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -22,9 +22,9 @@ import { RichConsoleLogger } from "matrix-bot-sdk"; import { Mjolnir} from '../../src/Mjolnir'; -import config from "../../src/config"; import { overrideRatelimitForUser, registerUser } from "./clientHelper"; import { patchMatrixClient } from "../../src/utils"; +import { IConfig } from "../../src/config"; /** * Ensures that a room exists with the alias, if it does not exist we create it. @@ -48,9 +48,9 @@ export async function ensureAliasedRoomExists(client: MatrixClient, alias: strin } } -async function configureMjolnir() { +async function configureMjolnir(config: IConfig) { try { - await registerUser(config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true) + await registerUser(config.homeserverUrl, config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true) } catch (e) { if (e?.body?.errcode === 'M_USER_IN_USE') { console.log(`${config.pantalaimon.username} already registered, skipping`); @@ -72,17 +72,17 @@ let globalMjolnir: Mjolnir | null; /** * Return a test instance of Mjolnir. */ -export async function makeMjolnir(): Promise { - await configureMjolnir(); +export async function makeMjolnir(config: IConfig): Promise { + await configureMjolnir(config); LogService.setLogger(new RichConsoleLogger()); LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); LogService.info("test/mjolnirSetupUtils", "Starting bot..."); const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()); const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password); - await overrideRatelimitForUser(await client.getUserId()); + await overrideRatelimitForUser(config.homeserverUrl, await client.getUserId()); patchMatrixClient(); await ensureAliasedRoomExists(client, config.managementRoom); - let mj = await Mjolnir.setupMjolnirFromConfig(client); + let mj = await Mjolnir.setupMjolnirFromConfig(client, config); globalClient = client; globalMjolnir = mj; return mj; diff --git a/test/integration/policyConsumptionTest.ts b/test/integration/policyConsumptionTest.ts index fd343ad..3b3028e 100644 --- a/test/integration/policyConsumptionTest.ts +++ b/test/integration/policyConsumptionTest.ts @@ -1,6 +1,7 @@ import { strict as assert } from "assert"; import { newTestUser } from "./clientHelper"; +import { Mjolnir } from "../../src/Mjolnir"; import config from "../../src/config"; import { getRequestFn, LogService, MatrixClient } from "matrix-bot-sdk"; import { createBanList, getFirstReaction } from "./commands/commandUtils"; @@ -8,9 +9,9 @@ import { createBanList, getFirstReaction } from "./commands/commandUtils"; /** * Get a copy of the rules from the ruleserver. */ -async function currentRules(): Promise<{ start: object, stop: object, since: string }> { +async function currentRules(mjolnir: Mjolnir): Promise<{ start: object, stop: object, since: string }> { return await new Promise((resolve, reject) => getRequestFn()({ - uri: `http://${config.web.address}:${config.web.port}/api/1/ruleserver/updates/`, + uri: `http://${mjolnir.config.web.address}:${mjolnir.config.web.port}/api/1/ruleserver/updates/`, method: "GET" }, (error, response, body) => { if (error) { @@ -25,8 +26,8 @@ async function currentRules(): Promise<{ start: object, stop: object, since: str * Wait for the rules to change as a result of the thunk. The returned promise will resolve when the rules being served have changed. * @param thunk Should cause the rules the RuleServer is serving to change some way. */ -async function waitForRuleChange(thunk): Promise { - const initialRules = await currentRules(); +async function waitForRuleChange(mjolnir: Mjolnir, thunk): Promise { + const initialRules = await currentRules(mjolnir); let rules = initialRules; // We use JSON.stringify like this so that it is pretty printed in the log and human readable. LogService.debug('policyConsumptionTest', `Rules before we wait for them to change: ${JSON.stringify(rules, null, 2)}`); @@ -35,7 +36,7 @@ async function waitForRuleChange(thunk): Promise { await new Promise(resolve => { setTimeout(resolve, 500); }) - rules = await currentRules(); + rules = await currentRules(mjolnir); }; // The problem is, we have no idea how long a consumer will take to process the changed rules. // We know the pull peroid is 1 second though. @@ -47,9 +48,9 @@ async function waitForRuleChange(thunk): Promise { describe("Test: that policy lists are consumed by the associated synapse module", function () { this.afterEach(async function () { - if(config.web.ruleServer.enabled) { + if(this.config.web.ruleServer.enabled) { this.timeout(5000) - LogService.debug('policyConsumptionTest', `Rules at end of test ${JSON.stringify(await currentRules(), null, 2)}`); + LogService.debug('policyConsumptionTest', `Rules at end of test ${JSON.stringify(await currentRules(this.mjolnir), null, 2)}`); const mjolnir = config.RUNTIME.client!; // Clear any state associated with the account. await mjolnir.setAccountData('org.matrix.mjolnir.watched_lists', { @@ -58,23 +59,23 @@ describe("Test: that policy lists are consumed by the associated synapse module" } }) this.beforeAll(async function() { - if (!config.web.ruleServer.enabled) { + if (!this.config.web.ruleServer.enabled) { LogService.warn("policyConsumptionTest", "Skipping policy consumption test because the ruleServer is not enabled") this.skip(); } }) this.beforeEach(async function () { this.timeout(1000); - const mjolnir = config.RUNTIME.client!; + const mjolnir = this.config.RUNTIME.client!; }) it('blocks users in antispam when they are banned from sending messages and invites serverwide.', async function() { this.timeout(20000); // Create a few users and a room. - let badUser = await newTestUser({ name: { contains: "spammer" }}); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer" }}); let badUserId = await badUser.getUserId(); const mjolnir = config.RUNTIME.client! let mjolnirUserId = await mjolnir.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" }}); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }}); this.moderator = moderator; await moderator.joinRoom(this.mjolnir.managementRoomId); let unprotectedRoom = await badUser.createRoom({ invite: [await moderator.getUserId()]}); @@ -84,7 +85,7 @@ describe("Test: that policy lists are consumed by the associated synapse module" const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator); await badUser.sendMessage(unprotectedRoom, {msgtype: 'm.text', body: 'Something bad and mean'}); - await waitForRuleChange(async () => { + await waitForRuleChange(this.config.web.address, this.mjolnir.config.web.port, async () => { await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir ban ${banList} ${badUserId}` }); }); @@ -95,7 +96,7 @@ describe("Test: that policy lists are consumed by the associated synapse module" assert.ok(await moderator.sendMessage(unprotectedRoom, { msgtype: 'm.text', body: 'test'}), 'They should be able to send messages still too.'); // Test we can remove the rules. - await waitForRuleChange(async () => { + await waitForRuleChange(this.config.web.address, this.mjolnir.config.web.port, async () => { await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unban ${banList} ${badUserId}` }); }); @@ -105,15 +106,15 @@ describe("Test: that policy lists are consumed by the associated synapse module" }) it('Test: Cannot send message to a room that is listed in a policy list and cannot invite a user to the room either', async function () { this.timeout(20000); - let badUser = await newTestUser({ name: { contains: "spammer" }}); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer" }}); const mjolnir = config.RUNTIME.client! - let moderator = await newTestUser({ name: { contains: "moderator" }}); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }}); await moderator.joinRoom(this.mjolnir.managementRoomId); const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator); let badRoom = await badUser.createRoom(); let unrelatedRoom = await badUser.createRoom(); await badUser.sendMessage(badRoom, {msgtype: 'm.text', body: "Very Bad Stuff in this room"}); - await waitForRuleChange(async () => { + await waitForRuleChange(this.config.web.address, this.mjolnir.config.web.port, async () => { await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir ban ${banList} ${badRoom}` }); }); @@ -123,7 +124,7 @@ describe("Test: that policy lists are consumed by the associated synapse module" assert.ok(await badUser.sendMessage(unrelatedRoom, { msgtype: 'm.text.', body: 'hey'}), 'should be able to send messages to unrelated room'); assert.ok(await badUser.inviteUser(await moderator.getUserId(), unrelatedRoom), 'They should still be able to invite to other rooms though'); // Test we can remove these rules. - await waitForRuleChange(async () => { + await waitForRuleChange(this.config.web.address, this.mjolnir.config.web.port, async () => { await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unban ${banList} ${badRoom}` }); }); @@ -135,19 +136,19 @@ describe("Test: that policy lists are consumed by the associated synapse module" it('Test: When a list becomes unwatched, the associated policies are stopped.', async function () { this.timeout(20000); const mjolnir = config.RUNTIME.client! - let moderator = await newTestUser({ name: { contains: "moderator" }}); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }}); await moderator.joinRoom(this.mjolnir.managementRoomId); const banList = await createBanList(this.mjolnir.managementRoomId, mjolnir, moderator); let targetRoom = await moderator.createRoom(); await moderator.sendMessage(targetRoom, {msgtype: 'm.text', body: "Fluffy Foxes."}); - await waitForRuleChange(async () => { + await waitForRuleChange(this.config.web.address, this.mjolnir.config.web.port, async () => { await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir ban ${banList} ${targetRoom}` }); }); }); await assert.rejects(moderator.sendMessage(targetRoom, { msgtype: 'm.text', body: 'test'}), 'should not be able to send messages to a room which is listed.'); - await waitForRuleChange(async () => { + await waitForRuleChange(this.config.web.address, this.mjolnir.config.web.port, async () => { await getFirstReaction(mjolnir, this.mjolnir.managementRoomId, '✅', async () => { return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir unwatch #${banList}:localhost:9999` }); }); diff --git a/test/integration/protectionSettingsTest.ts b/test/integration/protectionSettingsTest.ts index 2942cd6..0e38f13 100644 --- a/test/integration/protectionSettingsTest.ts +++ b/test/integration/protectionSettingsTest.ts @@ -1,6 +1,5 @@ import { strict as assert } from "assert"; -import config from "../../src/config"; import { Mjolnir } from "../../src/Mjolnir"; import { IProtection } from "../../src/protections/IProtection"; import { PROTECTIONS } from "../../src/protections/protections"; @@ -12,7 +11,7 @@ import { matrixClient, mjolnir } from "./mjolnirSetupUtils"; describe("Test: Protection settings", function() { let client; this.beforeEach(async function () { - client = await newTestUser({ name: { contains: "protection-settings" }}); + client = await newTestUser(this.config.homeserverUrl, { name: { contains: "protection-settings" }}); await client.start(); }) this.afterEach(async function () { @@ -59,7 +58,7 @@ describe("Test: Protection settings", function() { }); it("Mjolnir responds to !set correctly", async function() { this.timeout(20000); - await client.joinRoom(config.managementRoom); + await client.joinRoom(this.config.managementRoom); await this.mjolnir.registerProtection(new class implements IProtection { name = "JY2TPN"; @@ -84,7 +83,7 @@ describe("Test: Protection settings", function() { }); it("Mjolnir adds a value to a list setting", async function() { this.timeout(20000); - await client.joinRoom(config.managementRoom); + await client.joinRoom(this.config.managementRoom); await this.mjolnir.registerProtection(new class implements IProtection { name = "r33XyT"; @@ -108,7 +107,7 @@ describe("Test: Protection settings", function() { }); it("Mjolnir removes a value from a list setting", async function() { this.timeout(20000); - await client.joinRoom(config.managementRoom); + await client.joinRoom(this.config.managementRoom); await this.mjolnir.registerProtection(new class implements IProtection { name = "oXzT0E"; @@ -133,7 +132,7 @@ describe("Test: Protection settings", function() { }); it("Mjolnir will change a protection setting in-place", async function() { this.timeout(20000); - await client.joinRoom(config.managementRoom); + await client.joinRoom(this.config.managementRoom); await this.mjolnir.registerProtection(new class implements IProtection { name = "d0sNrt"; diff --git a/test/integration/reportPollingTest.ts b/test/integration/reportPollingTest.ts index 4e604df..a749bf0 100644 --- a/test/integration/reportPollingTest.ts +++ b/test/integration/reportPollingTest.ts @@ -5,7 +5,7 @@ import { newTestUser } from "./clientHelper"; describe("Test: Report polling", function() { let client; this.beforeEach(async function () { - client = await newTestUser({ name: { contains: "protection-settings" }}); + client = await newTestUser(this.config.homeserverUrl, { name: { contains: "protection-settings" }}); }) it("Mjolnir correctly retrieves a report from synapse", async function() { this.timeout(40000); diff --git a/test/integration/roomMembersTest.ts b/test/integration/roomMembersTest.ts index a4017f5..e5730d7 100644 --- a/test/integration/roomMembersTest.ts +++ b/test/integration/roomMembersTest.ts @@ -257,7 +257,7 @@ describe("Test: Testing RoomMemberManager", function() { const start = new Date(Date.now() - 10_000); // Setup a moderator. - this.moderator = await newTestUser({ name: { contains: "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); @@ -265,7 +265,7 @@ describe("Test: Testing RoomMemberManager", function() { this.users = []; const SAMPLE_SIZE = 10; for (let i = 0; i < SAMPLE_SIZE; ++i) { - this.users.push(await newTestUser({ name: { contains: `user_${i}_room_member_test` } })); + this.users.push(await newTestUser(this.config.homeserverUrl, { name: { contains: `user_${i}_room_member_test` } })); } const userIds = []; for (let client of this.users) { @@ -380,7 +380,7 @@ describe("Test: Testing RoomMemberManager", function() { const start = new Date(Date.now() - 10_000); // Setup a moderator. - this.moderator = await newTestUser({ name: { contains: "moderator" } }); + this.moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" } }); await this.moderator.joinRoom(this.mjolnir.managementRoomId); // Create a few users. @@ -388,8 +388,8 @@ describe("Test: Testing RoomMemberManager", function() { this.badUsers = []; const SAMPLE_SIZE = 10; for (let i = 0; i < SAMPLE_SIZE; ++i) { - this.goodUsers.push(await newTestUser({ name: { contains: `good_user_${i}_room_member_test` } })); - this.badUsers.push(await newTestUser({ name: { contains: `bad_user_${i}_room_member_test` } })); + 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[] = []; diff --git a/test/integration/standardConsequenceTest.ts b/test/integration/standardConsequenceTest.ts index a5fcd7a..d7bfe14 100644 --- a/test/integration/standardConsequenceTest.ts +++ b/test/integration/standardConsequenceTest.ts @@ -1,6 +1,5 @@ import { strict as assert } from "assert"; -import config from "../../src/config"; import { Mjolnir } from "../../src/Mjolnir"; import { IProtection } from "../../src/protections/IProtection"; import { newTestUser, noticeListener } from "./clientHelper"; @@ -11,8 +10,8 @@ describe("Test: standard consequences", function() { let badUser; let goodUser; this.beforeEach(async function () { - badUser = await newTestUser({ name: { contains: "standard-consequences" }}); - goodUser = await newTestUser({ name: { contains: "standard-consequences" }}); + badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "standard-consequences" }}); + goodUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "standard-consequences" }}); await badUser.start(); await goodUser.start(); }) diff --git a/test/integration/throttleTest.ts b/test/integration/throttleTest.ts index b2070c3..cec433f 100644 --- a/test/integration/throttleTest.ts +++ b/test/integration/throttleTest.ts @@ -1,11 +1,10 @@ import { strict as assert } from "assert"; -import { newTestUser, overrideRatelimitForUser, resetRatelimitForUser } from "./clientHelper"; +import { newTestUser } from "./clientHelper"; import { getMessagesByUserIn } from "../../src/utils"; -import { getFirstReaction } from "./commands/commandUtils"; describe("Test: throttled users can function with Mjolnir.", function () { it('throttled users survive being throttled by synapse', async function() { - let throttledUser = await newTestUser({ name: { contains: "throttled" }, isThrottled: true }); + let throttledUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "throttled" }, isThrottled: true }); let throttledUserId = await throttledUser.getUserId(); let targetRoom = await throttledUser.createRoom(); // send enough messages to hit the rate limit. @@ -18,58 +17,17 @@ describe("Test: throttled users can function with Mjolnir.", function () { }) }) -describe("Test: Mjolnir can still sync and respond to commands while throttled", function () { - beforeEach(async function() { - await resetRatelimitForUser(await this.mjolnir.client.getUserId()) - }) - afterEach(async function() { - // If a test has a timeout while awaitng on a promise then we never get given control back. - this.moderator?.stop(); - - await overrideRatelimitForUser(await this.mjolnir.client.getUserId()); - }) - - it('Can still perform and respond to a redaction command', async function () { - // Create a few users and a room. - let badUser = await newTestUser({ name: { contains: "spammer-needs-redacting" } }); - let badUserId = await badUser.getUserId(); - const mjolnir = this.mjolnir.client; - let mjolnirUserId = await mjolnir.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" } }); - this.moderator = moderator; - await moderator.joinRoom(this.mjolnir.managementRoomId); - let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId(), mjolnirUserId]}); - await moderator.setUserPowerLevel(mjolnirUserId, targetRoom, 100); - await badUser.joinRoom(targetRoom); - - // Give Mjolnir some work to do and some messages to sync through. - await Promise.all([...Array(25).keys()].map((i) => moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`}))); - await Promise.all([...Array(25).keys()].map(_ => moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: '!mjolnir status'}))); - - await moderator.sendMessage(this.mjolnir.managementRoomId, {msgtype: 'm.text', body: `!mjolnir rooms add ${targetRoom}`}); - - await Promise.all([...Array(25).keys()].map((i) => badUser.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Bad Message #${i}`}))); - - try { - await moderator.start(); - await getFirstReaction(moderator, this.mjolnir.managementRoomId, '✅', async () => { - return await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir redact ${badUserId} ${targetRoom}` }); - }); - } finally { - moderator.stop(); - } - - let count = 0; - await getMessagesByUserIn(moderator, badUserId, targetRoom, 1000, function(events) { - count += events.length - events.map(e => { - if (e.type === 'm.room.member') { - assert.equal(Object.keys(e.content).length, 1, "Only membership should be left on the membership event when it has been redacted.") - } else if (Object.keys(e.content).length !== 0) { - throw new Error(`This event should have been redacted: ${JSON.stringify(e, null, 2)}`) - } - }) - }); - assert.equal(count, 26, "There should be exactly 26 events from the spammer in this room."); - }) -}) +/** + * We used to have a test here that tested whether Mjolnir was going to carry out a redact order the default limits in a reasonable time scale. + * Now I think that's never going to happen without writing a new algorithm for respecting rate limiting. + * Which is not something there is time for. + * + * https://github.com/matrix-org/synapse/pull/13018 + * + * Synapse rate limits were broken and very permitting so that's why the current hack worked so well. + * Now it is not broken, so our rate limit handling is. + * + * https://github.com/matrix-org/mjolnir/commit/b850e4554c6cbc9456e23ab1a92ede547d044241 + * + * Honestly I don't think we can expect anyone to be able to use Mjolnir under default rate limits. + */ diff --git a/test/integration/timelinePaginationTest.ts b/test/integration/timelinePaginationTest.ts index 3081718..0df691f 100644 --- a/test/integration/timelinePaginationTest.ts +++ b/test/integration/timelinePaginationTest.ts @@ -10,9 +10,9 @@ describe("Test: timeline pagination", function () { it('does not paginate across the entire room history while backfilling.', async function() { this.timeout(60000); // Create a few users and a room. - let badUser = await newTestUser({ name: { contains: "spammer" }}); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer" }}); let badUserId = await badUser.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" }}); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }}); let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId()]}); await badUser.joinRoom(targetRoom); @@ -39,9 +39,9 @@ describe("Test: timeline pagination", function () { }) it('does not call the callback with an empty array when there are no relevant events', async function() { this.timeout(60000); - let badUser = await newTestUser({ name: { contains: "spammer" }}); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer" }}); let badUserId = await badUser.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" }}); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }}); let targetRoom = await moderator.createRoom(); // send some irrelevant messages await Promise.all([...Array(200).keys()].map((i) => moderator.sendMessage(targetRoom, {msgtype: 'm.text.', body: `Irrelevant Message #${i}`}))); @@ -54,9 +54,9 @@ describe("Test: timeline pagination", function () { }) it("The limit provided is respected", async function() { this.timeout(60000); - let badUser = await newTestUser({ name: { contains: "spammer" }}); + let badUser = await newTestUser(this.config.homeserverUrl, { name: { contains: "spammer" }}); let badUserId = await badUser.getUserId(); - let moderator = await newTestUser({ name: { contains: "moderator" }}); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }}); let targetRoom = await moderator.createRoom({ invite: [await badUser.getUserId()]}); await badUser.joinRoom(targetRoom); // send some bad person messages @@ -83,7 +83,7 @@ describe("Test: timeline pagination", function () { }); it("Gives the events to the callback ordered by youngest first (even more important when the limit is reached halfway through a chunk).", async function() { this.timeout(60000); - let moderator = await newTestUser({ name: { contains: "moderator" }}); + let moderator = await newTestUser(this.config.homeserverUrl, { name: { contains: "moderator" }}); let moderatorId = await moderator.getUserId(); let targetRoom = await moderator.createRoom(); for (let i = 0; i < 20; i++) { diff --git a/test/integration/utilsTest.ts b/test/integration/utilsTest.ts index bc9b2db..fa68ea1 100644 --- a/test/integration/utilsTest.ts +++ b/test/integration/utilsTest.ts @@ -8,11 +8,13 @@ describe("Test: utils", function() { it("replaceRoomIdsWithPills correctly turns a room ID in to a pill", async function() { this.timeout(20000); + const managementRoomAlias = this.config.managementRoom; + await this.mjolnir.client.sendStateEvent( this.mjolnir.managementRoomId, "m.room.canonical_alias", "", - { alias: config.managementRoom } + { alias: managementRoomAlias } ); const out = await replaceRoomIdsWithPills( @@ -24,7 +26,7 @@ describe("Test: utils", function() { const ourHomeserver = new UserID(await this.mjolnir.client.getUserId()).domain; assert.equal( out.formatted_body, - `it's fun here in ${config.managementRoom}` + `it's fun here in ${managementRoomAlias}` ); }); }); diff --git a/yarn.lock b/yarn.lock index 21df715..83fba16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -91,11 +91,6 @@ "@types/connect" "*" "@types/node" "*" -"@types/config@0.0.41": - version "0.0.41" - resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.41.tgz#c8237ab09730380801f3643beaefa077ca5f3c28" - integrity sha512-HjXUmIld0gwvyG8MU/17QtLzOyuMX4jbGuijmS9sWsob5xxgZ/hY9cbRCaHIHqTQ3HMLhwS3F8uXq3Bt9zgzHA== - "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -156,6 +151,11 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/js-yaml@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" + integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== + "@types/jsdom@^16.2.11": version "16.2.13" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f" @@ -168,7 +168,7 @@ "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== "@types/mime@^1": version "1.3.2" @@ -253,14 +253,6 @@ accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -368,12 +360,12 @@ argparse@^2.0.1: array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== asn1@~0.2.3: version "0.2.4" @@ -385,7 +377,7 @@ asn1@~0.2.3: assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== astral-regex@^2.0.0: version "2.0.0" @@ -395,12 +387,12 @@ astral-regex@^2.0.0: asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== aws4@^1.8.0: version "1.11.0" @@ -422,7 +414,7 @@ basic-auth@~2.0.1: bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== dependencies: tweetnacl "^0.14.3" @@ -452,22 +444,6 @@ body-parser@1.19.0: raw-body "2.4.0" type-is "~1.6.17" -body-parser@1.19.2: - version "1.19.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e" - integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.8.1" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.9.7" - raw-body "2.4.3" - type-is "~1.6.18" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -501,18 +477,13 @@ buffer-from@^1.0.0, buffer-from@^1.1.0: builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + integrity sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ== bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -526,7 +497,7 @@ camelcase@^6.0.0: caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== chalk@^2.0.0, chalk@^2.3.0: version "2.4.2" @@ -545,10 +516,10 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -586,7 +557,7 @@ color-convert@^2.0.1: color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== color-name@~1.1.4: version "1.1.4" @@ -613,14 +584,7 @@ commandpost@^1.0.0: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -config@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/config/-/config-3.3.6.tgz#b87799db7399cc34988f55379b5f43465b1b065c" - integrity sha512-Hj5916C5HFawjYJat1epbyY2PlAgLpBtDUlr0MxGLgo3p5+7kylyvnRY18PqJHgnNWXcdd0eWDemT7eYWuFgwg== - dependencies: - json5 "^2.1.1" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== content-disposition@0.5.3: version "0.5.3" @@ -629,13 +593,6 @@ content-disposition@0.5.3: dependencies: safe-buffer "5.1.2" -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -644,22 +601,17 @@ content-type@~1.0.4: cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== cookie@0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== -cookie@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== cross-spawn@^7.0.2: version "7.0.3" @@ -695,7 +647,7 @@ cssstyle@^2.3.0: dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== dependencies: assert-plus "^1.0.0" @@ -715,10 +667,10 @@ debug@2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.3.2, debug@^4.0.1, debug@^4.1.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== +debug@4, debug@4.3.3, debug@^4.0.1, debug@^4.1.1: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" @@ -745,12 +697,12 @@ deepmerge@^4.2.2: delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== depd@~2.0.0: version "2.0.0" @@ -760,7 +712,7 @@ depd@~2.0.0: destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== diff-sequences@^27.0.6: version "27.0.6" @@ -785,7 +737,7 @@ diff@^4.0.1: discontinuous-range@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" - integrity sha1-44Mx8IRLukm5qctxx3FYWqsbxlo= + integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== doctrine@^3.0.0: version "3.0.0" @@ -841,7 +793,7 @@ domutils@^2.0.0, domutils@^2.5.2: ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== dependencies: jsbn "~0.1.0" safer-buffer "^2.1.0" @@ -859,7 +811,7 @@ editorconfig@^0.15.0: ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== emoji-regex@^8.0.0: version "8.0.0" @@ -869,7 +821,7 @@ emoji-regex@^8.0.0: encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== enquirer@^2.3.5: version "2.3.6" @@ -891,7 +843,7 @@ escalade@^3.1.1: escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: version "4.0.0" @@ -901,7 +853,7 @@ escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^2.0.0: version "2.0.0" @@ -1037,7 +989,7 @@ esutils@^2.0.2: etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== expect@^27.0.6: version "27.2.4" @@ -1051,7 +1003,7 @@ expect@^27.0.6: jest-message-util "^27.2.4" jest-regex-util "^27.0.6" -express@^4.17: +express@^4.17, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== @@ -1087,42 +1039,6 @@ express@^4.17: utils-merge "1.0.1" vary "~1.1.2" -express@^4.17.1: - version "4.17.3" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" - integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.19.2" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.4.2" - cookie-signature "1.0.6" - debug "2.6.9" - depd "~1.1.2" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "~1.1.2" - fresh "0.5.2" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.9.7" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.17.2" - serve-static "1.14.2" - setprototypeof "1.2.0" - statuses "~1.5.0" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -1131,12 +1047,12 @@ extend@~3.0.2: extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + integrity sha512-6NW8DZ8pWBc5NbGYUiqqccj9dXnuSzilZYqprdKJBZsQodGH9IyUoFOGxIWVDcBzHMb8ET24aqx9p66tZEWZkA== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" @@ -1151,7 +1067,7 @@ fast-json-stable-stringify@^2.0.0: fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== file-entry-cache@^6.0.1: version "6.0.1" @@ -1209,7 +1125,7 @@ flatted@^3.1.0: forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== form-data@^3.0.0: version "3.0.1" @@ -1237,12 +1153,12 @@ forwarded@0.2.0: fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: version "2.3.2" @@ -1257,7 +1173,7 @@ function-bind@^1.1.1: functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== get-caller-file@^2.0.5: version "2.0.5" @@ -1267,7 +1183,7 @@ get-caller-file@^2.0.5: getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== dependencies: assert-plus "^1.0.0" @@ -1283,19 +1199,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.1, glob@^7.1.3: +glob@7.2.0, glob@^7.1.1, glob@^7.1.3: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -1327,7 +1231,7 @@ growl@1.10.5: har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== har-validator@~5.1.3: version "5.1.5" @@ -1340,7 +1244,7 @@ har-validator@~5.1.3: has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== has-flag@^4.0.0: version "4.0.0" @@ -1400,7 +1304,7 @@ html-to-text@^8.0.0: htmlencode@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/htmlencode/-/htmlencode-0.0.4.tgz#f7e2d6afbe18a87a78e63ba3308e753766740e3f" - integrity sha1-9+LWr74YqHp45jujMI51N2Z0Dj8= + integrity sha512-0uDvNVpzj/E2TfvLLyyXhKBRvF1y84aZsyRxRXFsQobnHaL4pcaXk+Y9cnFlvnxrBLeXDNq/VJBD+ngdBgQG1w== htmlparser2@^4.1.0: version "4.1.0" @@ -1433,17 +1337,6 @@ http-errors@1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" - integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.1" - http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" @@ -1467,7 +1360,7 @@ http-proxy-agent@^4.0.1: http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== dependencies: assert-plus "^1.0.0" jsprim "^1.2.2" @@ -1487,9 +1380,9 @@ humanize-duration-ts@^2.1.1: integrity sha512-TibNF2/fkypjAfHdGpWL/dmWUS0G6Qi+3mKyiB6LDCowbMy+PtzbgPTnFMNTOVAJXDau01jYrJ3tFoz5AJSqhA== humanize-duration@^3.27.1: - version "3.27.1" - resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.27.1.tgz#2cd4ea4b03bd92184aee6d90d77a8f3d7628df69" - integrity sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA== + version "3.27.2" + resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.27.2.tgz#4b4e565bec098d22c9a54344e16156d1c649f160" + integrity sha512-A15OmA3FLFRnehvF4ZMocsxTZYvHq4ze7L+AgR1DeHw0xC9vMd4euInY83uqGU9/XXKNnVIEeKc1R8G8nKqtzg== iconv-lite@0.4.24: version "0.4.24" @@ -1514,12 +1407,12 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -1532,7 +1425,7 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3: inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== ipaddr.js@1.9.1: version "1.9.1" @@ -1556,7 +1449,7 @@ is-core-module@^2.2.0: is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^3.0.0: version "3.0.0" @@ -1598,7 +1491,7 @@ is-promise@^2.1.0: is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== is-unicode-supported@^0.1.0: version "0.1.0" @@ -1608,12 +1501,12 @@ is-unicode-supported@^0.1.0: isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== jest-diff@^27.2.4: version "27.2.4" @@ -1683,7 +1576,7 @@ js-yaml@^3.13.1: jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== jsdom@^16.6.0: version "16.7.0" @@ -1728,20 +1621,20 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@^1.0.1: version "1.0.1" @@ -1750,21 +1643,14 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -json5@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" - jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" extsprintf "1.3.0" - json-schema "0.2.3" + json-schema "0.4.0" verror "1.10.0" klona@^2.0.3: @@ -1783,7 +1669,7 @@ levn@^0.4.1: levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== dependencies: prelude-ls "~1.1.2" type-check "~0.3.2" @@ -1798,7 +1684,7 @@ locate-path@^6.0.0: lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== lodash.merge@^4.6.2: version "4.6.2" @@ -1808,7 +1694,7 @@ lodash.merge@^4.6.2: lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== lodash@4, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.7.0: version "4.17.21" @@ -1877,17 +1763,17 @@ matrix-bot-sdk@^0.5.19: media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromatch@^4.0.4: version "4.0.4" @@ -1902,11 +1788,6 @@ mime-db@1.49.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== -mime-db@1.51.0: - version "1.51.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" - integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== - mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.32" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" @@ -1914,13 +1795,6 @@ mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: dependencies: mime-db "1.49.0" -mime-types@~2.1.34: - version "2.1.34" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" - integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== - dependencies: - mime-db "1.51.0" - mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -1931,7 +1805,14 @@ minimalistic-assert@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.0.4, minimatch@^3.0.4: +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -1956,31 +1837,31 @@ mkdirp@^1.0.4: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== mocha@^9.0.1: - version "9.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.2.tgz#93f53175b0f0dc4014bd2d612218fccfcf3534d3" - integrity sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w== + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== dependencies: "@ungap/promise-all-settled" "1.1.2" ansi-colors "4.1.1" browser-stdout "1.3.1" - chokidar "3.5.2" - debug "4.3.2" + chokidar "3.5.3" + debug "4.3.3" diff "5.0.0" escape-string-regexp "4.0.0" find-up "5.0.0" - glob "7.1.7" + glob "7.2.0" growl "1.10.5" he "1.2.0" js-yaml "4.1.0" log-symbols "4.1.0" - minimatch "3.0.4" + minimatch "4.2.1" ms "2.1.3" - nanoid "3.1.25" + nanoid "3.3.1" serialize-javascript "6.0.0" strip-json-comments "3.1.1" supports-color "8.1.1" which "2.0.2" - workerpool "6.1.5" + workerpool "6.2.0" yargs "16.2.0" yargs-parser "20.2.4" yargs-unparser "2.0.0" @@ -2004,7 +1885,7 @@ morgan@^1.10.0: ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.1: version "2.1.1" @@ -2026,20 +1907,15 @@ nanocolors@^0.2.2: resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.2.12.tgz#4d05932e70116078673ea4cc6699a1c56cc77777" integrity sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug== -nanoid@3.1.25: - version "3.1.25" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" - integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== - -nanoid@^3.1.25: - version "3.1.28" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.28.tgz#3c01bac14cb6c5680569014cc65a2f26424c6bd4" - integrity sha512-gSu9VZ2HtmoKYe/lmyPFES5nknFrHa+/DT9muUFWFMi6Jh9E1I7bkvlQ8xxf1Kos9pi9o8lBnIOkatMhKX/YUw== +nanoid@3.3.1, nanoid@^3.1.25: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== nearley@^2.20.1: version "2.20.1" @@ -2056,11 +1932,6 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -2079,7 +1950,7 @@ oauth-sign@~0.9.0: on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== dependencies: ee-first "1.1.1" @@ -2091,7 +1962,7 @@ on-headers@~1.0.2: once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -2148,7 +2019,7 @@ parse-duration@^1.0.2: parse-srcset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" - integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== parse5@6.0.1: version "6.0.1" @@ -2176,7 +2047,7 @@ path-exists@^4.0.0: path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.1.0: version "3.1.1" @@ -2191,12 +2062,12 @@ path-parse@^1.0.6: path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.0" @@ -2206,7 +2077,7 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== postcss@^8.0.2: version "8.3.8" @@ -2225,7 +2096,7 @@ prelude-ls@^1.2.1: prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== pretty-format@^27.2.4: version "27.2.4" @@ -2242,7 +2113,7 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -proxy-addr@~2.0.5, proxy-addr@~2.0.7: +proxy-addr@~2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -2253,7 +2124,7 @@ proxy-addr@~2.0.5, proxy-addr@~2.0.7: pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== psl@^1.1.28, psl@^1.1.33: version "1.8.0" @@ -2270,11 +2141,6 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== -qs@6.9.7: - version "6.9.7" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" - integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== - qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -2283,7 +2149,7 @@ qs@~6.5.2: railroad-diagrams@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" - integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234= + integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== randexp@0.4.6: version "0.4.6" @@ -2315,16 +2181,6 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c" - integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== - dependencies: - bytes "3.1.2" - http-errors "1.8.1" - iconv-lite "0.4.24" - unpipe "1.0.0" - react-is@^17.0.1: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" @@ -2388,7 +2244,7 @@ request@^2.88.2: require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" @@ -2425,7 +2281,7 @@ safe-buffer@5.1.2: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2493,25 +2349,6 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -send@0.17.2: - version "0.17.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" - integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "1.8.1" - mime "1.6.0" - ms "2.1.3" - on-finished "~2.3.0" - range-parser "~1.2.1" - statuses "~1.5.0" - serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -2529,26 +2366,11 @@ serve-static@1.14.1: parseurl "~1.3.3" send "0.17.1" -serve-static@1.14.2: - version "1.14.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" - integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.2" - setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2569,7 +2391,7 @@ shell-quote@^1.7.3: sigmund@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + integrity sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g== slash@^3.0.0: version "3.0.0" @@ -2606,7 +2428,7 @@ source-map@^0.6.0, source-map@~0.6.1: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== sshpk@^1.7.0: version "1.16.1" @@ -2633,17 +2455,17 @@ stack-utils@^2.0.3: "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + integrity sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g== steno@^0.4.1: version "0.4.4" resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" - integrity sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs= + integrity sha512-EEHMVYHNXFHfGtgjNITnka0aHhiAlo93F7z2/Pwd+g0teG9CnM3JIINM7hVVB5/rhw9voufD7Wukwgtw2uqh6w== dependencies: graceful-fs "^4.1.3" @@ -2666,7 +2488,7 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" @@ -2714,7 +2536,7 @@ table@^6.0.9: text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== to-regex-range@^5.0.1: version "5.0.1" @@ -2728,11 +2550,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -2824,14 +2641,14 @@ tsutils@^2.29.0: tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -2843,7 +2660,7 @@ type-check@^0.4.0, type-check@~0.4.0: type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== dependencies: prelude-ls "~1.1.2" @@ -2881,7 +2698,7 @@ universalify@^0.1.2: unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== uri-js@^4.2.2: version "4.4.1" @@ -2893,7 +2710,7 @@ uri-js@^4.2.2: utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== uuid@^3.3.2: version "3.4.0" @@ -2908,12 +2725,12 @@ v8-compile-cache@^2.0.3: vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== dependencies: assert-plus "^1.0.0" core-util-is "1.0.2" @@ -2976,10 +2793,10 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -workerpool@6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581" - integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw== +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== wrap-ansi@^7.0.0: version "7.0.0" @@ -2993,7 +2810,7 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== ws@^7.4.6: version "7.5.5" @@ -3018,13 +2835,18 @@ y18n@^5.0.5: yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec" + integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw== + yargs-parser@20.2.4: version "20.2.4" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" @@ -3061,7 +2883,7 @@ yargs@16.2.0: yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" - integrity sha1-5a2ryKz0CPY4X8dklWhMiOavaJo= + integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== yocto-queue@^0.1.0: version "0.1.0"