From 26ae55cd244837e54b66514c88e15927052945ac Mon Sep 17 00:00:00 2001 From: David Teller Date: Mon, 7 Mar 2022 11:34:25 +0100 Subject: [PATCH] A command to show when users in a given room have joined (#225) --- package.json | 1 + src/Mjolnir.ts | 15 + src/RoomMembers.ts | 270 ++++++++++++++++ src/commands/StatusCommand.ts | 80 ++++- src/protections/protections.ts | 2 +- test/integration/commands/commandUtils.ts | 3 + test/integration/roomMembersTest.ts | 377 ++++++++++++++++++++++ yarn.lock | 335 ++++++++++++++----- 8 files changed, 1002 insertions(+), 81 deletions(-) create mode 100644 src/RoomMembers.ts create mode 100644 test/integration/roomMembersTest.ts diff --git a/package.json b/package.json index 2ab9a2a..2fdf65e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "config": "^3.3.6", "express": "^4.17", "html-to-text": "^8.0.0", + "humanize-duration-ts": "^2.1.1", "js-yaml": "^4.1.0", "jsdom": "^16.6.0", "matrix-bot-sdk": "^0.5.19", diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts index 539df7b..0c00002 100644 --- a/src/Mjolnir.ts +++ b/src/Mjolnir.ts @@ -45,6 +45,7 @@ import { ReportManager } from "./report/ReportManager"; import { WebAPIs } from "./webapis/WebAPIs"; import { replaceRoomIdsWithPills } from "./utils"; import RuleServer from "./models/RuleServer"; +import { RoomMemberManager } from "./RoomMembers"; const levelToFn = { [LogLevel.DEBUG.toString()]: LogService.debug, @@ -67,6 +68,7 @@ export class Mjolnir { private displayName: string; private localpart: string; private currentState: string = STATE_NOT_STARTED; + public readonly roomJoins: RoomMemberManager; public protections = new Map(); /** * This is for users who are not listed on a watchlist, @@ -236,6 +238,9 @@ export class Mjolnir { const reportManager = new ReportManager(this); reportManager.on("report.new", this.handleReport); this.webapis = new WebAPIs(reportManager, this.ruleServer); + + // Setup join/leave listener + this.roomJoins = new RoomMemberManager(this.client); } public get lists(): BanList[] { @@ -363,6 +368,7 @@ export class Mjolnir { public async addProtectedRoom(roomId: string) { this.protectedRooms[roomId] = Permalinks.forRoom(roomId); + this.roomJoins.addRoom(roomId); const unprotectedIdx = this.knownUnprotectedRooms.indexOf(roomId); if (unprotectedIdx >= 0) this.knownUnprotectedRooms.splice(unprotectedIdx, 1); @@ -382,6 +388,7 @@ export class Mjolnir { public async removeProtectedRoom(roomId: string) { delete this.protectedRooms[roomId]; + this.roomJoins.removeRoom(roomId); const idx = this.explicitlyProtectedRoomIds.indexOf(roomId); if (idx >= 0) this.explicitlyProtectedRoomIds.splice(idx, 1); @@ -400,12 +407,20 @@ export class Mjolnir { if (!config.protectAllJoinedRooms) return; const joinedRoomIds = (await this.client.getJoinedRooms()).filter(r => r !== this.managementRoomId); + const oldRoomIdsSet = new Set(this.protectedJoinedRoomIds); + const joinedRoomIdsSet = new Set(joinedRoomIds); for (const roomId of this.protectedJoinedRoomIds) { delete this.protectedRooms[roomId]; + if (!joinedRoomIdsSet.has(roomId)) { + this.roomJoins.removeRoom(roomId); + } } this.protectedJoinedRoomIds = joinedRoomIds; for (const roomId of joinedRoomIds) { this.protectedRooms[roomId] = Permalinks.forRoom(roomId); + if (!oldRoomIdsSet.has(roomId)) { + this.roomJoins.addRoom(roomId); + } } this.applyUnprotectedRooms(); diff --git a/src/RoomMembers.ts b/src/RoomMembers.ts new file mode 100644 index 0000000..921d0cd --- /dev/null +++ b/src/RoomMembers.ts @@ -0,0 +1,270 @@ +import { MatrixClient } from "matrix-bot-sdk"; + +enum Action { + Join, + Leave, + Other +} + +const LEAVE_OR_BAN = ['leave', 'ban']; + +/** + * Storing a join event. + * + * We use `timestamp`: + * - to avoid maintaining tens of thousands of in-memory `Date` objects; + * - to ensure immutability. + */ +export class Join { + constructor( + public readonly userId: string, + public readonly timestamp: number + ) { } +} + +/** + * A data structure maintaining a list of joins since the start of Mjölnir. + * + * This data structure is optimized for lookup up of recent joins. + */ +class RoomMembers { + /** + * The list of recent joins, ranked from oldest to most recent. + * + * Note that a user may show up in both `_joinsByTimestamp` and `_leaves`, in which case + * they have both joined and left recently. Compare the date of the latest + * leave event (in `_leaves`) to the date of the join to determine whether + * the user is still present. + * + * Note that a user may show up more than once in `_joinsByTimestamp` if they have + * left and rejoined. + */ + private _joinsByTimestamp: Join[] = []; + private _joinsByUser: Map = new Map(); + + /** + * The list of recent leaves. + * + * If a user rejoins and leaves again, the latest leave event overwrites + * the oldest. + */ + private _leaves: Map = new Map(); + + /** + * Record a join. + */ + public join(userId: string, timestamp: number) { + this._joinsByTimestamp.push(new Join(userId, timestamp)); + this._joinsByUser.set(userId, timestamp); + } + + /** + * Record a leave. + */ + public leave(userId: string, timestamp: number) { + if (!this._joinsByUser.has(userId)) { + // No need to record a leave for a user we didn't see joining. + return; + } + this._leaves.set(userId, timestamp); + this._joinsByUser.delete(userId); + } + + /** + * Run a cleanup on the data structure. + */ + public cleanup() { + if (this._leaves.size === 0) { + // Nothing to do. + return; + } + this._joinsByTimestamp = this._joinsByTimestamp.filter(join => this.isStillValid(join)); + this._leaves = new Map(); + } + + /** + * Determine whether a `join` is still valid or has been superseded by a `leave`. + * + * @returns true if the `join` is still valid. + */ + private isStillValid(join: Join): boolean { + const leaveTS = this._leaves.get(join.userId); + if (!leaveTS) { + // The user never left. + return true; + } + if (leaveTS > join.timestamp) { + // The user joined, then left, ignore this join. + return false; + } + // The user had left, but this is a more recent re-join. + return true; + } + + /** + * Return a subset of the list of all the members, with their join date. + * + * @param since Only return members who have last joined at least as + * recently as `since`. + * @param max Only return at most `max` numbers. + * @returns A list of up to `max` members joined since `since`, ranked + * from most recent join to oldest join. + */ + public members(since: Date, max: number): Join[] { + const result = []; + const ts = since.getTime(); + // Spurious joins are legal, let's deduplicate them. + const users = new Set(); + for (let i = this._joinsByTimestamp.length - 1; i >= 0; --i) { + if (result.length > max) { + // We have enough entries, let's return immediately. + return result; + } + const join = this._joinsByTimestamp[i]; + if (join.timestamp < ts) { + // We have reached an older entry, everything will be `< since`, + // we won't find any other join to return. + return result; + } + if (this.isStillValid(join) && !users.has(join.userId)) { + // This entry is still valid, we'll need to return it. + result.push(join); + users.add(join.userId); + } + } + // We have reached the startup of Mjölnir. + return result; + } + + /** + * Return the join date of a user. + * + * @returns a `Date` if the user is currently in the room and has joined + * since the start of Mjölnir, `null` otherwise. + */ + public get(userId: string): Date | null { + let ts = this._joinsByUser.get(userId); + if (!ts) { + return null; + } + return new Date(ts); + } +} + +export class RoomMemberManager { + private perRoom: Map = new Map(); + private readonly cbHandleEvent; + constructor(private client: MatrixClient) { + // Listen for join events. + this.cbHandleEvent = this.handleEvent.bind(this); + client.on("room.event", this.cbHandleEvent); + } + + /** + * Start listening to join/leave events in a room. + */ + public addRoom(roomId: string) { + if (this.perRoom.has(roomId)) { + // Nothing to do. + return; + } + this.perRoom.set(roomId, new RoomMembers()); + } + + /** + * Stop listening to join/leave events in a room. + * + * Cleanup any remaining data on join/leave events. + */ + public removeRoom(roomId: string) { + this.perRoom.delete(roomId); + } + + public cleanup(roomId: string) { + this.perRoom.get(roomId)?.cleanup(); + } + + /** + * Dispose of this object. + */ + public dispose() { + this.client.off("room.event", this.cbHandleEvent); + } + + /** + * Return the date at which user `userId` has joined room `roomId`, or `null` if + * that user has joined the room before Mjölnir started watching it. + * + * @param roomId The id of the room we're interested in. + * @param userId The id of the user we're interested in. + * @returns a Date if Mjölnir has witnessed the user joining the room, + * `null` otherwise. The latter may happen either if the user has joined + * the room before Mjölnir or if the user is not currently in the room. + */ + public getUserJoin(user: { roomId: string, userId: string }): Date | null { + const { roomId, userId } = user; + const ts = this.perRoom.get(roomId)?.get(userId) || null; + if (!ts) { + return null; + } + return new Date(ts); + } + + /** + * Get the users in a room, ranked by most recently joined to oldest join. + * + * Only the users who have joined since the start of Mjölnir are returned. + */ + public getUsersInRoom(roomId: string, since: Date, max = 100): Join[] { + const inRoom = this.perRoom.get(roomId); + if (!inRoom) { + return []; + } + return inRoom.members(since, max); + } + + /** + * Record join/leave events. + */ + public async handleEvent(roomId: string, event: any, now?: Date) { + if (event['type'] !== 'm.room.member') { + // Not a join/leave event. + return; + } + + const members = this.perRoom.get(roomId); + if (!members) { + // Not a room we are watching. + return; + } + const userId = event['state_key']; + if (!userId) { + // Ill-formed event. + return; + } + + const userState = event['content']['membership']; + const prevMembership = event['unsigned']?.['prev_content']?.['membership'] || "leave"; + + // We look at the previous membership to filter out profile changes + let action; + if (userState === 'join' && prevMembership !== "join") { + action = Action.Join; + } else if (LEAVE_OR_BAN.includes(userState) && !LEAVE_OR_BAN.includes(prevMembership)) { + action = Action.Leave; + } else { + action = Action.Other; + } + switch (action) { + case Action.Other: + // Nothing to do. + return; + case Action.Join: + members.join(userId, now ? now.getTime() : Date.now()); + break; + case Action.Leave: + members.leave(userId, now ? now.getTime() : Date.now()); + break; + } + } +} \ No newline at end of file diff --git a/src/commands/StatusCommand.ts b/src/commands/StatusCommand.ts index ecb0871..c542234 100644 --- a/src/commands/StatusCommand.ts +++ b/src/commands/StatusCommand.ts @@ -17,6 +17,18 @@ limitations under the License. import { Mjolnir, STATE_CHECKING_PERMISSIONS, STATE_NOT_STARTED, STATE_RUNNING, STATE_SYNCING } from "../Mjolnir"; import { RichReply } from "matrix-bot-sdk"; import { htmlEscape } from "../utils"; +import { default as parseDuration } from "parse-duration"; +import { HumanizeDurationLanguage, HumanizeDuration } from "humanize-duration-ts"; + +// Define a few aliases to simplify parsing durations. + +parseDuration["days"] = parseDuration["day"]; +parseDuration["weeks"] = parseDuration["week"] = parseDuration["wk"]; +parseDuration["months"] = parseDuration["month"]; +parseDuration["years"] = parseDuration["year"]; + +const HUMANIZE_LAG_SERVICE: HumanizeDurationLanguage = new HumanizeDurationLanguage(); +const HUMANIZER: HumanizeDuration = new HumanizeDuration(HUMANIZE_LAG_SERVICE); // !mjolnir export async function execStatusCommand(roomId: string, event: any, mjolnir: Mjolnir, parts: string[]) { @@ -26,9 +38,11 @@ export async function execStatusCommand(roomId: string, event: any, mjolnir: Mjo return showMjolnirStatus(roomId, event, mjolnir); case 'protection': return showProtectionStatus(roomId, event, mjolnir, parts.slice(/* ["protection"] */ 1)); + case 'joins': + return showJoinsStatus(roomId, event, mjolnir, parts.slice(/* ["joins"] */ 1)); default: throw new Error(`Invalid status command: ${htmlEscape(parts[0])}`); - } + } } async function showMjolnirStatus(roomId: string, event: any, mjolnir: Mjolnir) { @@ -104,3 +118,67 @@ async function showProtectionStatus(roomId: string, event: any, mjolnir: Mjolnir reply["msgtype"] = "m.notice"; await mjolnir.client.sendMessage(roomId, reply); } + +/** + * Show the most recent joins to a room. + */ +async function showJoinsStatus(destinationRoomId: string, event: any, mjolnir: Mjolnir, args: string[]) { + const targetRoomAliasOrId = args[0]; + const maxAgeArg = args[1] || "1 day"; + const maxEntriesArg = args[2] = "200"; + const { html, text } = await (async () => { + if (!targetRoomAliasOrId) { + return { + html: "Missing arg: room id", + text: "Missing arg: `room id`" + }; + } + const maxAgeMS = parseDuration(maxAgeArg); + if (!maxAgeMS) { + return { + html: "Invalid duration. Example: 1.5 days or 10 minutes", + text: "Invalid duration. Example: `1.5 days` or `10 minutes`", + } + } + const maxEntries = Number.parseInt(maxEntriesArg, 10); + if (!maxEntries) { + return { + html: "Invalid number of entries. Example: 200", + text: "Invalid number of entries. Example: `200`", + } + } + const minDate = new Date(Date.now() - maxAgeMS); + const HUMANIZER_OPTIONS = { + // Reduce "1 day" => "1day" to simplify working with CSV. + spacer: "", + // Reduce "1 day, 2 hours" => "1.XXX day" to simplify working with CSV. + largest: 1, + }; + const maxAgeHumanReadable = HUMANIZER.humanize(maxAgeMS, HUMANIZER_OPTIONS); + let targetRoomId; + try { + targetRoomId = await mjolnir.client.resolveRoom(targetRoomAliasOrId); + } catch (ex) { + return { + html: `Cannot resolve room ${htmlEscape(targetRoomAliasOrId)}.`, + text: `Cannot resolve room \`${targetRoomAliasOrId}\`.` + } + } + const joins = mjolnir.roomJoins.getUsersInRoom(targetRoomId, minDate, maxEntries); + const htmlFragments = []; + const textFragments = []; + for (let join of joins) { + const durationHumanReadable = HUMANIZER.humanize(Date.now() - join.timestamp, HUMANIZER_OPTIONS); + htmlFragments.push(`
  • ${htmlEscape(join.userId)}: ${durationHumanReadable}
  • `); + textFragments.push(`- ${join.userId}: ${durationHumanReadable}`); + } + return { + html: `${joins.length} recent joins (cut at ${maxAgeHumanReadable} ago / ${maxEntries} entries):
      ${htmlFragments.join()}
    `, + text: `${joins.length} recent joins (cut at ${maxAgeHumanReadable} ago / ${maxEntries} entries):\n${textFragments.join("\n")}` + } + })(); + const reply = RichReply.createFor(destinationRoomId, event, text, html); + reply["msgtype"] = "m.notice"; + return mjolnir.client.sendMessage(destinationRoomId, reply); +} + diff --git a/src/protections/protections.ts b/src/protections/protections.ts index 61aa513..7218ba4 100644 --- a/src/protections/protections.ts +++ b/src/protections/protections.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/integration/commands/commandUtils.ts b/test/integration/commands/commandUtils.ts index 4b4fab6..22d0113 100644 --- a/test/integration/commands/commandUtils.ts +++ b/test/integration/commands/commandUtils.ts @@ -21,6 +21,9 @@ import * as crypto from "crypto"; try { client.on('room.event', addEvent) const targetEventId = await targetEventThunk(); + if (typeof targetEventId !== 'string') { + throw new TypeError(); + } for (let event of reactionEvents) { const in_reply_to = event.content['m.relates_to']?.['m.in_reply_to']; if (in_reply_to?.event_id === targetEventId) { diff --git a/test/integration/roomMembersTest.ts b/test/integration/roomMembersTest.ts new file mode 100644 index 0000000..c501de4 --- /dev/null +++ b/test/integration/roomMembersTest.ts @@ -0,0 +1,377 @@ +import { strict as assert } from "assert"; +import { SynapseRoomProperty } from "matrix-bot-sdk"; +import { RoomMemberManager } from "../../src/RoomMembers"; +import { newTestUser } from "./clientHelper"; +import { getFirstReply } from "./commands/commandUtils"; + +describe("Test: Testing RoomMemberManager", function() { + it("RoomMemberManager counts correctly when we call handleEvent manually", function() { + let manager: RoomMemberManager = this.mjolnir.roomJoins; + let start = new Date(Date.now() - 100_000_000); + const ROOMS = [ + "!room_0@localhost", + "!room_1@localhost" + ]; + for (let room of ROOMS) { + manager.addRoom(room); + } + + let joinDate = i => new Date(start.getTime() + i * 100_000); + let userId = i => `@sender_${i}:localhost`; + + // First, add a number of joins. + const SAMPLE_SIZE = 100; + for (let i = 0; i < SAMPLE_SIZE; ++i) { + const event = { + type: 'm.room.member', + state_key: userId(i), + sender: userId(i), + content: { + membership: "join" + } + }; + manager.handleEvent(ROOMS[i % ROOMS.length], event, joinDate(i)); + } + + { + const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000); + const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000); + + const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp])); + const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp])); + + for (let i = 0; i < SAMPLE_SIZE; ++i) { + const user = userId(i); + let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId; + const ts = map.get(user); + assert.ok(ts, `User ${user} should have been seen joining room ${i % 2}`); + assert.equal(ts, joinDate(i).getTime(), `User ${user} should have been seen joining the room at the right timestamp`); + map.delete(user); + } + + assert.equal(joins0ByUserId.size, 0, "We should have found all the users in room 0"); + assert.equal(joins1ByUserId.size, 0, "We should have found all the users in room 1"); + } + + // Now, let's add a few leave events. + let leaveDate = i => new Date(start.getTime() + (SAMPLE_SIZE + i) * 100_000); + + for (let i = 0; i < SAMPLE_SIZE / 3; ++i) { + const user = userId(i * 3); + const event = { + type: 'm.room.member', + state_key: user, + sender: user, + content: { + membership: "leave" + }, + unsigned: { + prev_content: { + membership: "join" + } + } + }; + manager.handleEvent(ROOMS[0], event, leaveDate(i)); + manager.handleEvent(ROOMS[1], event, leaveDate(i)); + } + + // Let's see if we have properly updated the joins/leaves + { + const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000); + const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000); + + const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp])); + const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp])); + + for (let i = 0; i < SAMPLE_SIZE; ++i) { + const user = userId(i); + let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId; + let isStillJoined = i % 3 != 0; + const ts = map.get(user); + if (isStillJoined) { + assert.ok(ts, `User ${user} should have been seen joining room ${i % 2}`); + assert.equal(ts, joinDate(i).getTime(), `User ${user} should have been seen joining the room at the right timestamp`); + map.delete(user); + } else { + assert.ok(!ts, `User ${user} should not be seen as a member of room ${i % 2} anymore`); + } + } + + assert.equal(joins0ByUserId.size, 0, "We should have found all the users in room 0"); + assert.equal(joins1ByUserId.size, 0, "We should have found all the users in room 1"); + } + + // Now let's make a few of these users rejoin. + let rejoinDate = i => new Date(start.getTime() + (SAMPLE_SIZE * 2 + i) * 100_000); + + for (let i = 0; i < SAMPLE_SIZE / 9; ++i) { + const user = userId(i * 9); + const event = { + type: 'm.room.member', + state_key: user, + sender: user, + content: { + membership: "join" + }, + unsigned: { + prev_content: { + membership: "leave" + } + } + }; + const room = ROOMS[i * 9 % 2]; + manager.handleEvent(room, event, rejoinDate(i * 9)); + } + + // Let's see if we have properly updated the joins/leaves + { + const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000); + const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000); + + const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp])); + const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp])); + + for (let i = 0; i < SAMPLE_SIZE; ++i) { + const user = userId(i); + let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId; + let hasLeft = i % 3 == 0; + let hasRejoined = i % 9 == 0; + const ts = map.get(user); + if (hasRejoined) { + assert.ok(ts, `User ${user} should have been seen rejoining room ${i % 2}`); + assert.equal(ts, rejoinDate(i).getTime(), `User ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`); + map.delete(user); + } else if (hasLeft) { + assert.ok(!ts, `User ${user} should not be seen as a member of room ${i % 2} anymore`); + } else { + assert.ok(ts, `User ${user} should have been seen joining room ${i % 2}`); + assert.equal(ts, joinDate(i).getTime(), `User ${user} should have been seen joining the room at the right timestamp`); + map.delete(user); + } + } + + assert.equal(joins0ByUserId.size, 0, "We should have found all the users in room 0"); + assert.equal(joins1ByUserId.size, 0, "We should have found all the users in room 1"); + } + + // Now let's check only the most recent joins. + { + const joins0 = manager.getUsersInRoom(ROOMS[0], rejoinDate(-1), 100_000); + const joins1 = manager.getUsersInRoom(ROOMS[1], rejoinDate(-1), 100_000); + + const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp])); + const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp])); + + for (let i = 0; i < SAMPLE_SIZE; ++i) { + const user = userId(i); + let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId; + let hasRejoined = i % 9 == 0; + const ts = map.get(user); + if (hasRejoined) { + assert.ok(ts, `User ${user} should have been seen rejoining room ${i % 2}`); + assert.equal(ts, rejoinDate(i).getTime(), `User ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`); + map.delete(user); + } else { + assert.ok(!ts, `When looking only at recent entries, user ${user} should not be seen as a member of room ${i % 2} anymore`); + } + } + + assert.equal(joins0ByUserId.size, 0, "We should have found all the users who recently joined room 0"); + assert.equal(joins1ByUserId.size, 0, "We should have found all the users who recently joined room 1"); + } + + // Perform a cleanup on both rooms, check that we have the same results. + for (let roomId of ROOMS) { + manager.cleanup(roomId); + } + + // Let's see if we have properly updated the joins/leaves + { + const joins0 = manager.getUsersInRoom(ROOMS[0], start, 100_000); + const joins1 = manager.getUsersInRoom(ROOMS[1], start, 100_000); + + const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp])); + const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp])); + + for (let i = 0; i < SAMPLE_SIZE; ++i) { + const user = userId(i); + let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId; + let hasLeft = i % 3 == 0; + let hasRejoined = i % 9 == 0; + const ts = map.get(user); + if (hasRejoined) { + assert.ok(ts, `After cleanup, user ${user} should have been seen rejoining room ${i % 2}`); + assert.equal(ts, rejoinDate(i).getTime(), `After cleanup, user ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`); + map.delete(user); + } else if (hasLeft) { + assert.ok(!ts, `After cleanup, user ${user} should not be seen as a member of room ${i % 2} anymore`); + } else { + assert.ok(ts, `After cleanup, user ${user} should have been seen joining room ${i % 2}`); + assert.equal(ts, joinDate(i).getTime(), `After cleanup, user ${user} should have been seen joining the room at the right timestamp`); + map.delete(user); + } + } + + assert.equal(joins0ByUserId.size, 0, "After cleanup, we should have found all the users in room 0"); + assert.equal(joins1ByUserId.size, 0, "After cleanup, we should have found all the users in room 1"); + } + + // Now let's check only the most recent joins. + { + const joins0 = manager.getUsersInRoom(ROOMS[0], rejoinDate(-1), 100_000); + const joins1 = manager.getUsersInRoom(ROOMS[1], rejoinDate(-1), 100_000); + + const joins0ByUserId = new Map(joins0.map(join => [join.userId, join.timestamp])); + const joins1ByUserId = new Map(joins1.map(join => [join.userId, join.timestamp])); + + for (let i = 0; i < SAMPLE_SIZE; ++i) { + const user = userId(i); + let map = i % 2 == 0 ? joins0ByUserId : joins1ByUserId; + let hasRejoined = i % 9 == 0; + const ts = map.get(user); + if (hasRejoined) { + assert.ok(ts, `After cleanup, user ${user} should have been seen rejoining room ${i % 2}`); + assert.equal(ts, rejoinDate(i).getTime(), `After cleanup, user ${user} should have been seen rejoining the room at the right timestamp, got ${ts}`); + map.delete(user); + } else { + assert.ok(!ts, `After cleanup, when looking only at recent entries, user ${user} should not be seen as a member of room ${i % 2} anymore`); + } + } + + assert.equal(joins0ByUserId.size, 0, "After cleanup, we should have found all the users who recently joined room 0"); + assert.equal(joins1ByUserId.size, 0, "After cleanup, we should have found all the users who recently joined room 1"); + } + }); + + afterEach(async function() { + await this.moderator?.stop(); + if (this.users) { + for (let client of this.users) { + await client.stop(); + } + } + }); + + it("RoomMemberManager counts correctly when we actually join/leave/get banned from the room", async function() { + this.timeout(60000); + const start = new Date(Date.now() - 10_000); + + // Setup a moderator. + this.moderator = await newTestUser({ name: { contains: "moderator" } }); + await this.moderator.joinRoom(this.mjolnir.managementRoomId); + + // Create a few users and two rooms. + this.users = []; + const SAMPLE_SIZE = 10; + for (let i = 0; i < SAMPLE_SIZE; ++i) { + this.users.push(await newTestUser({ name: { contains: `user_${i}_room_member_test` } })); + } + const userIds = []; + for (let client of this.users) { + userIds.push(await client.getUserId()); + } + const roomId1 = await this.moderator.createRoom({ + invite: userIds, + }); + const roomId2 = await this.moderator.createRoom({ + invite: userIds, + }); + const roomIds = [roomId1, roomId2]; + + for (let roomId of roomIds) { + await this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: `!mjolnir rooms add ${roomId}` }); + } + + let protectedRoomsUpdated = false; + do { + let protectedRooms = this.mjolnir.protectedRooms; + protectedRoomsUpdated = true; + for (let roomId of roomIds) { + if (!(roomId in protectedRooms)) { + protectedRoomsUpdated = false; + await new Promise(resolve => setTimeout(resolve, 1_000)); + } + } + } while (!protectedRoomsUpdated); + + + // Initially, we shouldn't know about any user in these rooms... except Mjölnir itself. + const manager: RoomMemberManager = this.mjolnir.roomJoins; + for (let roomId of roomIds) { + const joined = manager.getUsersInRoom(roomId, start, 100); + assert.equal(joined.length, 1, "Initially, we shouldn't know about any other user in these rooms"); + assert.equal(joined[0].userId, await this.mjolnir.client.getUserId(), "Initially, Mjölnir should be the only known user in these rooms"); + } + + const longBeforeJoins = new Date(); + + // Initially, the command should show that same result. + for (let roomId of roomIds) { + const reply = await getFirstReply(this.mjolnir.client, this.mjolnir.managementRoomId, () => { + const command = `!mjolnir status joins ${roomId}`; + return this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command }); + }); + const body = reply["content"]?.["body"] as string; + assert.ok(body.includes("\n1 recent joins"), "Initially the command should respond with 1 user"); + } + + // Now join a few rooms. + for (let i = 0; i < userIds.length; ++i) { + await this.users[i].joinRoom(roomIds[i % roomIds.length]); + } + + // Lists should have been updated. + for (let i = 0; i < roomIds.length; ++i) { + const roomId = roomIds[i]; + const joined = manager.getUsersInRoom(roomId, start, 100); + assert.equal(joined.length, SAMPLE_SIZE / 2 /* half of the users */ + 1 /* mjolnir */, "We should now see all joined users in the room"); + const reply = await getFirstReply(this.mjolnir.client, this.mjolnir.managementRoomId, () => { + const command = `!mjolnir status joins ${roomId}`; + return this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command }); + }); + const body = reply["content"]?.["body"] as string; + assert.ok(body.includes(`\n${joined.length} recent joins`), `After joins, the command should respond with ${joined.length} users`); + for (let j = 0; j < userIds.length; ++j) { + if (j % roomIds.length == i) { + assert.ok(body.includes(userIds[j]), `After joins, the command should display user ${userIds[j]} in room ${roomId}`); + } else { + assert.ok(!body.includes(userIds[j]), `After joins, the command should NOT display user ${userIds[j]} in room ${roomId}`); + } + } + } + + // Let's kick/ban a few users and see if they still show up. + const removedUsers = new Set(); + for (let i = 0; i < SAMPLE_SIZE / 2; ++i) { + const roomId = roomIds[i % roomIds.length]; + const userId = userIds[i]; + if (i % 3 == 0) { + await this.moderator.kickUser(userId, roomId); + removedUsers.add(userIds[i]); + } else if (i % 3 == 1) { + await this.moderator.banUser(userId, roomId); + removedUsers.add(userId); + } + } + + // Lists should have been updated. + + for (let i = 0; i < roomIds.length; ++i) { + const roomId = roomIds[i]; + const joined = manager.getUsersInRoom(roomId, start, 100); + const reply = await getFirstReply(this.mjolnir.client, this.mjolnir.managementRoomId, () => { + const command = `!mjolnir status joins ${roomId}`; + return this.moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text', body: command }); + }); + const body = reply["content"]?.["body"] as string; + for (let j = 0; j < userIds.length; ++j) { + const userId = userIds[j]; + if (j % roomIds.length == i && !removedUsers.has(userId)) { + assert.ok(body.includes(userId), `After kicks, the command should display user ${userId} in room ${roomId}`); + } else { + assert.ok(!body.includes(userId), `After kicks, the command should NOT display user ${userId} in room ${roomId}`); + } + } + } + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 427e54f..762fa7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,6 +70,11 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@napi-rs/cli@^2.2.0": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-2.4.4.tgz#878a38f0fba1709d89d66eba706745ce728a61a5" + integrity sha512-f+tvwCv1ka24dBqI2DgBhR7Oxl3DKHOp4onxLXwyBFt6iCADnr3YZIr1/2Iq5r3uqxFgaf01bfPsRQZPkEp0kQ== + "@selderee/plugin-htmlparser2@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz#27e994afd1c2cb647ceb5406a185a5574188069d" @@ -83,6 +88,14 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@turt2live/matrix-sdk-crypto-nodejs@^0.1.0-beta.10": + version "0.1.0-beta.10" + resolved "https://registry.yarnpkg.com/@turt2live/matrix-sdk-crypto-nodejs/-/matrix-sdk-crypto-nodejs-0.1.0-beta.10.tgz#9b0a8e1f48badeb37a0b0f8eb0fb6dc9bbb1949a" + integrity sha512-y5TA8fD5a7xaIwjZhQ66eT3scDsU47GkcCuQ0vjlXB0shY2cCMB4MF1nY/7c1/DniM+KvDXxrhs2VXphlPLpaA== + dependencies: + "@napi-rs/cli" "^2.2.0" + shelljs "^0.8.4" + "@types/body-parser@*": version "1.19.1" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c" @@ -117,7 +130,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.7": +"@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -298,6 +311,11 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" +another-json@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/another-json/-/another-json-0.2.0.tgz#b5f4019c973b6dd5c6506a2d93469cb6d32aeedc" + integrity sha1-tfQBnJc7bdXGUGotk0acttMq7tw= + ansi-colors@4.1.1, ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -434,6 +452,22 @@ body-parser@1.19.0: raw-body "2.4.0" type-is "~1.6.17" +body-parser@1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.1.tgz#1499abbaa9274af3ecc9f6f10396c995943e31d4" + integrity sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA== + dependencies: + bytes "3.1.1" + 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.6" + raw-body "2.4.2" + 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" @@ -474,6 +508,11 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" + integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -498,7 +537,7 @@ chalk@^2.0.0, chalk@^2.3.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4, chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -590,6 +629,13 @@ 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" @@ -605,6 +651,11 @@ cookie@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.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + 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" @@ -764,13 +815,6 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" -domhandler@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" - integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== - dependencies: - domelementtype "^2.0.1" - domhandler@^4.0.0, domhandler@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f" @@ -778,7 +822,7 @@ domhandler@^4.0.0, domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" -domutils@^2.0.0, domutils@^2.5.2: +domutils@^2.5.2: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== @@ -1000,7 +1044,7 @@ expect@^27.0.6: jest-message-util "^27.2.4" jest-regex-util "^27.0.6" -express@^4.17, express@^4.17.1: +express@^4.17: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== @@ -1036,6 +1080,42 @@ express@^4.17, express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +express@^4.17.2: + version "4.17.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.2.tgz#c18369f265297319beed4e5558753cc8c1364cb3" + integrity sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.4.1" + 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.6" + 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" @@ -1208,7 +1288,7 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1, glob@^7.1.3: +glob@^7.0.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== @@ -1287,17 +1367,6 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" -html-to-text@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-6.0.0.tgz#8b48adb1b781a8378f374c5bb481864a169f59f4" - integrity sha512-r0KNC5aqCAItsjlgtirW6RW25c92Ee3ybQj8z//4Sl4suE3HIPqM4deGpYCUJULLjtVPEP1+Ma+1ZeX1iMsCiA== - dependencies: - deepmerge "^4.2.2" - he "^1.2.0" - htmlparser2 "^4.1.0" - lodash "^4.17.20" - minimist "^1.2.5" - html-to-text@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-8.0.0.tgz#5848681a5a38d657a7bb58cf5006d1c29fe64ce3" @@ -1310,21 +1379,23 @@ html-to-text@^8.0.0: minimist "^1.2.5" selderee "^0.6.0" +html-to-text@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-8.1.0.tgz#0c35fc452e6eccb275669adb8bcc61d93ec43ed5" + integrity sha512-Z9iYAqYK2c18GswSbnxJSeMs7lyJgwR2oIkDOyOHGBbYsPsG4HvT379jj3Lcbfko8A5ceyyMHAfkmp/BiXA9/Q== + dependencies: + "@selderee/plugin-htmlparser2" "^0.6.0" + deepmerge "^4.2.2" + he "^1.2.0" + htmlparser2 "^6.1.0" + minimist "^1.2.5" + selderee "^0.6.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= -htmlparser2@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" - integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== - dependencies: - domelementtype "^2.0.1" - domhandler "^3.0.0" - domutils "^2.0.0" - entities "^2.0.0" - htmlparser2@^6.0.0, htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -1346,6 +1417,17 @@ 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" @@ -1383,6 +1465,11 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +humanize-duration-ts@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/humanize-duration-ts/-/humanize-duration-ts-2.1.1.tgz#5382b2789f851005a67229eaf031931d71f37ee9" + integrity sha512-TibNF2/fkypjAfHdGpWL/dmWUS0G6Qi+3mKyiB6LDCowbMy+PtzbgPTnFMNTOVAJXDau01jYrJ3tFoz5AJSqhA== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1426,6 +1513,11 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -1445,6 +1537,13 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -1659,11 +1758,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -klona@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" - integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1702,7 +1796,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@4, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.7.0: +lodash@4, lodash@^4.17.19, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1715,7 +1809,7 @@ log-symbols@4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" -lowdb@^1.0.0: +lowdb@^1: version "1.0.0" resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ== @@ -1746,25 +1840,25 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -matrix-bot-sdk@^0.5.19: - version "0.5.19" - resolved "https://registry.yarnpkg.com/matrix-bot-sdk/-/matrix-bot-sdk-0.5.19.tgz#6ce13359ab53ea0af9dc3ebcbe288c5f6d9c02c6" - integrity sha512-RIPyvQPkOVp2yTKeDgp5rcn6z/DiKdHb6E8c69K+utai8ypRGtfDRj0PGqP+1XzqC9Wb1OFrESCUB5t0ffdC9g== +"matrix-bot-sdk@file:../blurbs/matrix-bot-sdk": + version "99.0.2" dependencies: - "@types/express" "^4.17.7" - chalk "^4.1.0" - express "^4.17.1" + "@turt2live/matrix-sdk-crypto-nodejs" "^0.1.0-beta.10" + "@types/express" "^4.17.13" + another-json "^0.2.0" + chalk "^4" + express "^4.17.2" glob-to-regexp "^0.4.1" hash.js "^1.1.7" - html-to-text "^6.0.0" + html-to-text "^8.1.0" htmlencode "^0.0.4" - lowdb "^1.0.0" + lowdb "^1" lru-cache "^6.0.0" mkdirp "^1.0.4" morgan "^1.10.0" request "^2.88.2" request-promise "^4.2.6" - sanitize-html "^2.3.2" + sanitize-html "^2.6.1" media-typer@0.3.0: version "0.3.0" @@ -1901,20 +1995,15 @@ ms@2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanocolors@^0.2.2: - version "0.2.12" - 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.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.0.tgz#5906f776fd886c66c24f3653e0c46fcb1d4ad6b0" + integrity sha512-JzxqqT5u/x+/KOFSd7JP15DOo9nOoHpx6DYatqIHUW2+flybkm+mdcraotSQR5WcnZr+qhGVh8Ted0KdfSMxlg== natural-compare@^1.4.0: version "1.4.0" @@ -2058,7 +2147,7 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -2073,6 +2162,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" @@ -2083,14 +2177,14 @@ pify@^3.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= -postcss@^8.0.2: - version "8.3.8" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.8.tgz#9ebe2a127396b4b4570ae9f7770e7fb83db2bac1" - integrity sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA== +postcss@^8.3.11: + version "8.4.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" + integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== dependencies: - nanocolors "^0.2.2" - nanoid "^3.1.25" - source-map-js "^0.6.2" + nanoid "^3.2.0" + picocolors "^1.0.0" + source-map-js "^1.0.2" prelude-ls@^1.2.1: version "1.2.1" @@ -2117,7 +2211,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.5, proxy-addr@~2.0.7: 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== @@ -2145,6 +2239,11 @@ 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.6: + version "6.9.6" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" + integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" @@ -2185,6 +2284,16 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" + integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== + dependencies: + bytes "3.1.1" + 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" @@ -2197,6 +2306,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + regexpp@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" @@ -2260,6 +2376,15 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve@^1.1.6: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.3.2: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" @@ -2285,7 +2410,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.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: +safe-buffer@5.2.1, 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== @@ -2295,18 +2420,17 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@^2.3.2: - version "2.5.1" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.5.1.tgz#f49998dc54c8180153940440d3a7294b09e4258a" - integrity sha512-hUITPitQk+eFNLtr4dEkaaiAJndG2YE87IOpcfBSL1XdklWgwcNDJdr9Ppe8QKL/C3jFt1xH/Mbj20e0GZQOfg== +sanitize-html@^2.6.1: + version "2.7.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.0.tgz#e106205b468aca932e2f9baf241f24660d34e279" + integrity sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" htmlparser2 "^6.0.0" is-plain-object "^5.0.0" - klona "^2.0.3" parse-srcset "^1.0.2" - postcss "^8.0.2" + postcss "^8.3.11" saxes@^5.0.1: version "5.0.1" @@ -2353,6 +2477,25 @@ 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" @@ -2370,11 +2513,26 @@ 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" @@ -2387,6 +2545,15 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shelljs@^0.8.4: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + sigmund@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" @@ -2406,10 +2573,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -source-map-js@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" - integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== source-map-support@^0.5.6: version "0.5.20" @@ -2515,6 +2682,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -2549,6 +2721,11 @@ 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"